10.1我们提到 renderRootSync 是调和过程中的渲染阶段,下面来详细讲解一下
fiber更新循环 workLoop
来看函数体:
function renderRootSync(root,lanes){
workLoopSync();
/* workLoop 完毕,证明已遍历所有节点,那么重置状态,进入 commit 阶段 */
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
}
首先我们来解释下本文最重要的一个概念-- workLoop
我们先来看下实现:
function workLoopSync() {
/* 循环执行 performUnitOfWork,一直到 workInProgress 为空 */
while (workInProgress ! = = null) {
performUnitOfWork(workInProgress);
}
}
书中有这么一段比喻:
我们用一个例子来形容 workLoop,假设将应用看作一台设备,每一次更新看作一次检修维护,维 修师傅应该如何检修呢?维修师傅会用一台机器 (workLoop 可以看作这台机器),依次检查每一个需 要维护更新的零件 (fiber 可以看作零件),每一个需要检修的零件会进入检查流程(performUnitOfWork)。
如果需要更新, 那么会更新;如果有子零件更新 (子代 fiber),父代本身也会进入机器运转 (workLoop)流程中。
workLoop 是一个工作循环:
- 在初始化过程中,fiber 树通过 workLoop 过程去构建。
在一次更新中,并不是所有的 fiber 会进入workLoop 中,而是每一个待更新或者有 Child 需要更新的 fiber,会进入 workLoop 流程。- workLoop 在整个渲染流程中的角色非常重要,可以将 workLoop 当作一个循环运作的加工器,每一个需要调和的 fiber 可以当作一个零件,每一个零件需要进入加工器,如果没有待加工的零件,加工器才停止运转。
workInProgress 永远指向正在被调和的fiber, workLoop 的执行单元就是 workInProgress fiber 节点,而且更新每一个 fiber 的函数叫作 performUnitOfWork。
我们看下 performUnitOfWork 都干啥了?
function performUnitOfWork(unitOfWork){
/* 执行 */
let next = beginWork(current, unitOfWork, subtreeRenderLanes);
unitOfWork.memoizedProps = unitOfWork.pendingProps;
/* 优先将 next 赋值给 workInProgress,如果没有 next,那么调用 completeUnitOfWork 向上归并处理。 */
if (next = = = null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
- beginWork:是向下调和的过程。就是由 fiberRoot 按照 Child 指针逐层向下调和,期间会执行函数组件、实例类组件、diff 调和子节点,打不同的标签。
- completeUnitOfWork:是
向上归并的过程,如果有兄弟节点,会返回 sibling 兄弟,没有返回 return父级,一直返回到 fiberRoot,对于初始化流程会创建 DOM,对于 DOM 元素进行事件收集,处理 style、 className 等。 - 这样一上一下,完成了整个 fiber 树的 workLoop 流程。
function Index(){
return <div>
Hello,world
<p > Let us learn React! </p>
<button >Go</button>
</div>
}
在初始化或者一次更新中调和顺序是怎样的呢?beginWork 和 completeUnitOfWork 的执行先后顺序:
- beginWork → rootFiber
- beginWork → Index fiber
- beginWork → div fiber
- beginWork → hello,world fiber
completeWork → hello,world fiber (completeWork 返回 sibling)beginWork → p fibercompleteWork → p fiber- beginWork → button fiber
- completeWork → button fiber (此时没有 sibling,返回 return)
completeWork → div fibercompleteWork → Index fibercompleteWork → rootFiber (完成整个 workLoop)
最小的更新单元
先说结论:
fiber 是调和过程中的最小单元,每一个需要调和的 fiber 会进入 workLoop 中。而组件是最小的更新单元,React 的更新源于数据层 state 的变化。
怎么理解呢?
首先我们知道 fiber的类型有很多种
有 function Component类型的,有 Class Component类型的,还有 hostComponent 类型的fiber(普通div元素这种)
哪些类型fiber 可以触发更新呢?
我们知道 react终究是 数据驱动视图,这个数据又是什么呢?可以是父组件传进来的props,可以是 组件自身的state,可以是 Context数据,但终究是来自某个组件的 state 变化
而state是建立在组件类型的fiber上的,所以只有组件能触发更新 ,hostComponent类型(div元素)fiber是无法触发更新的,但是 hostComponent类型 fiber也是会进入调和阶段的
下面的例子可以加深理解
场景一:更新 A 组件 state,A 触发更新,如果 B、C 没有做渲染控制处理 (比如 memo PureComponent),那么更新会影响 B、C,而 A、B、C 会重新渲染。
场景二:当更新 B 组件,那么组件 A fiber 会被标记,然后 A 会调和,但是不会重新渲染;组件B 是当事人,既会进入调和,也会重新渲染;组件 C 受到父组件 B 的影响,会重新渲染。
场景三:当更新 C 组件,那么 A、B 会进入调和流程,但是不会重新渲染,C 是当事人,会调和并 重新渲染。
从 beginWork 到组件更新全流程
/**
* @param {*} current current 树 fiber
* @param {*} workInProgress workInProgress 树 fiber
* @param {*} renderLanes 当前的渲染优先级
* @returns
*/function beginWork(current,workInProgress,renderLanes){
/* -------------------第一部分-------------------- */
if(current ! = = null){
/* 更新流程 */
/* current 树上上一次渲染后的 props */
const oldProps = current.memoizedProps;
/* workInProgress 树上这一次更新的 props */
const newProps = workInProgress.pendingProps;
if(oldProps ! = = newProps || hasLegacyContextChanged()){
didReceiveUpdate = true;
}else{ /* props 和 Context 没有发生变化,检查是否更新来自自身或者 Context 的改变 */
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current,renderLanes)
if(!hasScheduledUpdateOrContext){
didReceiveUpdate = false;
return attemptEarlyBailoutIfNoScheduledUpdate(current,workInProgress,renderLanes)
}
/* 这里省略了一些判断逻辑 */
didReceiveUpdate = false;
}
}else{
didReceiveUpdate = false
}
/* -------------------第二部分-------------------- */
/* TODO: 走到这里会触发组件更新,比如函数执行,类组件会执行渲染。*/
switch(workInProgress.tag){
/* 函数组件的情况 */
case FunctionComponent: {
return updateFunctionComponent (current, workInProgress, Component, resolvedProps, renderLanes)
}
/* 类组件的情况 */
case ClassComponent:{
return updateClassComponent(current,workInProgress,Component,resolvedProps,renderLanes)
}
/* 元素类型 fiber <div>, <span> */
case HostComponent:{
return updateHostComponent(current, workInProgress, renderLanes)
}
/* 其他 fiber 情况 */
}
}
接下来我们来看下 一个变量的意义:didReceiveUpdate
1. 第一阶段
这个阶段非常重要,也就是判断更新情况。前面的三种场景可以在第一阶段进行判断处理。先来 分析一下第一阶段做了哪些事。正式讲解之前,先来看一个变量的意义,也就是 didReceiveUpdate。
didReceiveUpdate:这个变量主要证明当前更新是否来源于父级的更新,那么自身并没有更新。
比如下图,更新 A 组件,那么 B 组件也会跟着更新,这个情况下 didReceiveUpdate = true。
首先通过 current!= = null 来判断当前 fiber 是否创建过,如果第一次 mounted,那么 current 为
null,而第一阶段主要针对更新的情况。如果初始化,那么直接跳过第一阶段,到第二阶段。
首先我们看看哪些情况会触发 didReceiveUpdate = false,即更新不来自父组件(oldProps = = = newProps)
情况一:还是回到上面的场景,如果 C 组件更新,那么 B 组件被标记 ChildLanes,会进入 beginWork 调和阶段,但是 B 组件本身 props 不会发生变化。即子组件更新,父组件会发生调和,进入 beginWork,父组件跳出调和,返回子组件继续下面的流程
-
情况二:通过 useMemo 等方式缓存了 React element 元素,在渲染控制章节讲到过。 -
情况三:就是更新发生在当前组件本身,比如 B 组件发生更新,但是 B 组件的 props 并没有发生 变化,所以也会走到这个流程上。比如B自身 state发生变化 -
情况四:初始渲染
我们如何区分 情况一 和 情况三呢,因为显然 情况一不需要走后面的渲染流程,但情况三需要,所以单用 didReceiveUpdate 来区分是不够的
checkScheduledUpdateOrContext就来给您区分了
function checkScheduledUpdateOrContext(current,renderLanes){
const updateLanes = current.lanes;
/* 这种情况说明当前更新 */
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
/* 如果该 fiber 消费了 Context,并且 Context 发生了改变。*/
if (enableLazyContextPropagation) {
const dependencies = current.dependencies;
if (dependencies ! = = null && checkIfContextChanged(dependencies)) {
return true;
}
}
return false;
}
如何判断更新来自自身呢?
答案:检查当前 fiber 的 lane 是否等于 renderLanes(当前的渲染优先级),如果相等,那么证明更 新来源于当前 fiber,比如 B 组件发生更新,会走这里 (情况三)。
所以 checkScheduledUpdateOrContext() 返回 false 就包含 情况一
其实 checkScheduledUpdateOrContext() 返回 false 还有一种情况,就是F触发更新,但如果当前fiber为A时,这种情况 其实代表整个左边子树都可以跳过 调和,-- 情况五
所以呢,还需要分别处理情况一和情况五 attemptEarlyBailoutIfNoScheduledUpdate 就是干这个的,
attemptEarlyBailoutIfNoScheduledUpdate 最重要的是调用bailoutOnAlreadyFinishedWork。
function bailoutOnAlreadyFinishedWork(current,workInProgress,renderLanes){
/* 如果 Children 没有高优先级的任务,说明所有的 Child 没有更新,那么直接返回,Child 也不会被调和
*/
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
/* 这里做了流程简化 */
return null
}
/* 当前 fiber 没有更新。但是它的 Children 需要更新。 */
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
首先通过 includesSomeLane 判断 ChildLanes 是不是高优先级任务,如果不是,那么所有子孙 fiber不需要调和,直接返回 null,Child 也不会被调和。这就是针对 情况5的处理
如果 ChildLanes 优先级高,那么说明 Child 需要被调和,但是当前组件不需要,所以会克隆一下
Children,返回 Children,本身不会重新渲染。这就是针对 情况一的处理
bailoutOnAlreadyFinishedWork 流程非常重要, 为什么呢?我理解,其实这五种情况,情况一,情况五 的处理对于 react 的性能 至关重要,因为它可以提前return,跳过改fiber的渲染,它可以减少fiber的不必要渲染,甚至是不必要的 调和,大大节省了时间。
2. 第二阶段
从 beginWork 的源码中可以看到,第二阶段就是更新 fiber,比如函数组件,就会调用 updateFunctionComponent,类组件就会调用 updateClassComponent,然后进行重新渲染。
3. 流程总结
最后作者 又贴心的针对我们上面列举的情况 再次总结说明,这里不再赘述了
最后总结
这节内容还是需要好好消化的,尤其是 workloop 概念,以及 react对情况一 和情况五的 特殊处理,我觉得是react 架构的一大亮点。