# 2020-100 道面试题

# 6: React 项目中有哪些细节可以优化,实际开发中都做过哪些性能优化

对于正常的项目优化,一般涉及到几个方面:开发过程中、上线之后的首屏、运行过程的状态

  • 上线后的首屏及运行状态

    • 首屏的优化涉及到几个指标:FP、FCP、FMP;要有一个良好的体验是尽可能的把 FCP 提前,需要做一些工程化的处理,去优化资源的加载
    • 方式及分包策略,资源的减少是最有效的加快首屏打开方式
    • 对于 CSR 应用,FCP 的过程一般是首先加载 js 与 css 资源,js 在本地执行完成,然后加载数据回来,做内容初始化渲染,这中间就有几次网络反复请求的过程,所以 CSR 可以考虑使用骨架屏及预渲染(部分结构预渲染),suspence 与 lazy 做懒加载动态组件的方式
    • 当然还有另一种方式就是 SSR 的方式,SSR 对于首屏的优化有一定的优势,但这种瓶颈一般在 Node 服务端的处理,建议使用 stream 流的方式来处理,对于体验与 node 端的内存管理等,都有优势。
    • 不管对于 CSR 还是 SSR,都建议配合使用 ServiceWorker,来控制资源的调配及骨架屏秒开的体验
    • react 项目上线后,首先需要保障可用性,所以可以通过 React.Profiler 分析组件渲染次数及耗时的一些任务,但 Profile 记录的是 commit 阶段的数据,所以对于 react 的调和阶段需要结合 performance API 一起分析。
    • 由于 React 是父级 props 改变之后,所有与 props 不相关的子组件在没有添加条件控件的情况下,也会触发 render 的渲染,这是没必要的,可以结合 React 的 PureComponent 以及 React.memo 等做浅处理,这中间有涉及到不可变数据的处理,当然也可以结合使用 ShouldComponentUpdate 做深比较处理。
    • 所有的运行状态优化,都是减少不必要的 render,React.useMemo 与 React.useCallback 也是可以做很多优化地方
    • 在很多应用中,都会涉及到使用 redux 以及使用 context,这两个都可能造成许多不必要的 render,所以在使用的时候,也需要谨慎处理一些数据
    • 最后就是保证整个应用的可用性,为组件创建错误边界,可以使用 componentDidCatch 来处理
  • 实际项目中开发过程中还有很多其他优化点:

      1. 保证数据的不可变性
      1. 使用唯一的键值迭代
      1. 使用 web worker 做密集型的任务处理
      1. 不在 render 中处理数据
      1. 不必要的标签,使用 React.Fragments
SSR:【服务端渲染】 CSR【客户方渲染】
FP(first paint): 首次绘制
FCP(first contentful paint): 首次内容绘制
LCP(Largest contentful paint): 最大内容渲染
DCL(DomContentloaded)
FMP(First Meaningful Paint): 首次有效绘制
L(onload)
TTL(Time to Interactive): 可交互时间
TBT(Total Blocking Time): 页面阻塞总时长
FID(First Input Delay): 首次输入延迟
CLS(Cumulative Layout Shift): 累积布局偏移
SI(Speed Index)

