React源码解析-commit

22,505 阅读2分钟

上一篇,我们了解到beginWork和completeWork阶段,到这里为止,协调的数据都在内存中,页面上还没有任何变化。接下来的commit阶段至关重要,接下来我们一探究竟吧~

一. commitRoot

function commitRoot(root) {
  var renderPriorityLevel = getCurrentPriorityLevel();
  runWithPriority$1(ImmediatePriority$1, commitRootImpl.bind(null, root, renderPriorityLevel));
  return null;
}

getCurrentPriorityLevel是优先级有关,这里涉及到schedule调度内部优先级问题。这块,我们再后续的schedule阶段系统性分享。

二. commitRootImpl

rootWithPendingPassiveEffects

do {
  flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
  
  // ...
  
  if (rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = false;
      rootWithPendingPassiveEffects = root;
      pendingPassiveEffectsLanes = lanes;
      pendingPassiveEffectsRenderPriority = renderPriorityLevel;
  }
  // ...
  onCommitRoot(finishedWork.stateNode, renderPriorityLevel);
  // ...
  ensureRootIsScheduled(root, now());
  // ...
  flushSyncCallbackQueue();
}

rootWithPendingPassiveEffects是rootFiber对应的effects,在执行commit核心流程之前,先flushPassiveEffects,简单来说,可能存在后续的update,但rootFiber上存在effects,因为flushPassiveEffects是异步宏任务执行的,并不会同步在commit中处理掉。并且异步宏任务队列推入schedule的taskQueue里面,消息队列的执行也并非是按照队列顺序执行。

firstEffect

var firstEffect;

if (finishedWork.flags > PerformedWork) {
  if (finishedWork.lastEffect !== null) {
    finishedWork.lastEffect.nextEffect = finishedWork;
    firstEffect = finishedWork.firstEffect;
  } else {
    firstEffect = finishedWork;
  }
} else {
  // There is no effect on the root.
  firstEffect = finishedWork.firstEffect;
}

firstEffect的判断,如果是flags大于PerformedWork,即存在副作用。这里第一个firstEffect就是rootFiber上的firstEffect(在beginWork阶段生成的)

下面是commit的核心三阶段

三. commit第一阶段

if (firstEffect !== null) {
  // ...
  nextEffect = firstEffect;
  do {
    {
      invokeGuardedCallback(null, commitBeforeMutationEffects, null);

      if (hasCaughtError()) {
        if (!(nextEffect !== null)) {
          {
            throw Error( "Should be working on an effect." );
          }
        }

        var error = clearCaughtError();
        captureCommitPhaseError(nextEffect, error);
        nextEffect = nextEffect.nextEffect;
      }
    }
  } while (nextEffect !== null);

可以到看到,遍历所有的effect调用commitBeforeMutationEffects

commitBeforeMutationEffects

 while (nextEffect !== null) {
   var current = nextEffect.alternate;
   var flags = nextEffect.flags;
   
   // ...
   if ((flags & Snapshot) !== NoFlags) {
     setCurrentFiber(nextEffect);
     commitBeforeMutationLifeCycles(current, nextEffect);
     resetCurrentFiber();
   }
   
   // ...
   if ((flags & Passive) !== NoFlags) {
       
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;

      scheduleCallback(NormalPriority$1, function () {
        flushPassiveEffects();
        return null;
      });
    }
  }

  nextEffect = nextEffect.nextEffect;
 }

这是Snapshot其实是针对classComponent的生命周期调用,我们重点研究function Component,这里先忽略。感兴趣的朋友可以给我留言,我会针对classCompoent单独聊聊。

rootDoesHavePassiveEffects初始化是空,是标志着rootFiber上是否有passive effect。除了这里,后续的commit三阶段也会使用它。

scheduleCallback相对复杂,从这里开始,开始涉及schedule核心,这块我们在后续schedule单独章节聊。这里,我们只需记住发起了异步任务,等待schedule调度。

这里先留个思考题🤔:函数组件的useEffect在什么时候执行的?

四. commit第二阶段

 nextEffect = firstEffect;
  do {
    {
      invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);

      if (hasCaughtError()) {
        if (!(nextEffect !== null)) {
          {
            throw Error( "Should be working on an effect." );
          }
        }

        var _error = clearCaughtError();

        captureCommitPhaseError(nextEffect, _error);
        nextEffect = nextEffect.nextEffect;
      }
    }
  } while (nextEffect !== null);

经过commit第一阶段nextEffect变成了null,需要重置为firstEffect。继续第二次循环。

commitMutationEffects

function commitMutationEffects(root, renderPriorityLevel) {
  while (nextEffect !== null) {
    // ...
    var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    switch (primaryFlags) {
      case Placement:
        commitPlacement(nextEffect);
        nextEffect.flags &= ~Placement;
        break;
      case PlacementAndUpdate:
        commitPlacement(nextEffect);
        nextEffect.flags &= ~Placement;
        var _current = nextEffect.alternate;
        commitWork(_current, nextEffect);
            break;
      // ...
      case Update:
        var _current3 = nextEffect.alternate;
        commitWork(_current3, nextEffect);
        break;
      case Deletion:
        commitDeletion(root, nextEffect);
        break;
    }
    nextEffect = nextEffect.nextEffect;
  }
}

上面虽然有许多case,本质上只需关注创建和更新。

子组件将进入Update,根函数组件将进入PlacementAndUpdate,最后rootFiber不会匹配任何case。

子组件Update Case,执行commitWork,本质上执行的commitHookEffectListUnmount,这里可能有人要问:初始化阶段,哪有unmount?确实是没有,commitHookEffectListUnmount方法会判断effect上是否有destroy,有则执行。那么effect.destory什么时候才会有值呢?答案是执行了effect.create方法之后,所以初始阶段即使也执行了commitHookEffectListUnmount,也没关系,因为effect.destory为undefined。

