干货时刻
本文首先分析了react 15架构及其缺陷,进而引入react 16架构的原理介绍。
调和
在本文的开始首先弄清楚调和的概念:调和指的是通过ReactDOM等库使VDOM和DOM同步。也就是将VOM映射成DOM的过程。调和所做的工作包括组件的挂载、卸载、更新等过程,其中更新就用到了Diff算法。
Diff 算法
Diff算法的本质是对比新旧VDOM树的变更差异。其核心思想分为三个方面:
-
同层比较
- 忽略跨层级操作,同层比较中如果发现节点不存在,则该节点及其子节点均会被删除。
-
类型不同,原地替换;类型一致,分层递归
- 当根节点是不同类型的元素时,会放弃比较,原地替换旧节点
- 当根节点是相同类型的
DOM元素,保留根节点,只更新节点属性 - 当根节点是相同类型的组件,需要在组件
render执行后,根据render返回的VDOM决定如何更新DOM。在比较完根节点后,会以同样的原则递归比较子节点。
-
通过
key重用节点- 在列表元素的比较中,如果定义了
key,则react根据key匹配子节点,每次渲染后,只要子节点的key不变,则认为是同一个节点,进行复用,提高了更新效率。
- 在列表元素的比较中,如果定义了
react 15 架构
言归正传,react架构的组成可以分为两点:调和器和渲染器。在V15版本中的调和器称为栈调和器。
stack reconciler栈调和器- 调用函数组件或类组件的
render方法,将返回的jsx转化为VDOM。 - 将
VDOM和上次更新时的VDOM对比,找出本次更新中发生变化的VDOM。 - 通知
renderer将变化的VDOM渲染到页面
- 调用函数组件或类组件的
renderer渲染器ReactDOM:浏览器环境渲染ReactNative:app原生组件渲染ReactTest:渲染出纯js对象用于测试ReactArt:canvas、svg等容器上渲染
react 15 架构的缺点
主流浏览器每隔16.7ms就刷新一次,而浏览器渲染进程中js线程和GUI渲染线程是互斥的,不能同时执行,所以在每个16.7ms内既要执行js脚本,又要布局和绘制。react 15的更新属于同步更新策略,要递归遍历所有子节点进行diff、更新真实的DOM。并且这个过程是不能被打断的。当组件树的层级很深时,递归调和的时间超过了16.7ms,就会导致用户的交互出现卡顿。
react 16 的解决思路
与V15版本不同的是,在V16版本中的调和器称为纤程调和器(fiber reconciler),并且新增了调度器调度任务的优先级,使得高优先级任务优先进入fiber reconciler。从整体来看,新架构的核心解决思路就是降低视图更新的优先级。
fiber reconciler会把更新过程进行分片,调度器(scheduler)进行任务分配。当每个片段的任务执行完后就去看看有没有优先级更高的任务去做。如果有,就去把这个高优先级的任务做完,然后重新做更新任务。如果没有,才继续做其它的分片任务。
其中,任务的优先级分为六种:
synchronous,和stack reconciler一样都是同步执行task:在next tick前执行animation:下一帧前执行high:不久的将来立即执行low:稍微延迟执行offscreen:下一次render或scroll时执行
fiber reconciler在执行中分细分为两个阶段:
render | reconciliation阶段:生成fiber树,对新旧VDOM进行diff,找到需要更新的节点,放入更新队列。这个阶段进行分片处理,可以被高优先级的任务打断。值得注意的是,在类组件中,componentWillMount、componentWillUpdate、componentWillReceiveProps、shouldComponentUpdata这几个生命周期钩子可能会被多次调用,所以不要在以上钩子中做只需要做一次的操作,比方说ajax请求。commit阶段:将需要更新的节点一次性更新完,渲染真实DOM,不能被打断。
fiber 工作机制
fiber reconciler在reconciliation阶段会生成fiber树用于diff。fiber树和react 15架构中的VDOM树有什么区别?
我们先看看一个fiber节点长什么样:
function FiberNode() {
this.tag = tag; // fiber 标签,代表的类型
this.key = key; // 用来调和子节点
this.type = null; // 对应的 dom 元素类型
this.stateNode = null; // 对应的 dom 元素
this.return = null; // 指向父级 fiber
this.child = null; // 指向子级 fiber
this.sibling = null; //指向兄弟 fiber
this.index = 0; //索引
this.ref = null; // 指向 ref 对象。
this.pendingProps = pendingProps; // 在一次更新中,代表 element 创建
this.memoizedProps = null; // 记录上一次更新完毕后的 props
this.updateQueue = null; // 存放 setState 更新队列
this.memoizedState = null; // 类组件保存 state 信息,函数组件保存 hooks 信息
this.dependencies = null;
this.mode = mode; // 描述 fiber 树的模式,比如 ConcurrentMode 模式
this.effectTag = NoEffect; // effect 标签,用于收集 effectList
this.nextEffect = null; // 指向下一个 effect
this.firstEffect = null; // 第一个 effect
this.lastEffect = null; // 最后一个 effect
this.expirationTime = NoWork; // 通过不同过期时间,判断任务是否过期, 在 v17 版本用 lane 表示。
this.alternate = null; // 双缓存树,指向缓存的 fiber。更新阶段,两颗树互相交替。
}
每一个fiber节点都和一个react element一一对应,fiber节点之间是通过return、child、sibling三个属性相连。
举个例子,同学们请看这个App组件:
export default class App extends React.Component{
state={ number:666 }
render(){
return <div>
hello world
<p > 东曜说 { this.state.number }</p>
<p>关注走一走</p>
</div>
}
}
它的fiber树就长这样

