【React源码笔记10】- completeUnitOfWork

React | 2020-08-09 11:05:06 569次 5次

创建新的 fiber 节点之后,进入 render 阶段的最后一个操作,就是工作完成 completeWork,经过深度优先遍历,从第一个最小左孩子开始完成工作的处理。

再来梳理一下之前的流程:

function workLoopConcurrent() {
    //不停的任务分片
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}
function performUnitOfWork(...){
  next = beginWork(...);
  if (next === null) {
    //参考模拟实现中的那张图,此时节点没有子孩子,需要开始完成处理
    next = completeUnitOfWork(unitOfWork);
  }
  ...
  return next;
}
function completeUnitOfWork(currentFiber) {
    while (currentFiber) {
      next = completeWork(current, workInProgress, renderExpirationTime);
    if (currentFiber.sibling) {
        return currentFiber.sibling;//如果有兄弟节点返回兄弟节点去开始工作
    }
    currentFiber = currentFiber.return;// 找父节点,让父节点完成
    }
}

可以看到这三个方法做的事情就是一个大递归,从 rootFiber 上进行一个深度优先遍历,这个过程中遍历到的节点执行 beginWork,等遇到最小左孩子(没有子孩子的节点)开始执行 completeUnitOfWork,然后判断当前节点是否还有兄弟节点,

如果没有兄弟节点则继续父节点执行 completeUnitOfWork

如果有兄弟节点则返回兄弟,让兄弟节点执行 beginWork,按照这种步骤,直到完成到 rootFiber

c: child | s: sibling

            A1
            |
            |-c
            |
            B1 ---s--- B2
            |
            |-c
            |
            C1 ---s--- C2
                        |
                        |
                        D2
                        
   流程:
   A1 begindWork
   B1 begindWork
   C1 begindWork
   C1 complete
   C2 begindWork
   D2 begindWork
   D2 complete
   C2 complete
   B1 complete
   B2 begindWork
   B2 complete
   A1 complete

再补充下 fiber 节点的创建过程,在 reconcileChildFibers 阶段创建好 fiber 树关系,returnsibling 等指向:

初始加载 fiber构建:

 进入 reconcileChildrenArray

进入 if (oldFiber === null) {} 遍历所有子节点(这里的逻辑是新节点的创建)

执行 createChild 里面构建 retrun 构建父节点

执行 previousNewFiber = _newFiber;

执行 previousNewFiber.sibling = _newFiber; 构建兄弟

更新时 fiber构建:

  由于关系初始已经构建完成,更新时有可能复用节点(会删除被复用节点的兄弟),

所以只需要在 diff 第二个遍历中不停构建兄弟关系即可:previousNewFiber.sibling = newFiber;

注意的是如果新增的节点会走进上面说的那个 if (oldFiber === null) {} 逻辑同上


一、completeWork

为何到了这里又执行一堆更新操作,和 beginWork 中的有什么区别?

beginWork 中主要做了一些初始化创建或者复用可用的 fiber 节点,然后经过对比做一些标记。

completeWork 通过 props 处理的主要是原生组件,也就是 HostComponent HostText,以及 SuspenseComponent 等系列组件,对函数组件或者类组件等不会处理(除非老版本的 context 处理),赋值 workInProgress.updateQueue 等待下一阶段使用。

方法概览:

function completeWork(...){
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      ...
      return null;
    }
    case HostRoot: {
      ...
      return null;
    }
    case HostComponent: {
      ...
      return null;
    }
   ...

HostComponent 就是元素节点,依据这个展开分析,过程分为两个阶段,第一是初始加载,第二是更新时:

case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;

  if (current !== null && workInProgress.stateNode != null) {
    //更新
  } else {
    //初始加载
  }
  return null;
}

初始加载:

// 来源 pushHostContainer(workInProgress, root.containerInfo); 后面 context 源码再讲
const rootContainerInstance = getRootHostContainer();

