大家好,我是前端西瓜哥。
为了提高 React 的性能,React 团队在开发 React 16 时做了底层的重构,引入了 React Fiber 的概念。
React Fiber 是什么?
Fiber,本意为 “纤维”,在计算机世界中则是 ”纤程“ 的意思。纤程可以看作是协程的一种,是一种 任务调度 方式。
JavaScript 是单线程的,有一个 event loop 的概念,它有一个有优先级的任务队列,只能按顺序执行一个任务,是不支持多个任务同时执行的。
这种设计的好处就是不用考虑多线程导致的顺序问题,并为此做一些加锁的额外逻辑,确保执行顺序符合预期。但也因为无法使用并行能力,在 CPU 密集的场景会有性能问题, 比如一个任务耗时过长会导致其他的任务,导致用户的交互响应发生延迟。
React 的组件更新是 CPU 密集的操作,因为它要做对比新旧虚拟 DOM 树的操作(diff,React 中 Reconcilation 负责),找出需要更新的内容(patch),通过打补丁的方式更新真实 DOM 树(React 中 Renderer 负责)。当要对比的组件树非常多时,就会发生大量的新旧节点对比,CPU 花费时间庞大,当耗时大大超过 16.6ms(一秒 60 帧的基准) 时,用户会感觉到明显的卡顿。
这一系列操作是通过递归的方式实现的,是 同步且不可中断 的。因为一旦中断,调用栈就会被销毁,中间的状态就丢失了。这种基于调用栈的实现,我们称为 Stack Reconcilation。
React 16 的一个重点工作就是优化更新组件时大量的 CPU 计算,最后选择了使用 “时间分片” 的方案,就是将原本要一次性做的工作,拆分成一个个异步任务,在浏览器空闲的时间时执行。这种新的架构称为 Fiber Reconcilation。
在 React 中,Fiber 模拟之前的递归调用,具体通过链表的方式去模拟函数的调用栈,这样就可以做到中断调用,将一个大的更新任务,拆分成小的任务,并设置优先级,在浏览器空闲的时异步执行。
FiberNode
前面我们说到使用了链表的遍历来模拟递归栈调用,其中链表的节点 React 用 FiberNode 表示。
FiberNode 其实就是虚拟 DOM,它记录了:
- 节点相关类型,比如 tag 表示组件类型、type 表示元素类型等;
- 节点的指向;
- 副作用相关的属性;
- lanes 是关于调度优先级的;
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // 组件类型,比如 Function/Class/Host
this.key = key; // key 唯一值,通常会在列表中使用
this.elementType = null;
this.type = null; // 元素类型,字符串或类或函数,比如 "div"/ComponentFn/Class
this.stateNode = null; // 指向真实 DOM 对象
// Fiber
this.return = null; // 父 Fiber
this.child = null; // 子 Fiber 的第一个
this.sibling = null; // 下一个兄弟节点
this.index = 0; // 在同级兄弟节点中的位置
this.ref = null;
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
// ...
}
Fiber 通过 return 指向父 Fiber,child 指向子 Fiber 的首位、sibling 指向下一个兄弟节点。通过它们我们其实就能拿到一个完整的结构树。
对于:
代码语言:javascript复制function App() {
return (
<div className="app">
<span>hello</span>, Fiber
</div>
);
}
形成的 Fiber 树为:
其中弧线为调用顺序。紫色为 beginWork、粉色为 completeWork。beginWork 是 “递” 的过程,而 comleteWork 则是 “归” 的过程。
为什么不用 generator 或 async/await?
generator 和 async/await 也可以做到在函数中间暂停函数执行的逻辑,将执行让出去,能做到将同步变成异步。
但 React 没有选择它们,这是因为:
- 具有传染性,比如一个函数用了 async,调用它的函数就要加上 async,有语法开销,此外也会有性能上的额外开销;
- 无法在 generator 和 async/await 中恢复一些中间状态。
具体见官方的 github issue 讨论:
https://github.com/facebook/react/issues/7942#issuecomment-254987818
Scheduler
做了时间分片,拆分了多个任务,React 就可以以此为基石,给任务设置优先级。
React 实现了一个 Scheduler(调度器)来实现任务调度执行,并单独抽离为一个单独的包,它会在浏览器有空闲的时候执行。其实浏览器也提供了一个 requestIdleCallback 的 API,支持这个能力,但兼容性实在不好,React 还是自己实现了一套。
这个 Scheduler 支持优先级,底层使用了 小顶堆,确保能高效拿到最快要过期的任务,然后执行它。
小顶堆,其实就是优先级队列。小顶堆在结构上是一个完全二叉树,但能保证每次从堆顶取出元素时,是最小的元素。
任务的 优先级 分为几种:
- NoPriority:无优先级
- ImmediatePriority:立即执行
- UserBlockingPriority:用户阻塞优先级,不执行可能会导致用户交互阻塞
- NormalPriority:普通优先级
- LowPriority:低优先级
- IdlePriority:空闲优先级
React 自身也有优先级,叫做 Lane,两者是不同的。
结尾
React 的架构过于宏大,今天先随便说一点吧。
总的来说,React Fiber 是在 React 16 中引入的新的架构,将原本同步不可中断的更新,变成异步可中断更新,将原本一个耗时的大任务做了时间分片,拆分成一个个小任务,在浏览器空闲的时间执行。此外添加优先级的概念,将一些重要的任务先执行,比如一些用户交互的响应函数。
一切为了更好的用户体验。
我是前端西瓜哥,欢迎关注我,学习更多前端知识。