# 7. react 最新版本解决了什么问题,加了哪些东西,去了哪些东西

    1. React 16.x 去掉了componentWillMountcomponentWillReceivePropscomponentsWillUpdata三个生命周期,为弥补失去上面三个生命周期的不足,又加了两个,static getDerivedStateFromPropsgetSnapshotBeforeUpdate,为啥要改,因为新的 React 用 Fiber 优化了算法,当有紧急的事情时,会打断渲染,处理完会从头渲染组件,不能保证只在挂载/拿到 props/状态变化的时候刷新一次,所以被标记为 Unsafe。
    1. React 16.x 的三大新特性 Time Slicing, Suspense, hooks
    • i. Time Slicing(解决 CPU 速度问题),使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然能保持良好的性能
    • ii. Suspense(解决网络 IO 问题)和 lazy 配合,实现异步加载组件。能暂停当前组件的渲染,当完成某件事以后在继续渲染,解决从 react 出生到现在都存在的【异步副作用】的问题,并且解决得非常优雅,使用的是【异步但是同步的写法】,个人认为,这是最好的解决异步问题的方式
    • iii. 此外,还提供了一个内置函数 componentDidCatch,当有错误发生时,我们可以友好地展示 fallback 组件;可以捕捉到它的子元素(包括嵌套子元素)抛出的异常,可以复用错误组件
    1. React16.8
    • 加了 hooks,让 React 函数式组件更加灵活
    • hooks 之前,React 存在很多问题
      • a. 在组件间复用状态逻辑很难
      • b. 复杂组件变得难以理解,高阶组件和函数组件嵌套过深
      • c. class 组件的 this 指向问题
      • d. 难以记忆的生命周期
    • hooks 很好的解决了上述问题,hooks 提供很多方法
      • a. useState 返回有状态值,以及更新这个状态值的函数
      • b. useEffect 接受包含命令的方式,可能有副作用代码的函数
      • c. useContext 接受上下文对象(从 React.createContext 返回值)并返回当前上下文值
      • d. useReducer useState 的替代方案,接受类型为(state, action)=> newState 的 reducer,并返回与 dispatch 方法配对的当前状态。
      • e. useCallback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性。
      • f. useMemo 纯的记忆函数
      • g. useRef 返回一个可变的 ref 对象,其.current 属性被初始化为传递的参数,返回 ref 对象在组件的整个生命周期内保持不变
      • h. useImperativeMethods 自定义使用 ref 时公开给父组件的实例值
      • i. useMutationEffect 更新兄弟组件之前,它在 react 执行其 DOM 改变的同一阶段同步触发
      • j. useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染
    1. React16.9
    • i. 重命名 Unsafe 生命周期方法,新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
    • ii. 废弃 javascript: 形式的 URL,以 javascript 开头的 URL 非常容易遭受攻击,造成安全漏洞。
    • iii. 废弃Factory组件,工厂组件会导致 React 变大且变慢
    • iv. act()也支持异步函数,并且可以在调用它时使用 await
    • v. 使用<React.Profiler>进行性能评估,在较大的应用中追踪性能回归可能会很方便
    1. React16.13.0
    • i. 支持在渲染期间调用 setState,但仅适用于同一组件
    • ii. 可检测冲突样式规则并记录警告
    • iii. 废弃 unstatble_createPortal,使用 createPortal
    • iv. 将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,更快的定位和修复错误

# 17. React 事件绑定原理

React 并不是将 click 事件绑定在该 div 的真实 DOM 上,而是在 document 处监听所有支持的事件,当事件发生并冒泡至 document 处时,react 将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少内存消耗,还能在组件挂载销毁时统一订阅和移除事件。 另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现合成事件(SyntheticEvent). 因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。

    1. 事件注册
    • 组件装载/更新
    • 通过 lastProps、nextProps 判断是否新增、删除事件分别调用事件注册、卸载方法
    • 通过 EventPluginHub 的 enqueuePutListener 进行事件存储
    • 获取 document 对象
    • 根据事件名称(如 onClick、onCaptureClick)判断是进行冒泡还是捕获
    • 判断是否存在 addEventListener 方法,否则使用 attachEvent(兼容 IE)
    • 给 document 注册原生事件回调为 dispatchEvent(统一的事件分发机制)
    1. 事件存储
    • EventPluginHub 负责管理 React 合成事件 callback,它将 callback 存储在 listenerBank 中,另外还存储了负责合成事件的 Plugin
    • EventPluginHub 的 putListener 方法是向存储容器中增加一个 listener
    • 获取绑定事件的元素的唯一标识 key
    • 将 callback 根据事件类型,元素的唯一标识 key 存储在 listenerBank 中
    • listenerBank 的结构是:listenerBank[registrationName][key]
    1. 事件触发执行
    • 触发 document 注册原生事件的回调 dispatchEvent
    • 获取到触发这个事件最深一级的元素
      • 这里的事件执行利用了 React 的批处理机制
<div onClick={this.parentClick} ref={(ref) => (this.parent = ref)}>
  <div onClick={this.childClick} ref={(ref) => (this.child = ref)}>
    test
  </div>
</div>
/**
 * 首先会获取到this.child
 * 遍历这个元素的所有父元素,依次对每一级元素进行处理
 * 构造合成事件
 * 将每一级的合成事件存储在eventQueue事件队列中
 * 遍历eventQueue
 * 通过isPropagationStopped判断当前事件是否执行了阻止冒泡方法
 * 如果阻止了冒泡,停止遍历,否则通过executeDispatch执行合成事件
 * 释放处理完成的事件
 */
    1. 合成事件
    • 调用 EventPluginHub 的 extractEvents 方法
    • 循环所有类型的 EventPlugin(用来处理不同事件的工具方法)
    • 在每个 EventPlugin 中根据不同的事件类型,返回不同的事件池
    • 在事件池中取出合成事件,如果事件池为空,则创建一个新的
    • 根据元素 nodeid(唯一标识 key)和事件类型从 listenerBink 中取出回调函数
    • 返回带有合成事件参数的回调函数

