前言
React16架构可以分为三层也是最核心的三个功能分别是:
- Scheduler(调度器)—调度任务的优先级,高优任务优先进入Reconciler(16新增)
- Reconciler(协调器)—负责找出变化的组件
- Renderer(渲染器)— 负责将变化的组件渲染到页面上
本文主要是分析一下Reconciler协调器的理念以及流程。简单就是一句话:Reconciler协调器中主要功能就是使用循环实现可中断递归,并进行Fiber节点的对比,然后打上标记,然后通知Renderer更新
背景
在React中可以通过this.setState、useState、this.forceUpdate、ReactDOM.render等API触发更新,在Reconciler中,mount的组件会调用mountComponent ,update的组件会调用updateComponent 。这两个方法都会递归更新子组件。15版本的时候,是使用递归的方式来遍历Diff对比虚拟DOM的差异然后通知Renderer来进行渲染的,主要是如下功能:
- 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
- 将虚拟DOM和上次更新时的虚拟DOM对比,找出本次更新中变化的虚拟DOM
- 通知Renderer将变化的虚拟DOM渲染到页面上
由于递归一旦开始就不可中断,如果diff的层级较多的时候,就会出现递归时间超过16ms,导致页面卡顿。以及在ReReconciler和Renderer是交替工作的。如下图:当第一个li在页面上已经变化后,第二个li再进入Reconciler。
由于整个过程都是同步的,所以在用户看来所有DOM是同时更新的
为了解决这些问题,React决定重写这块架构(Fiber架构),用异步可中断的更新来代替同步更新。使用循环的方式来代替递归实现可中断,从代码可以看出,每次循环都会调用shouldYield判断当前是否有剩余时间。
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
而且Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记(使用二进制),类似这样:
export const Placement = /* 插入 */ 0b0000000000010;
export const Update = /* 更新 */ 0b0000000000100;
export const PlacementAndUpdate = /*插入并更新*/ 0b0000000000110;
export const Deletion = /* 删除 */ 0b0000000001000;
整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。所以即使Scheduler和Reconciler是异步可中断的,但是用户也不会看到未完全更新数据,因为当处理完了之后,统一通知Renderer更新,而Renderer是同步不可中断更新的。
接下来解释一下待会儿会提到的一些术语:
- Reconciler工作的阶段被称为render阶段。因为在该阶段会调用组件的render方法。
- Renderer工作的阶段被称为commit阶段。就像你完成一个需求的编码后执行git commit提交代码。commit阶段会把render阶段提交的信息渲染在页面上。
- render与commit阶段统称为work,即React在工作中。相对应的,如果任务正在Scheduler内调度,就不属于work。
双缓存树
都知道React在Fiber架构中使用了双缓存,所以开始之前我们先要了解一下什么是双缓存树。
当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。这种在内存中构建并直接替换的技术叫做双缓存 。
简单来说,双缓存树就是使用两个树来避免更新时的闪烁问题的解决策略,一棵Current Tree(视图中的),一个WorkInProgress Tree(内存中的),这两颗树是可以通过修改alternate指向来互相转化的,当需要更新时,直接用缓存树替换视图树。React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。
双缓存Fiber树
在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React应用的根节点通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。
接下来我们以具体例子讲解mount时、update时的构建/替换流程。
Mount时
以下面代码为例:
function App() {
const [num, add] = useState(0);
return (
<p onClick={
() => add(num + 1)}>{
num}</p>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
mount阶段
首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点。(之所以要区分fiberRootNode与rootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode。)
ReactDOM.render(<A/>, dom) // rootFiberA
ReactDOM.render(<B/>, dom) // roo