react为什么使用fiber链表结构去遍历组件树
1. 背景介绍
react fiber架构有两个主要的阶段:reconciliation/render 和 commit。在render阶段,react遍历整个组件树执行了以下操作:
- 更新state和props
- 调用生命周期钩子函数
- 遍历组件的子元素,与之前的子元素进行比较,得到需要进行的DOM更新
如果react同步的遍历整个组件树,执行上述操作,可能会执行超过16ms(如果屏幕帧率60HZ),阻塞UI渲染,造成动画掉帧,出现视觉上的卡顿等。
所以应该怎么办呢?
浏览器幕后任务协作调度 API requestIdeleCallback在浏览器的空闲时段内调用的函数排队,使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
如果我们使用一个performWork函数来执行React对组件树的整个操作,并使用requestIdleCallback API来对该任务进行调度,那么我们的代码逻辑可能是这样子:我们对一个组件进行处理并返回下一个组件等待调度,这样我们就不用像React16前的版本那样,同步的遍历整个组件树。
但是我们需要一种将render中的组件树的遍历过程分解为一个个增量单元的方法,即可以在完成某个组件的reconciliation之后,将调度权交还给浏览器以执行高优先级的用户交互、UI渲染等操作,待浏览器空闲时再继续下一个组件的reconciliation。 如果继续使用之前的组件树形结构,如下图所示,我们只能用递归的方式去实现组件树的遍历。
// 深度优先遍历组件树
const root = document.getElementById('root');
function logName(node){
console.log(node.dataset.name)
}
function traversalTree(root){
logName(root);
const childNodes = root.childNodes;
for(let childNode of childNodes){
if(childNode.nodeType !== 3){
traversalTree(childNode);
}
}
}
traversalTree(root);
//输出
a1, b1, b2, c1, d1, d2, b3, c2
递归方法非常适用于遍历树形结构,如上所示,但是递归模型无法做到增量渲染,也不能实现暂停某个组件的渲染并在浏览器空闲的时候继续执行。所以React采用了基于链表的Fiber模型
2. Fiber链表遍历过程
Fiber链表结构遍历需要以下三个字段:
- child——指向第一个子节点
- sibiling——指向第一个兄弟节点
- return——指向父节点
上文中的组件树对应的Fiber结构如下所示:
遍历fiber链表的过程如下所示:
function workLoop(){
while(nextUnitOfWork && !shouldYield()){
nextUnitOfWork = performUnitWork(nextUnitOfWork)
}
}
const root = rootFiber;
function performUnitWork(node){
// 这里对该节点执行render流程
let child = perWorkOfNode(node)
// 如果有子节点,继续遍历子节点
if(child){
return child;
}
// 如果回到了根节点,表示Fiber链表遍历完成
if(node === root){
return null
}
//如果没有子节点,也没有兄弟节点,则回父节点,如果父节点依然没有兄弟节点,则回到更上一层节点
while(!node.sibling){
if(!node.return || node.return === root){
return nill
}
node = node.return;
}
return node.sibling;
}
上述算法使用nextUnitOfWork变量保存对当前Fiber节点的引用,能够异步的遍历组件树对应的每个Fiber节点,用requestIdleCallback
包裹workLoop,使用shouldYield来检查是否有剩余时间执行nextUnitOfWork,如果没有剩余时间,则将控制权交还给浏览器,等待下一次调度从中断的nextUnitOfWork继续执行。
3. 从React elements 到 Fiber Nodes
我们从一个 React classComponent开始分析从React elements 到 Fiber Nodes的转化过程。
class ClickCounter extends React.Componenet{
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) => {
return {count: state.count + 1};
});
}
render(){
return [
<button key='1' onClick={this.handleClick}>Update counter</button>
<span key='2'>{this.state.count}</span>
]
}
}
// babel jsx转化 =>
React.createElement(
'button',
{
key:'1',
onClick: this.onClick
},
'Update counter'
),
React.createElement(
'span',
{
key:'2'
},
this.state.count
)
// createElement执行得到react elements
[
{
$$typeof: Symbol(react.element),
type: 'button',
key: '1',
props: {
children: 'Update counter',
onClick: () => { ... }
}
},
{
$$typeof: Symbol(react.element),
type: 'span',
key: "2",
props: {
children: 0
}
}
]
每个react element都有一个对应的fiber node,我们可以把fiber node看作一种描述unitWork的对象,这些对象按照链表的方式链接起来,可以方便的进行调度执行。ClickCounter
组件fiber node的简略版的数据结构如下:
{
return: null,
child: null,
sibling: null,
stateNode: new ClickCounter,
type: ClickCounter,
tag: 1,
alternate: null,
key: null,
updateQueue: null,
memoizedProps: {},
pendingProps: {},
effectTag: 0,
nextEffect: null
}
stateNode
保存类组件的实例,HostComponent的DOM节点或者其他React元素类型的类实例的引用。
type
保存类组件的构造函数,或者HostComponent的Html标签,或者函数组件。
tag
定义fiber的类型,例如0表示FunctionComponent,1表示ClassComponent,详情见此处
alternate
指向workInProgress上的对应fiber node,current <==> workInProgress
key
是多个子组件的唯一标识,在组件更新时便于复用
updateQueue
队列结构,保存状态更新、回调函数、DOM更新等
memoizedProps
保存上一次渲染的props
pendingProps
保存nextProps
effectTag
标记该节点在commit阶段需要执行的副作用类型
nextEffect
单链表用来快速查找下一个side effect
参考资料
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。