前端面试高频知识点 - React 篇
1. setState
1.1. 调用 setState 之后发生了什么
- 将传入的参数与组件当前的状态合并
- 构建新的 React 元素树并且计算出新树与老树的差异
- 根据差异对界面进行最小化重新渲染
1.2. setState 执行时机
React 18 之前
- 在合成事件和生命周期中,React 会在执行栈末尾批量更新 state
- 在原生事件和异步任务(promise.then()、setTimeout()、ajax)中,同步更新 state
可通过 ReactDom.unstable_batchedUpdates 来手动批量更新 state,以优化渲染次数。常见场景:通过 axios 调用接口完成后的 .then 回调函数中,多次执行复杂、性能消耗大的 setState 渲染。
React 18
- 任意场景都将自动批量更新 state,无需再手动处理。
若一定需要同步执行 setState,可通过 flushSync,将 setState 放入 flushSync 的回调函数中,强制同步更新。
2. 生命周期函数
2.1. 页面整个渲染过程中,常见生命周期函数执行顺序
挂载过程
- constructor()
- render()
- componentDidMount()
更新过程
- shouldComponentUpdate(nextProps, nextState)
- render()
- componentDidUpdate(prevProps, prevState)
卸载过程
- componentWillUnmount()
2.2. 父子组件生命周期函数执行顺序遵循的原则
- 父组件 render() 后子组件才开始加载
- 子组件 componentDidMount() 后父组件再 componentDidMount()
3. React Hooks 注意点
useEffect 等绝大多数 hooks API 的第二个参数依赖项数组,该数组对其中每个元素的比较是浅比较,如果是引用类型则比较其地址,比较所采用的方法 Object.is()。(因此建议依赖项数组里的每个元素为基本类型,若为引用类型,则应保证每次改变值时重新创建一个对象,否则可能导致本应执行 hooks 的那次渲染却没有执行)。
4. useEffect 执行时机及其与 useState 的关系
刚开始使用 React Hooks 时,对 Hooks 不熟悉,我们或多或少会遇到 useEffect 闭包问题,也就是在 useEffect 中取到的状态不是我们想要的值,这些问题通常不容易理解,但是,只要我们掌握了下面 useEffect 的部分原理,这种问题就能迎刃而解,还能让我们书写的代码更加优雅。
- useEffect 在页面渲染完成后才会执行,先执行函数体和 return 后再执行 useEffect,不会阻塞浏览器渲染。
- 多个 useEffect 的调用顺序取决于代码书写的顺序。
- 在 useEffect 内返回一个函数来清除副作用时,执行顺序如下:
- 挂载阶段:执行 useEffect 函数体,并将返回的清除副作用函数保存,在下次 useEffect 前执行
- 更新阶段:先执行上一次 useEffect 返回的清除副作用函数,再执行本次的 useEffect 函数体
- 卸载阶段:执行最后一次 useEffect 返回的清除副作用函数
e.g. 可参考在 useEffect 中设置定时器,并在返回的清除副作用函数里,取消定时器。
- 所有 useEffect 都执行完后才会更新 use 的引用(例如其他 useEffect 代码中使用的 state 值等)
- 当 state 逻辑复杂,或 state 是个对象,或一个 state 依赖于另一个 state 时,可通过以下办法保证 useEffect 中 state 值为最新:
- 使用回调函数形式的 setState(setCount(x => x + 1))
- 使用 useReducer 管理状态,将处理状态的逻辑收敛于 reducer 函数中,避免状态混乱
- 使用 flushSync 强制更新(不推荐)
5. 使用 useRef 保存上一次 props 或 state
理解完 useEffect 的执行时机后,我们便可以轻松的实现 usePrevious,来获取上一次的 props 或 state。
import { useEffect, useRef } from 'react';
export function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
- useEffect 在 return 完成后才执行,因此我们返回的仍然是上一轮的 current 值。
- useRef 返回的 ref 对象在组件的整个生命周期内保持不变,变更 .current 属性不会引发组件重新渲染。
6. React 代码优化
- 通过 React.lazy 和 Suspense 对路由组件进行懒加载。
- 类组件使用 React.PureComponent 或 shouldComponentUpdate 避免重复渲染,函数组件使用 React.memo 包裹。
- 复杂的计算值使用 useMemo 缓存,需要传递给其他组件的方法使用 useCallback 包裹。
- 将修改 DOM 的操作放到 useLayoutEffect 里,useLayoutEffect 会在所有的 DOM 变更之后、浏览器执行重绘之前同步调用 effect(与 componentDidMount、componentDidUpdate 的调用阶段相同,都会阻塞浏览器渲染),放在 useLayoutEffect 里只有一次重绘的代价,放在 useEffect 里,会在浏览器渲染到屏幕后再次触发重绘,增加性能损耗。
- 使用 useTransition 将高消耗的状态转换标记为非阻塞,允许其他更新中断它(React 18)。
- 使用 useDeferredValue 获取某个状态的延迟值,延迟更新依赖该状态的用户界面非重要部分(React 18)。
7. 原生事件与合成事件
7.1. 原生事件
指 js 原生事件,如通过 document.addEventListener 设置的监听事件。
7.2. 合成事件
react 自身实现的一套事件机制(jsx 中绑定的事件)。react 会将所有合成事件,以事件委托的形式,绑定到 root 节点(在 document 节点下),用一个统一的监听器去监听处理。
7.3. 注意事项
- 冒泡或捕获到 root 节点时,才会执行 react 合成事件。
- 因 root 节点在 document 节点下,所以一般情况下,在合成事件中执行 e.stopPropagation() 可以阻止合成事件冒泡,但不能阻止原生事件;在原生事件中执行 e.stopPropagation(),会同时阻止原生事件和合成事件冒泡。尽量避免合成事件和原生事件混用。
8. React 元素的 key
当使用 Array.map() 等方法循环遍历 React 元素时,不加 key 或者 key 使用 index 赋值都会产生一定问题
- 不加 key:非受控的表单的值不符合预期,因为无法正确识别列表中元素变化,对于非受控的表单,React 无法知道表单的变化,也就没法正确地更新其值(比如 antd 表格的 checkbox)。
- 使用 index 作为 key:性能损耗,当列表中新增或删除元素时,无法正确识别元素变化,可能做一些多余的更新操作,造成性能损失。
9. React Diff
- 当一个组件触发更新(setState),React 会 diff 以这个组件为根节点的整个虚拟 DOM 树,对比更新后的虚拟 DOM 和更新前的虚拟 DOM。
- 如果两个元素的类型相同(原生标签 tag 名或 react 组件),且 key 和 prop 相同,则 React 认为它们是同一个节点。
- 从 diff 的根节点开始,按层遍历,直到对比完整个 DOM 树。
虚拟 DOM 的优点:将所有真实的 DOM 更新一次性批处理完成,避免反复操作 DOM,造成性能损耗。
10. React Fiber
10.1. 原理
React 将渲染任务转化成 fiber 树,每个 fiber 节点都是一个执行单元,渲染过程可以中断。按照浏览器每秒 60 帧的刷新率,一帧就是 16ms,在这一帧中浏览器要进行下列任务:处理用户输入、js 执行、requestAnimation 调用、布局 Layout、绘制 Paint,如果执行完以上任务后还有剩余时间,就去执行 fiber,执行完一个 fiber 后还有剩余时间就继续执行下一个 fiber,如果剩余时间不足,就让出控制权,在下一帧有空闲时接着上次的任务继续执行。
react 自己实现了一个类似 chrome 浏览器的 requestIdleCallback API,用来注册在下一帧空闲时间要执行的回调函数。第一个参数是回调函数,这个回调函数接收一个方法,返回当前帧的剩余空闲时间和任务执行是否超时,第二个参数是超时时间,在这个超时时间到期的时候,不管有没有空闲都必须执行当前任务。
总结:
- 将渲染任务拆分成 fiber 执行单元,在浏览器每一帧空闲的时候执行。
- 任务可中断,优先处理高优先级的任务。
10.2. fiber 的工作流程
- reconcile 阶段
- 本阶段可中断、挂起、恢复。
- 生成两棵树,一颗代表当前状态的 current tree,一颗代表待更新的 workInProgress tree,遍历 current tree,更新 fiber 节点到待更新树(两棵树目的:中断更新时恢复到之前的状态,只需将指针指向 current tree 即可)。
- 每更新一个节点,生成该节点对应的 Effect List。
- commit 阶段
- 遍历 Effect List 将所有变更一次性更新到 DOM 上。
- 这一阶段不可中断,因为会导致用户可见的变化,必须一次性执行完成。
10.3. fiber 为什么可中断?
- 每个 fiber 节点会记录自己的父节点、子节点和兄弟节点。
- 更新时生成两棵树,一颗为当前状态树,一颗为待更新树,执行时更新 fiber 节点到待更新树,中断时只需将指针指向当前状态树即可。
11. React 18 新特性
- 并发模式:fiber 节点执行划分优先级,可通过代码标记哪些更新优先级较低、可中断(useTranstion、useDeferredValue)。
- 自动批处理:任意场景都将接管 setState,进行批处理更新。