根函数组件,相比较普通子组件,多执行了commitPlacement方法,其核心的代码如下:

  var child = node.child;

  if (child !== null) {
    insertOrAppendPlacementNodeIntoContainer(child, before, parent);
    var sibling = child.sibling;

    while (sibling !== null) {
      insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
      sibling = sibling.sibling;
    }
  }

其实就是递归调用自己,最终都将进入appendChildToContainer方法调用,appendChildToContainer直接使用parentNode.appendChild(child)。到这里就很清晰了,在completeWork阶段,rootFiber上stateNode实例即整个根函数组件的DOM对象,在这里一把插入到DOM文档流中。其中child就是之前在completeWork阶段准备好的整个DOM树。

五. commit第三阶段

 nextEffect = firstEffect;
  do {
    {
      invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);

      if (hasCaughtError()) {
        if (!(nextEffect !== null)) {
          {
            throw Error( "Should be working on an effect." );
          }
        }

        var _error2 = clearCaughtError();

        captureCommitPhaseError(nextEffect, _error2);
        nextEffect = nextEffect.nextEffect;
      }
    }
  } while (nextEffect !== null);
  
  nextEffect = null;

经过第二阶段循环后,nextEffect又变成了null,再次重置为firstEffect,继续第三次循环

commitLayoutEffects

第三阶段,其核心也是循环所有nextEffect链接,核心是执行commitLifeCycles方法。

if (flags & (Update | Callback)) {
    var current = nextEffect.alternate;
    commitLifeCycles(root, current, nextEffect);
  }

commitLifeCycles

function commitLifeCycles(finishedRoot, current, finishedWork, committedLanes) {
 switch (finishedWork.tag) {
   case // ...
   case Block:
     commitHookEffectListMount();
     schedulePassiveEffects();
     return;
   // ...
 }
}

commitHookEffectListMount

function commitHookEffectListMount(tag, finishedWork) {
    var updateQueue = finishedWork.updateQueue;
    var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    if (lastEffect !== null) {
      var firstEffect = lastEffect.next;
      var effect = firstEffect;
      do {
        if ((effect.tag & tag) === tag) {
          // Mount
          var create = effect.create;
          effect.destroy = create();

          {
            var destroy = effect.destroy;

            if (destroy !== undefined && typeof destroy !== 'function') {
              var addendum = void 0;

              if (destroy === null) {
                addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
              } else if (typeof destroy.then === 'function') {
                addendum = '\n\nIt looks like you wrote useEffect(async () => ...) or returned a Promise. ' + 'Instead, write the async function inside your effect ' + 'and call it immediately:\n\n' + 'useEffect(() => {\n' + '  async function fetchData() {\n' + '    // You can await here\n' + '    const response = await MyAPI.getData(someId);\n' + '    // ...\n' + '  }\n' + '  fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching';
              } else {
                addendum = ' You returned: ' + destroy;
              }

              error('An effect function must not return anything besides a function, ' + 'which is used for clean-up.%s', addendum);
            }
          }
        }

        effect = effect.next;
      } while (effect !== firstEffect);
    }
  }

这段代码也很简单,对于函数组件updateQueue是一个effect环状链表,执行tag=3,即useLayoutEffect回调。 这也意味着,这个阶段useLayoutEffect开始执行了,但要注意的是useEffect并没有执行,因为他的tag = 5。

schedulePassiveEffects

 function schedulePassiveEffects(finishedWork) {
    var updateQueue = finishedWork.updateQueue;
    var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
    if (lastEffect !== null) {
      var firstEffect = lastEffect.next;
      var effect = firstEffect;
      do {
        var _effect = effect,
            next = _effect.next,
            tag = _effect.tag;
       
        if ((tag & Passive$1) !== NoFlags$1 && (tag & HasEffect) !== NoFlags$1) {
         
          enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
         
          enqueuePendingPassiveHookEffectMount(finishedWork, effect);
        }

        effect = next;
      } while (effect !== firstEffect);
    }
}

这里也是如此,代码也很简单,对于函数组件,如果是Passive或HasEffect,执行。这里就过滤useLayoutEffect了,这里是useEffect进入。

enqueuePendingPassiveHookEffectUnmount

function enqueuePendingPassiveHookEffectUnmount(fiber, effect) {
    pendingPassiveHookEffectsUnmount.push(effect, fiber);

    {
      fiber.flags |= PassiveUnmountPendingDev;
      var alternate = fiber.alternate;

      if (alternate !== null) {
        alternate.flags |= PassiveUnmountPendingDev;
      }
    }

    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalPriority$1, function () {
        flushPassiveEffects();
        return null;
      });
    }
  }

enqueuePendingPassiveHookEffectMount

function enqueuePendingPassiveHookEffectMount(fiber, effect) {
    pendingPassiveHookEffectsMount.push(effect, fiber);

    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalPriority$1, function () {
        flushPassiveEffects();
        return null;
      });
    }
  }

这里也很简单,将effect添加到pendingPassiveHookEffectsMount队列中。而rootDoesHavePassiveEffects在commit第一阶段被设置成了true,下面的调度不会执行。

好了,至此commit三阶段核心内容结束了。

那么问题来了:pendingPassiveHookEffectsMount这个东西有啥用?

这个东西非常重要,这个是react执行useEffect回调的任务队列。但为什么commit三阶段之后没有地方调用呢?

这待回到scheduleCallback异步宏任务里,回调中才会用到pendingPassiveHookEffectsMount。

下一章节,我们重点聊聊schedule

码字不易,多多点赞关注~