既然明白了fiber节点的结构,以及fiber是如何联系的。那么接下来讲一下页面初始化时fiber的工作机制。
第一次挂载过程中,创建fiberRoot和rootFiber。
fiberRoot:首次构建应用时,会创建一个fiberRoot作为应用的fiber根节点rootFiber:组件的fiber根节点,可以通过ReactDOM.render渲染。
创建fiberRoot时会将fiberRoot的current指针指向rootFiber。
function createFiberRoot(containerInfo, tag) {
/* 创建一个root */
const root = new FiberRootNode(containerInfo, tag);
const rootFiber = createHostRootFiber(tag);
root.current = rootFiber;
return root;
}
在渲染过程中,会复用当前current树的alternate作为workInProgress树。如果没有alternate(在第一次挂载时current树的fiber节点没有alternate),则会创建一个新fiber节点作为workInProgress树的rootFiber节点,同时两个颗树的fiber节点的alternate均指向彼此。

在workInProgress树上会完成整个fiber树的遍历,包括fiber节点创建。完成后,以workInProgress树作为新的渲染树,将fiberRoot的current指向workInProgress树的rootFiber,使其转化为current树。

如果我们将App组件修改一下:
export default class App extends React.Component{
state={ number:666 }
handleClick=()=>{
this.setState({
number:this.state.number + 1
})
}
render(){
return <div>
hello world
<p > 东曜说 { this.state.number }</p>
<button onClick={handleClick}>关注走一走</button>
</div>
}
}
此时点击一次按钮,页面就会重新渲染一次。即重新创建一颗workInProgress树,复用当前current树上的alternate作为新的workInProgress。对于剩余子节点,react还需要创建一份,和current树上的fiber建立alternate关联。渲染完毕后,workInProgress再次转化为current树。
上述更新逻辑称为双缓冲树:workInProgress树在内存中构建,current树用作渲染视图,两棵树用alternate指针互相指向,在下一次渲染的时候直接复用缓存树作为一下次的渲染树,上一次的渲染树这次作为缓存树。这样可以防止只用一棵树更新时出现页面白屏闪烁的情况,又加快DOM节点的替换和更新。
fiber reconciler
由上节可知,fiber reconciler可以分为两个阶段:reconciliation阶段和commit阶段。
reconciliation阶段负责fiber树的调和,其执行过程分细分为beginWork和completeWork两个阶段。
-
beginWork是workInProgress树自顶向下DFS调和的阶段,由fiberRoot按照child指针逐层向下调和,期间执行组件render拿到children,然后遍历children,diff子节点,复用oldFiber,同时打上不同的副作用标签。 -
completeWork是自底向上归并的过程,如果有兄弟节点返回sibling,没有就返回父级fiber,一直到fiberRoot。期间将打上副作用标签的fiber节点放进effectList单向链表中,在commit阶段不需要遍历每一个fiber,只需要执行更新effectList即可。在页面初始化情况下还会创建DOM,针对DOM元素进行事件收集,处理style等。
commit阶段可以细分为beforeMutaion、mutation、layout等三个阶段。
beforeMutaion具体指执行DOM操作前,如果是类组件则会执行getSnapshotBeforeUpdate生命周期钩子,如果是函数组件则会异步调用useEffect。mutation具体指执行DOM操作阶段,会进行真实DOM元素的增、删、改,同时置空ref。layout具体指执行DOM操作后,针对类组件会执行生命周期、setState的callback。针对函数组件会执行useLayoutEffect钩子,如果有ref则重新赋值。
结束语
想要学习更多优质内容的同学,关注收藏一下作者的个人主页:东曜说 ~~