//createElement 创建了 dom 真实节点
let instance = createInstance(...);
// 调和的过程中提到,初始的数据 placeSingleChild 不会标记插入
// 这里进行插入 每次都将子节点插入到父节点,但是不会渲染,到commit阶段一次性的挂载渲染
appendAllChildren(instance, workInProgress, false, false);

// 挂载真实 dom
workInProgress.stateNode = instance;

appendAllChildren = function(
  parent: Instance,
  workInProgress: Fiber,
  needsVisibilityToggle: boolean,
  isHidden: boolean,
) {
  let node = workInProgress.child;
  // 
  while (node !== null) {
    if (node.tag === HostComponent || node.tag === HostText) {
        // parent  ==> document.createElement(type);
        // parentInstance.appendChild(child);
      appendInitialChild(parent, node.stateNode);
    } ...
       ...
    else if (node.child !== null) {
        //这种情况下会不断的找子节点 <Com><span>1111</span></Com>
      node.child.return = node;
      node = node.child;
      continue;
    }
    // 为了 node.child !== null 的情况
    if (node === workInProgress) {
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === workInProgress) {
        return;
      }
      node = node.return;
    }
    node.sibling.return = node.return;
    // 接着挂兄弟节点
    node = node.sibling;
  }
};

对这个方法也是很精炼的实现,从最先完成的那个节点开始,从底往上开始挂载,直到 wip。举个例子,描述下流程辅助理解:

        <div id="A1">
          <div id="B1">
            <div id="C1"><span>1</span></div>
            <div id="C2"><span>2</span></div>
            <div id="C3"><span>3</span></div>
          </div>
          <div id="B2"></div>
        </div>
        
        流程如下:
        node | wip  挂载 结果
        --------------------
        null:  span1 -> 跳过
        span1: C1    -> span1
        null:  span2 -> 跳过
        span2: C2    -> span2
        null:  span3 -> 跳过
        span3: C3    -> span3
        
        C1  : B1 -> C1
        C2  : B1 -> C2 
        C3  : B1 -> C3 
        
        null: B2 -> 跳过
        B1:  A1 -> B1
        B2:   A1 -> B2

最终都挂载到了 A1,此时不会渲染,等提交时候直接全部插入渲染。

更新 updateHostComponent

updateHostComponent = function(...) {
  ...
  const updatePayload = prepareUpdate(...);
  // updateQueue 他的奇数索引的值为变化的prop key,偶数索引的值为变化的prop value。[ 'child', 1 ]
  // <span style={{color: 'red'}}>1</span>   <span>1122</span>  [ 'child', 1, 'style', {color: ''} ]
  workInProgress.updateQueue = (updatePayload: any)
  if (updatePayload) {
    //标记更新
    markUpdate(workInProgress);
  }
};

这里面主要是 updateQueue 的创建过程,updateQueue 分为三种类型:

第一种就是之前提到的 类组件更新时候的那个,update 通过 next 形成一个链表,updateQueue 中 firstBaseUpdate 指向第一个 update;

本次的是第二种类型,也就是是一个数组形式,奇数索引存储属性,偶数索引存储对应的值;

第三种就是 hooks 中用到,后面再看;

prepareUpdate 中做的事情就是调用下面这个方法,也可以理解为一种 diff  只不过是节点属性的 diff,比如样式、事件、节点的内容,DANGEROUSLY_SET_INNER_HTML 等。

diffProperties(
    domElement,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
  );


二、Effect 链表创建

effectList 链表的创建,为了方便在 commit 阶段执行发生信息改变的节点遍历,不需要再深度遍历 fiber 树。

按照如下结构举例,下面的结构代表更新后的节点:

    return !flag ? (
        <div id="A1">
          <div id="B1">
            <div id="C1">
                <span id='span1'>11</span>
            </div>
            <div id="C2"></div>
          </div>
          <div id="B2"></div>
        </div>
      ): (
        <div id="A11">
          <div id="B11">
            <div id="C11">
                <span id='span1'>11</span>
            </div>
            <div id="C22"></div>
          </div>
          <div id="B22"></div>
        </div>
      )

