Fiber
深入探究 React Fiber(译文)原文链接:A deep dive into React Fiber - LogR - 掘金
Fiber 是 React 为了解决复杂、高频渲染时的卡顿提出的架构。Fiber 是 React 的一种新的协调引擎,它通过将更新过程拆分为多个小任务来解决性能瓶颈和用户体验问题,主要完成了以下任务: 增量渲染:将渲染过程分成多个可以中断的小任务,避免长时间阻塞
优先级调度:优先响应用户交互
双缓存:两棵 fiber 树,一棵进行 diff,另一棵显示在界面上
并行模式:可以响应用户操作的同时,进行 fiber 树更新
Fiber 节点是一个 JavaScript 对象,用于描述组件树的结构和状态。每个 Fiber 节点包含了与组件相关的信息,如类型、props、state、效果标记(effect tag)等。Fiber 节点还包含了指向其子节点、兄弟节点和父节点的引用,以构建组件树的层级结构。
Fiber 树的创建
Fiber 树的构建是一个增量的过程,主要包括以下步骤:
- 由 JSX 生成虚拟 DOM
- 由虚拟 DOM 生成 Fiber 节点
- 由 Fiber 节点构成 Fiber 树
- 渲染真实节点
虚拟 dom
用 js 对象描述组件结构,最大的优势在于跨平台性,配合 diff 算法可以优化渲染。
diff 算法
比较新旧 fiber 树,进行差异化更新的算法,有以下要点:
- 同层比较:不会进行跨层级比较
- 尽量复用:节点类型相同,复用。否则新增/删除。优先移动而不是重新创建。
- 列表元素:使用 key 进行 diff 算法。
与 vue3 diff 算法的区别
-
- vue3 采用双端队列(头头、尾尾、头尾、尾头,对交换时的处理更好) + 最长子序列算法,react 只是比较
-
- vue3 有静态节点标记
props 与 state 区别
它们都能触发重新渲染。
- props 来自外界,state 是自身状态
- 接收方不能改 props,但可以改自己的 state
可以在条件语句/循环中使用 Hook 吗
不能,因为 react 维护一个状态表,只记得状态的顺序不记得名字
React 如何进行视图更新
-
触发更新
-
调和 生成 workInProgress Fiber 树
-
提交 这个阶段无法中断。
React 为什么选择自己实现 requestIdleCallback
- 兼容性:原生对于 safari 不支持
- 每个浏览器的实现策略不同,导致结果差距大
- 跨平台性
- 需要更精细的调控,比如优先级
- 原生 requestIdleCallback 执行间隔太长
类组件与函数组件区别
- 状态管理: 类组件采用 this.state 和 this.setState 管理内部状态,而函数组件采用 useState
- 生命周期: 类组件有生命周期函数,而函数组件需要自己使用 useEffect 等去模拟
- 逻辑复用: 类组件采用高阶组件(HOC)实现逻辑复用,函数组件可以自定义 Hook
- 性能优化:
类组件采用
shouldComponentUpdate或React.PureComponent减少重复渲染,而函数式组件采用 React.memo 或 useCallback 等。
函数组件优势:
- 简洁:就是个普通的有返回值的函数,省去了很多样板代码
- 便于逻辑复用:
React 18新特性
虽然 React16 就提出了 Fiber 架构,但 18 才利用了 Fiber 的某些特性以达到:
- 合并渲染,自动批处理(之前
promise、setTimeout、原生应用的事件处理程序以及任何其他事件中的更新都不会被批量处理,现在可以了) - 新的根渲染函数 createRoot
- 并行模式,可以使用 useTransition(提供过渡状态,可以被打断,startTransition 的 Hook 版本,用于函数)、useDefferedValue(延迟更新状态,用于值) 等 Hook
- 不再支持 IE
- 其他的新 API:startTransition(用于标记低优先级更新,避免阻塞用户输入)等
还有一些其他的改动:
- root API: createRoot 开启并发模式,unmount 卸载组件
- 删除了 render 的回调函数
- props 中的 children 需要显式定义了
- 严格模式的日志会打印两次了,第二次显示灰色
组件生命周期
类组件
挂载:
- render 阶段:
- contructor(props):初始化 state、绑定事件处理函数,否则可以不写
- 静态方法 geDerivedStateFromProps:根据 props 更新 state
- render:返回 JSX
- commit 阶段:
- react 更新 DOM
- componentDidMount:组件更新完毕后调用,常用于数据请求、DOM 操作、添加事件监听器等
更新:
- render 阶段:
- 静态方法 getDerivedStateFromProps:父组件重新渲染、调用 setState 时会被调用,不要在里面执行副作用
- shouldComponentUpdate:是否应该重新渲染
- render
- pre-commit 阶段:
- getSnapshotBeforeUpdate:在 DOM 更新前获取一些信息,比如滚动位置,不要在这里触发更新
- commit 阶段:
- react 更新 DOM
- componentDidUpdate:更新完毕,首次挂载不会执行
卸载:
- componentWillUnmount:可以用来清理定时器、监听器、取消未完成的网络请求等
函数组件
没有生命周期钩子,但可以通过 useEffect 进行模拟
- 只在第一次渲染执行:useEffect 的依赖数组为 [ ],相当于 componentDidMount
- 每次渲染时执行(包括第一次挂载):useEffect 的依赖数组不写
- 在特定依赖项变化时执行:相当于 componentDidMount + componentDidUpdate
- 副作用清理:useEffect 的返回函数
- 是否更新:useMemo、useCallback 等,相当于 shouldComponentUpdate