前端面试高频知识点 - React 篇

1. setState

1.1. 调用 setState 之后发生了什么

  1. 将传入的参数与组件当前的状态合并
  2. 构建新的 React 元素树并且计算出新树与老树的差异
  3. 根据差异对界面进行最小化重新渲染

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. 页面整个渲染过程中,常见生命周期函数执行顺序

挂载过程
  1. constructor()
  2. render()
  3. componentDidMount()
更新过程
  1. shouldComponentUpdate(nextProps, nextState)
  2. render()
  3. componentDidUpdate(prevProps, prevState)
卸载过程
  1. componentWillUnmount()

2.2. 父子组件生命周期函数执行顺序遵循的原则

  1. 父组件 render() 后子组件才开始加载
  2. 子组件 componentDidMount() 后父组件再 componentDidMount()

3. React Hooks 注意点

useEffect 等绝大多数 hooks API 的第二个参数依赖项数组,该数组对其中每个元素的比较是浅比较,如果是引用类型则比较其地址,比较所采用的方法 Object.is()。(因此建议依赖项数组里的每个元素为基本类型,若为引用类型,则应保证每次改变值时重新创建一个对象,否则可能导致本应执行 hooks 的那次渲染却没有执行)。

4. useEffect 执行时机及其与 useState 的关系

刚开始使用 React Hooks 时,对 Hooks 不熟悉,我们或多或少会遇到 useEffect 闭包问题,也就是在 useEffect 中取到的状态不是我们想要的值,这些问题通常不容易理解,但是,只要我们掌握了下面 useEffect 的部分原理,这种问题就能迎刃而解,还能让我们书写的代码更加优雅。

  1. useEffect 在页面渲染完成后才会执行,先执行函数体和 return 后再执行 useEffect,不会阻塞浏览器渲染。
  2. 多个 useEffect 的调用顺序取决于代码书写的顺序。
  3. 在 useEffect 内返回一个函数来清除副作用时,执行顺序如下:
    • 挂载阶段:执行 useEffect 函数体,并将返回的清除副作用函数保存,在下次 useEffect 前执行
    • 更新阶段:先执行上一次 useEffect 返回的清除副作用函数,再执行本次的 useEffect 函数体
    • 卸载阶段:执行最后一次 useEffect 返回的清除副作用函数

    e.g. 可参考在 useEffect 中设置定时器,并在返回的清除副作用函数里,取消定时器。

  4. 所有 useEffect 都执行完后才会更新 use 的引用(例如其他 useEffect 代码中使用的 state 值等)
  5. 当 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;
}
  1. useEffect 在 return 完成后才执行,因此我们返回的仍然是上一轮的 current 值。
  2. useRef 返回的 ref 对象在组件的整个生命周期内保持不变,变更 .current 属性不会引发组件重新渲染。

6. React 代码优化

  1. 通过 React.lazy 和 Suspense 对路由组件进行懒加载。
  2. 类组件使用 React.PureComponent 或 shouldComponentUpdate 避免重复渲染,函数组件使用 React.memo 包裹。
  3. 复杂的计算值使用 useMemo 缓存,需要传递给其他组件的方法使用 useCallback 包裹。
  4. 将修改 DOM 的操作放到 useLayoutEffect 里,useLayoutEffect 会在所有的 DOM 变更之后、浏览器执行重绘之前同步调用 effect(与 componentDidMount、componentDidUpdate 的调用阶段相同,都会阻塞浏览器渲染),放在 useLayoutEffect 里只有一次重绘的代价,放在 useEffect 里,会在浏览器渲染到屏幕后再次触发重绘,增加性能损耗。
  5. 使用 useTransition 将高消耗的状态转换标记为非阻塞,允许其他更新中断它(React 18)。
  6. 使用 useDeferredValue 获取某个状态的延迟值,延迟更新依赖该状态的用户界面非重要部分(React 18)。

7. 原生事件与合成事件

7.1. 原生事件

指 js 原生事件,如通过 document.addEventListener 设置的监听事件。

7.2. 合成事件

react 自身实现的一套事件机制(jsx 中绑定的事件)。react 会将所有合成事件,以事件委托的形式,绑定到 root 节点(在 document 节点下),用一个统一的监听器去监听处理。

7.3. 注意事项

  1. 冒泡或捕获到 root 节点时,才会执行 react 合成事件。
  2. 因 root 节点在 document 节点下,所以一般情况下,在合成事件中执行 e.stopPropagation() 可以阻止合成事件冒泡,但不能阻止原生事件;在原生事件中执行 e.stopPropagation(),会同时阻止原生事件和合成事件冒泡。尽量避免合成事件和原生事件混用

8. React 元素的 key

当使用 Array.map() 等方法循环遍历 React 元素时,不加 key 或者 key 使用 index 赋值都会产生一定问题

  • 不加 key:非受控的表单的值不符合预期,因为无法正确识别列表中元素变化,对于非受控的表单,React 无法知道表单的变化,也就没法正确地更新其值(比如 antd 表格的 checkbox)。
  • 使用 index 作为 key:性能损耗,当列表中新增或删除元素时,无法正确识别元素变化,可能做一些多余的更新操作,造成性能损失。

9. React Diff

  1. 当一个组件触发更新(setState),React 会 diff 以这个组件为根节点的整个虚拟 DOM 树,对比更新后的虚拟 DOM 和更新前的虚拟 DOM。
  2. 如果两个元素的类型相同(原生标签 tag 名或 react 组件),且 key 和 prop 相同,则 React 认为它们是同一个节点。
  3. 从 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,用来注册在下一帧空闲时间要执行的回调函数。第一个参数是回调函数,这个回调函数接收一个方法,返回当前帧的剩余空闲时间和任务执行是否超时,第二个参数是超时时间,在这个超时时间到期的时候,不管有没有空闲都必须执行当前任务。

总结:

  1. 将渲染任务拆分成 fiber 执行单元,在浏览器每一帧空闲的时候执行。
  2. 任务可中断,优先处理高优先级的任务。

10.2. fiber 的工作流程

  1. reconcile 阶段
  • 本阶段可中断、挂起、恢复。
  • 生成两棵树,一颗代表当前状态的 current tree,一颗代表待更新的 workInProgress tree,遍历 current tree,更新 fiber 节点到待更新树(两棵树目的:中断更新时恢复到之前的状态,只需将指针指向 current tree 即可)。
  • 每更新一个节点,生成该节点对应的 Effect List。
  1. commit 阶段
  • 遍历 Effect List 将所有变更一次性更新到 DOM 上。
  • 这一阶段不可中断,因为会导致用户可见的变化,必须一次性执行完成。

10.3. fiber 为什么可中断?

  1. 每个 fiber 节点会记录自己的父节点、子节点和兄弟节点。
  2. 更新时生成两棵树,一颗为当前状态树,一颗为待更新树,执行时更新 fiber 节点到待更新树,中断时只需将指针指向当前状态树即可。

11. React 18 新特性

  1. 并发模式:fiber 节点执行划分优先级,可通过代码标记哪些更新优先级较低、可中断(useTranstion、useDeferredValue)。
  2. 自动批处理:任意场景都将接管 setState,进行批处理更新。
  • 26
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值