# 25. React 组件通信方式

react 组件间通信通常有以下几种情况

  • i. 父组件向子组件通信
  • ii. 子组件向父组件通信
  • iii. 跨级组件通信
  • iv. 非嵌套关系的组件通信
    • a. 可以使用自定义事件通信(发布订阅模式)
    • b. 可以通过 redux 等进行全局状态管理
    • c. 如果是兄弟组件通信,可以找到两个兄弟的共同父节点,结合父子间通信方式进行通信
// i. 父组件向子组件通信
const Child = (props) => {
  return <p>{props.name}</p>;
};
const Parent = () => {
  return <Child name="baba" />;
};
// ii. 子组件向父组件通信: props+回调方式
const Child = (props) => {
  const cb = (msg) => {
    return () => {
      props.callback(msg);
    };
  };
  return <button onClick={cb("baba")}>click me</button>;
};
const Paret = ()=>{
  callback(msg){
    console.log(msg);
  }
  render(){
    return <Child callback = {this.callback.bind(this)}/>
  }
}
// iii. 跨级组件通信 即: 父组件向组件通信,向更深层子组件通信
// 3.1 使用props利用中间组件传递,果然过深会增加复杂度,并且这些props并不是中间组件自己想要的
// 3.2 使用context,context相当于一个大容器,可以把要通信的内容放到容器里,contex📱目的就是为了共享对于一个组件树而言是全局的数据
const BatteryContext = createConext();
// 子组件的子组件
class GrandChild extends Component{
  render(){
    return (
      <BatteryContext.Consumer>
        {
          color=> <h1 style={{color: color}}>我的颜色: {color}</h1>
        }
      </BatterConntext.Consumer>
    )
  }
}
// 子组件
class Child = ()=>{
  return (
    <GrandChild />
  )
}
// 父组件
class Parant extends Component{
  state = {
    color: "red"
  }
  render(){
    const {color} = this.state;
    return (
      <ButteryContext.Provider value={color}>
        <Child />
      </ButteryContext.Provider>
    )
  }
}

# 26. redux-sagamobx的比较

saga 是 redux 处理异步的一种方式。saga 需要一个全局监听器(watcher saga),用于监听组件发出 action,将监听的 action 转发给对应的接收器(workersaga),再由接收器执行具体任务,任务完了在发出另一个 action 交由 reducer 修改 state. mobx 与 redux 的功能相似,mobx 实现思想和 Vue 几乎一样,所以其优点跟 Vue 也差不多:通过监听数据(对象,数组)的属性变化,可以通过直接在数据上更改就能触发 UI 渲染,从而做到MVVM、响应式、上手成本低、开发效率高。

  • redux 将数据保存在单一的 store 中,mobx 将数据保存在分散的多个 store 中
  • redux 使用plain object保存数据,需要手动处理变化后的操作,mobx 使用 observeble 保存数据,数据变化后自动处理响应操作。
  • redux 使用不可变状态,这意味着状态是只读的,不能直接去修改他,而是应该返回一个新的状态,同时使用纯函数;mobx 中状态是可变的,可直接对其进行修改。
  • mobx 相对来说比较简单,在其中有很多抽象,mobx 更多的使用面向对象的编程思维;redux 会比较复杂,因为其中的函数式编程思想掌握起来不太容易,同时需要借助一系列的中间件来处理异常和副作用
  • mobx 中有更多的抽象和封装,调试会比较困难,同时结果难以预测,而 redux 提供能够进行时间回溯的开发工具,同时其纯函数以及更少的抽象,让调试变的更加容易。
    1. 状态管理
    • redux-sageredux的一个异步处理中间件
    • mobx是数据管理库,和 redux 一样
    1. 设计思想
    • redux-sage属于 flux 体系,函数式编程思想
    • mobx不属于 flux 体系,面向对象编程和响应式编程
    1. 主要特点
    • redux-sage因为是中间件,更关注异步处理的,通过 Genetator 函数来将异步变为同步,使代码可读性高,结构清晰。action 也不是action creator而是pure action
    • Generator函数中通过callput方法直接声明式调用,并自带一些方法,如takeEverytakeLastrace等,控制多异步操作,让多个异步更简单
    • mobx是更简单方便灵活的处理数据,store 是包含了 state 和 action。state 包装成一个可被视察的对象,action 可以直接修改 state,之后通过Computed values将依赖 state 的计算属性更新,之后触发 Reactions 响应依赖 state 的变更,输出相应的副作用,但不生成新的 state.
    1. 数据可变性
    • redux-sage强调 state 不可变,不能直接操作 state,通过 action 和 reducer 在原来的 state 基础上返回一个新的 state 达到改变 state 的目的
    • mobx直接在方法中更改 state, 同时所有使用 state 都发生变化,不生成新的 state
    1. 写法难易度
    • redux-sagareduxactionnreducer上要简单一些,需要用 dispatch 触发 state 的改变,需要 mapStateToProps 订阅 state
    • mobx 在非严格模式下不用 action 和 reducer,在严格模式下需要 action 中修改 state, 并自动触发相关的依赖更新
    1. 使用场景
    • redux-sage很好的解决了 redux 关于异步处理时的复杂度和代码冗余的问题,数据流向好追踪。但 redux 的学习成本比较高,代码比较冗余,不是特别需要状态管理,可以用别的替换
    • mobx学习成本低,能快速上手,代码比较简洁,但可能因为代码编写的原因和数据更新时相对黑盒,导致数据流向不利于追踪。