链表创建,既然创建链表在这个方法中,那么它的起点是和节点完成的顺序一致,参照上面的流程 complete 的节点顺序:

  function completeUnitOfWork(){
      ...
      if (returnFiber ) {
        // 先将自身的子节点中 effect 挂载到 自身的父节点effect上
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }
        if (workInProgress.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            // 这里貌似没有什么用  插眼
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }
          returnFiber.lastEffect = workInProgress.lastEffect;
        }

        const effectTag = workInProgress.effectTag;
        //初始加载时候这里不成立   因为初始的加载会一次性插入,不需要花里胡哨
        // 将自己挂载给父节点
        if (effectTag > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            // 最终的链表通过 nextEffect 指针串联 
            returnFiber.lastEffect.nextEffect = workInProgress;
          } else {
            returnFiber.firstEffect = workInProgress;
          }
          returnFiber.lastEffect = workInProgress;
        }
      }
    }
    ...
  }

上面这段代码很容易读懂,但是很难想明白为什么这么做,先通过 debugger 记录下运行的流程,再根据运行结果反推下这个方法的目的:

span1:
    C11 --> firstEffect = null
    C11 --> firstEffect = span1
    C11 --> lastEffect = span1

C11:
    B11 --> firstEffect = span1
    B11 --> lastEffect = span1
    (B11 --> lastEffect)span1 --> nextEffect = C11
    B11 --> lastEffect = C11

C22:
    (B11 --> lastEffect)C11 --> nextEffect = C22
    B11 --> lastEffect = C22

B11:
    A11 --> firstEffect = span1
    A11 --> lastEffect = C22
   (A11 --> lastEffect)C22 --> nextEffect = B11
   A11 --> lastEffect = B11

B22:
   (A11 --> lastEffect)B11 --> nextEffect = B22
   A11 --> lastEffect = B22

A11:
    root --> firstEffect = span1
    root --> lastEffect = B22
   (root --> lastEffect)B22 --> nextEffect = A11
   root --> lastEffect = A11

最终会生成如下结构的一个单链表,通过 nextEffect 连接:

root -> span1 -> C11 -> C22 -> B11 -> B22 -> A11

根据这个结果再绘制一张图或许会更清晰:

微信截图_20200809232950.png

根据这个结果来看,每个节点上都有 firstEffect lastEffect 属性,但是最小子节点中的值不存在,图中没有绘制。

需要注意的是上方举例的节点是 key 全部发生改变,也就是都产生了 effect,所以看到的结果是 nextEffect 会贯穿所有节点,接下来换个举例加深一下理解:

...
// C1 不发生变化,仅修改内容
<div id="C1"><span>11</span></div>  
...

那么此时的流程也肯定会发生改变,因为 C1 没有变化:

...
// 这里span1 nextEffect 直接指向了 C22  跳过了 C1
C22:
    (B11 --> lastEffect)span1 --> nextEffect = C22
    B11 --> lastEffect = C22
...

最终的 effectList 链表为:

root -> span1 -> C22 -> B11 -> B22 -> A11

经过这个过程的操作,react 最终生成的链表仅仅是发生过标记的一些节点,未发生变化的 fiber 节点会被忽略。

再考虑如下结构,有新插入的节点 Com

return !flag ? (
        <div id="A1">
          <p>11</p>
          <p>22</p>
        </div>
      ): (
        <div id="A1">
          <Com />
          <p>111</p>
          <p>222</p>
        </div>
      )

因为 p11 并没有直接移除 而是被打上了删除标记在 deleteChild 方法,所以这里的 effectlist 为,和上面的方式是有点出入的

p11 -> Com -> p111 -> p222

  到这里 render 阶段就暂时告一段落了,下一篇开始进入 commit 阶段分析。

5人赞

分享到: