综述:“fiber” reconciler 是一个新尝试,致力于解决 stack reconciler 中固有的问题,同时解决一些历史遗留问题。Fiber 从 React 16 开始变成了默认的 reconciler。
1.背景:React 15 面临的一个问题
我们知道从广义上来讲,浏览器是单线程的,它将 GUI 描绘,时间器处理,事件处理,js 执行,远程资源加载统统放在一起。
在 React 15 及之前的版本,React 在对组件进行更新时,如果需要渲染更新的组件过于庞大,js 执行就会长时间占据主线程,导致页面的响应变慢。
当然 React 也提供了优化的手段(shouldComponentUpdate),但是这种优化方式更多是依赖于使用者自身,这种单纯的人肉优化并没有很好地改善这种情况。
2.前置概念
屏幕刷新率和 FPS
当前大多数的屏幕刷新率都是 60hz,也就是每秒屏幕刷新 60 次,低于 60hz 人眼就会感知卡顿掉帧等情况。
FPS (frame per second) 是界面每秒刷新的次数,理论上 FPS 越高人眼觉得界面越流畅。比较理想的情况是在两次屏幕硬件刷新之间,浏览器正好进行一次刷新,实际体验也会很流畅。
如果浏览器重绘一次的时间是硬件多次刷新的时间,那么人眼将感知卡顿掉帧等, 所以浏览器对一次重绘的渲染工作需要在 16ms (1000ms/60) 之内完成,也就是说每一次重绘小于 16ms才不会卡顿掉帧。
浏览器在一帧里完成的工作
3.React 源码简析
React 采用 monorepo 的管理方式。仓库中包含多个独立的包,以便于更改可以一起联调,并且问题只会出现在同一地方。
渲染器 (Renderers) 和协调器 (Reconcilers)
渲染器:用于管理一棵 React 树,使其根据底层平台进行不同的调用。
协调器:不同的渲染器彼此共享一些代码。我们称 React 的这一部分为“reconciler(协调器)”
Fiber 架构的目标
4.Fiber 架构的目标
1. 能够把可中断的任务切片处理
2. 能够调整优先级,重置并复用任务
3. 能够在父元素与子元素之间交错处理,以支持 React 中的布局
4. 能够在 render() 中返回多个元素
5. 更好地支持错误边界
运行时存在的实例:
react15之前:
DOM - 真实的 DOM 节点
Instances - react 维护的虚拟 DOM 节点
Elements - 对 UI 进行描述 eg. type, props
react16:
DOM - 真实的 DOM 节点 effect - 即副作用 (side effect),包括 DOM change 等操作
workInProgress - fiber tree 建立的当前进度快照,用于断点恢复
fiber - fiber tree 与 vDOM tree 类似,用来描述增量更新所需的上下文信息
Elements - 对 UI 进行描述 eg. type, props
总结一下:
Reconciliation 以 fiber tree 为蓝本,把每个 fiber 作为一个工作单元,自顶向下逐节点构造 workInProgress tree(构建中的新 fiber tree)。具体过程如下:
如果当前节点不需要更新,直接把子节点 clone 过来,跳到5;要更新的话打个 tag 更新当前节点状态(props, state, context等) 调用 shouldComponentUpdate(),false 的话,跳到5 调用 render() 获得新的子节点,并为子节点创建 fiber(创建过程会尽量复用现有 fiber,子节点增删也发生在这里) 如果没有产生 child fiber,该工作单元结束,把 effect list 归并到 return,并把当前节点的 sibling 作为下一个工作单元;否则把 child 作为下一个工作单元 如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做 如果没有下一个工作单元了(回到了 workInProgress tree 的根节点),第 1 阶段结束,进入pendingCommit 状态
Fiber 架构其实就是 React 自己内部实现了一套调度系统,通过某些调度策略合理地分配 CPU 资源,达到快速响应用户,让用户觉得够快,不阻塞用户的交互的目标。
这一套调度系统还有一点好处:
给浏览器一点喘息的机会,他会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。