# 27. 说一下 react-fiber

  • 背景
    • react 在进行组件渲染时,从 setState 开始到渲染完成整个过程是同步的(一气呵成)。如果需要渲染的组件比较庞大,js 执行会占据主线程事件较长,会导致页面响应度变差,使得 react 在动画、手势等应用中效果比较差。
    • 页面卡顿:Stackreconciler 的工作流程很像函数调用过程,父组件里调用子组件,可以类比为函数递归,对于特别庞大的 vDOM 树来说,reconciliatio 过程会很长(x00ms),超过 16ms,在这期间,主线程被 js 占用,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住。
  • 实现原理
    • 旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而 Fiber 实现了自己的组件调用栈,它以链表的修饰遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的 requesIdleCallback 这个 api
    • Fiber 其实指的是一种数据结构,它可以用一个纯的 js 对象来表示:
    const fiber = {
      stateNode, // 节点实例
      child, // 子节点
      sibling, // 兄弟节点
      return, // 父节点
    };
    
  • react 内部运转分为三层:
    • Virtual DOM 层,描述页面长什么样子
    • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等
    • Renderer 层,根据不同平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative
  • 为实现不卡顿,就需要有一个调度器(Scheduler)来进行任务分配。优先级高的任务(如:键盘输入)可以打断优先级低的任务(如:Diff)的执行,从而更快的生效。任务的优先级有六种
    • synchronous 与之前的 StackReconciler 操作一样,同步执行
    • task:在 next tick 之前执行
    • animation:下一帧之前执行
    • high:在不久的将来执行
    • low:稍微延迟执行也没关系
    • offscreen 下一次 render 或 scroll 时执行
  • Fiber Reconciler(react)执行阶段
    • 阶段一:生成 Fiber 树,得出需要更新的节点信息,这一步是一个渐进的过程,可以被打断
    • 阶段二:将需要更新的节点一次过批量更新,这个过程不能被打断
  • Fiber 树:Fiber Reconciler 在阶段一进行 Diff 计算的时候,会基于 VirtualDOM 树生成一课 Fiber 树,它的本质是链表
  • 从 Stack Reconciler 到 FiberReconciler,源码层面其实就是干了一件递归改循环的事情。

# 42. 说一下 React Hooks 在平时开发中需要注意的问题及原因

    1. 不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 hook
    • 这是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数,一旦在循环或条件语句中调用 Hook,就容易导致调用顺序不一致性,从而产生难以预料的后果。
    1. 使用useState时候,使用 push,pop,splice 等直接更改数组对象的坑
    • 使用 push 直接更改数组无法获取到最新的值,应该采用析构方式,但在 class 里不会有这个问题
    function Indicatorfilter() {
      let [num, setNums] = useState([0, 1, 2, 3]);
      const test = () => {
        // 这里的坑是直接采用push去更新num,setNums(num)是无法更新num的
        // 必须使用 num = [...num, 1] setNums(num);
        num.push(1);
        setNums(num);
      };
      return (
        <div className="filter">
          <button onClick={test}>click test</button>
          <p>{num.join()}</p>
        </div>
      );
    }
    
  • useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
// error
const TableDetail = ({ columns }, TableData) => {
  const [tabColumn, setTabColumn] = useState(columns);
};

// success
const TableDetail = ({ columns }: TableData) => {
  const [tabColumn, setTabColumn] = useState(columns);
  useEffect(() => {
    setTabColumn(columns);
  }, [columns]);
};
    1. 善用useCallback
    • useCallback父组件传递子组件事件句柄时,如果没有任何参数变动的组件即使用 useMeno,也会跟着渲染一次
    1. 不要滥用useContext,可以使用基于useContext封装的状态管理工具