准备工作
为了方便讲解,假设我们有下面这样一段代码:
 function App(){  const [count, setCount] = useState(0)
  useEffect(() => {    setCount(1)  }, [])
  const handleClick = () => setCount(count => count++)
  return (    <div>        勇敢牛牛,        <span>不怕困难</span>        <span onClick={handleClick}>{count}</span>    </div>  )}
ReactDom.render(<App />, document.querySelector('#root'))
   复制代码
 
在 React 项目中,这种 jsx 语法首先会被编译成:
 React.createElement("App", null)orjsx("App", null)
   复制代码
 
这里不详说编译方法,感兴趣的可以参考:
babel 在线编译
新的 jsx 转换
jsx 语法转换后,会通过creatElement或jsx的 api 转换为React element作为ReactDom.render()的第一个参数进行渲染。
在上一篇文章Fiber中,我们提到过一个 React 项目会有一个fiberRoot和一个或多个rootFiber。fiberRoot是一个项目的根节点。我们在开始真正的渲染前会先基于rootDOM 创建fiberRoot,且fiberRoot.current = rootFiber,这里的rootFiber就是currentfiber 树的根节点。
 if (!root) {    // Initial mount    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);    fiberRoot = root._internalRoot;}
   复制代码
 
在创建好fiberRoot和rootFiber后,我们还不知道接下来要做什么,因为它们和我们的<App />函数组件没有一点关联。这时 React 开始创建update,并将ReactDom.render()的第一个参数,也就是基于<App />创建的React element赋给update。
 var update = {    eventTime: eventTime,    lane: lane,    tag: UpdateState,    payload: null,    callback: element,    next: null  };
   复制代码
 
有了这个update,还需要将它加入到更新队列中,等待后续进行更新。在这里有必要讲下这个队列的创建流程,这个创建操作在 React 有多次应用。
 var sharedQueue = updateQueue.shared;  var pending = sharedQueue.pending;
  if (pending === null) {     // mount时只有一个update,直接闭环    update.next = update;  } else {     // update时,将最新的update的next指向上一次的update, 上一次的update的next又指向最新的update形成闭环    update.next = pending.next;    pending.next = update;  }  // pending指向最新的update, 这样我们遍历update链表时, pending.next会指向第一个插入的update。  sharedQueue.pending = update;   
   复制代码
 
我将上面的代码进行了一下抽象,更新队列是一个环形链表结构,每次向链表结尾添加一个update时,指针都会指向这个update,并且这个update.next会指向第一个更新:
上一篇文章也讲过,React 最多会同时拥有两个fiber树,一个是currentfiber 树,另一个是workInProgressfiber 树。currentfiber 树的根节点在上面已经创建,下面会通过拷贝fiberRoot.current的形式创建workInProgressfiber 树的根节点。
到这里,前面的准备工作就做完了, 接下来进入正菜,开始进行循环遍历,生成fiber树和dom树,并最终渲染到页面中。
render 阶段
这个阶段并不是指把代码渲染到页面上,而是基于我们的代码画出对应的fiber树和dom树。
workloopSync
 function workLoopSync() {  while (workInProgress !== null) {    performUnitOfWork(workInProgress);  }}
   复制代码
 
在这个循环里,会不断根据 workInProgress 找到对应的 child 作为下次循环的 workInProgress,直到遍历到叶子节点,即深度优先遍历。在performUnitOfWork会执行下面的beginWork。
beginWork
简单描述下beginWork的工作,就是生成fiber树。
基于workInProgress的根节点生成<App />的fiber节点并将这个节点作为根节点的child,然后基于<App />的fiber节点生成<div />的fiber节点并作为<App />的fiber节点的child,如此循环直到最下面的牛牛文本。
注意, 在上面流程图中,updateFunctionComponent会执行一个renderWithHooks函数,这个函数里面会执行App()这个函数组件,在这里会初始化函数组件里所有的hooks,也就是上面实例代码的useState()。
当遍历到牛牛文本时,它的下面已经没有了child,这时beginWork的工作就暂时告一段落,为什么说是暂时,是因为在completeWork时,如果遍历的fiber节点有sibling会再次走到beginWork。
completeWork
当遍历到牛牛文本后,会进入这个completeWork。
在这里,我们再简单描述下completeWork的工作, 就是生成dom树。
基于fiber节点生成对应的dom节点,并且将这个dom节点作为父节点,将之前生成的dom节点插入到当前创建的dom节点。并会基于在beginWork生成的不完全的workInProgressfiber 树向上查找,直到fiberRoot。在这个向上的过程中,会去判断是否有sibling,如果有会再次走beginWork,没有就继续向上。这样到了根节点,一个完整的dom树就生成了。
额外提一下,在completeWork中有这样一段代码
 if (flags > PerformedWork) {  if (returnFiber.lastEffect !== null) {    returnFiber.lastEffect.nextEffect = completedWork;  } else {    returnFiber.firstEffect = completedWork;  }
  returnFiber.lastEffect = completedWork;}
   复制代码
 
解释一下, flags > PerformedWork代表当前这个fiber节点是有副作用的,需要将这个fiber节点加入到父级fiber的effectList链表中。
commit 阶段
这个阶段的主要工作是处理副作用。所谓副作用就是不确定操作,比如:插入,替换,删除 DOM,还有useEffect()hook 的回调函数都会被作为副作用。
commitWork
准备工作
在commitWork前,会将在workloopSync中生成的workInProgressfiber 树赋值给fiberRoot的finishedWork属性。相关参考视频讲解:进入学习
 var finishedWork = root.current.alternate;  // workInProgress fiber树root.finishedWork = finishedWork;  // 这里的root是fiberRootroot.finishedLanes = lanes;commitRoot(root);
   复制代码
 
在上面我们提到,如果一个fiber节点有副作用会被记录到父级fiber的lastEffect的nextEffect。在下面代码中,如果fiber树有副作用,会将rootFiber.firstEffect节点作为第一个副作用firstEffect,并且将effectList形成闭环。
 var firstEffect;// 判断当前rootFiber树是否有副作用if (finishedWork.flags > PerformedWork) {
    // 下面代码的目的还是为了将这个effectList链表形成闭环    if (finishedWork.lastEffect !== null) {      finishedWork.lastEffect.nextEffect = finishedWork;      firstEffect = finishedWork.firstEffect;    } else {      firstEffect = finishedWork;    }} else {// 这个rootFiber树没有副作用firstEffect = finishedWork.firstEffect;}
   复制代码
 mutation 之前
简单描述 mutation 之前阶段的工作:
- 处理 DOM 节点渲染/删除后的 autoFocus、blur 逻辑; 
- 调用 getSnapshotBeforeUpdate,fiberRoot 和 ClassComponent 会走这里; 
- 调度 useEffect(异步);在 mutation 之前的阶段,遍历- effectList链表,执行- commitBeforeMutationEffects方法。
 
 do {  // mutation之前
  invokeGuardedCallback(null, commitBeforeMutationEffects, null);
} while (nextEffect !== null);
   复制代码
 
我们进到commitBeforeMutationEffects方法,我将代码简化一下:
 function commitBeforeMutationEffects() {  while (nextEffect !== null) {    var current = nextEffect.alternate;    // 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑;    if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null){...}
    var flags = nextEffect.flags;    // 调用getSnapshotBeforeUpdate,fiberRoot和ClassComponent会走这里    if ((flags & Snapshot) !== NoFlags) {...}    // 调度useEffect(异步)    if ((flags & Passive) !== NoFlags) {      // rootDoesHavePassiveEffects变量表示当前是否有副作用      if (!rootDoesHavePassiveEffects) {        rootDoesHavePassiveEffects = true;        // 创建任务并加入任务队列,会在layout阶段之后触发        scheduleCallback(NormalPriority$1, function () {          flushPassiveEffects();          return null;        });      }    }    // 继续遍历下一个effect    nextEffect = nextEffect.nextEffect;    }}
   复制代码
 
按照我们示例代码,我们重点关注第三件事,调度 useEffect(注意,这里是调度,并不会马上执行)。
scheduleCallback主要工作是创建一个task:
 var newTask = {    id: taskIdCounter++,    callback: callback,  //上面代码传入的回调函数    priorityLevel: priorityLevel,    startTime: startTime,    expirationTime: expirationTime,    sortIndex: -1};
   复制代码
 
它里面有个逻辑会判断startTime和currentTime, 如果startTime > currentTime,会把这个任务加入到定时任务队列timerQueue,反之会加入任务队列taskQueue,并task.sortIndex = expirationTime。
mutation
简单描述 mutation 阶段的工作就是负责 dom 渲染。
区分fiber.flags,进行不同的操作,比如:重置文本,重置 ref,插入,替换,删除 dom 节点。
和 mutation 之前阶段一样,也是遍历effectList链表,执行commitMutationEffects方法。
 do {    // mutation  dom渲染  invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);
} while (nextEffect !== null);
   复制代码
 
看下commitMutationEffects的主要工作:
 function commitMutationEffects(root, renderPriorityLevel) {  // TODO: Should probably move the bulk of this function to commitWork.  while (nextEffect !== null) {     // 遍历EffectList    setCurrentFiber(nextEffect);    // 根据flags分别处理    var flags = nextEffect.flags;    // 根据 ContentReset flags重置文字节点    if (flags & ContentReset) {...}    // 更新ref    if (flags & Ref) {...}
    var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
    switch (primaryFlags) {      case Placement:   // 插入dom        {...}
      case PlacementAndUpdate:    //插入dom并更新dom        {          // Placement          commitPlacement(nextEffect);          nextEffect.flags &= ~Placement; // Update          var _current = nextEffect.alternate;          commitWork(_current, nextEffect);          break;        }
      case Hydrating:     //SSR        {...}
      case HydratingAndUpdate:      // SSR        {...}
      case Update:      // 更新dom        {...}
      case Deletion:    // 删除dom        {...}    }
    resetCurrentFiber();    nextEffect = nextEffect.nextEffect;  }}
   复制代码
 
按照我们的示例代码,这里会走PlacementAndUpdate,首先是commitPlacement(nextEffect)方法,在一串判断后,最后会把我们生成的dom树插入到rootDOM 节点中。
 function appendChildToContainer(container, child) {  var parentNode;
  if (container.nodeType === COMMENT_NODE) {    parentNode = container.parentNode;    parentNode.insertBefore(child, container);  } else {    parentNode = container;    parentNode.appendChild(child);    // 直接将整个dom作为子节点插入到root中  }}
   复制代码
 
到这里,代码终于真正的渲染到了页面上。下面的commitWork方法是执行和useLayoutEffect()有关的东西,这里不做重点,后面文章安排,我们只要知道这里是执行上一次更新的effect unmount。
fiber 树切换
在讲layout阶段之前,先来看下这行代码
 root.current = finishedWork  // 将`workInProgress`fiber树变成`current`树
   复制代码
 
这行代码在 mutation 和 layout 阶段之间。在 mutation 阶段, 此时的currentfiber 树还是指向更新前的fiber树, 这样在生命周期钩子内获取的 DOM 就是更新前的, 类似于componentDidMount和compentDidUpdate的钩子是在layout阶段执行的,这样就能获取到更新后的 DOM 进行操作。
layout
简单描述 layout 阶段的工作:
和 mutation 之前阶段一样,也是遍历effectList链表,执行commitLayoutEffects方法。
 do {   // 调用生命周期和hook相关操作, 赋值ref   invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);} while (nextEffect !== null);
   复制代码
 
来看下commitLayoutEffects方法:
 function commitLayoutEffects(root, committedLanes) {  while (nextEffect !== null) {    setCurrentFiber(nextEffect);    var flags = nextEffect.flags;    // 调用生命周期或钩子函数    if (flags & (Update | Callback)) {      var current = nextEffect.alternate;      commitLifeCycles(root, current, nextEffect);    }
    {      // 获取dom实例,更新ref      if (flags & Ref) {        commitAttachRef(nextEffect);      }    }
    resetCurrentFiber();    nextEffect = nextEffect.nextEffect;  }}
   复制代码
 
提一下,useLayoutEffect()的回调会在commitLifeCycles方法中执行,而useEffect()的回调会在commitLifeCycles中的schedulePassiveEffects方法进行调度。从这里就可以看出useLayoutEffect()和useEffect()的区别:
- useLayoutEffect的上次更新销毁函数在- mutation阶段销毁,本次更新回调函数是在 dom 渲染后的- layout阶段同步执行;
 
- useEffect在- mutation之前阶段会创建调度任务,在- layout阶段会将销毁函数和回调函数加入到- pendingPassiveHookEffectsUnmount和- pendingPassiveHookEffectsMount队列中,最终它的上次更新销毁函数和本次更新回调函数都是在- layout阶段后异步执行; 可以明确一点,他们的更新都不会阻塞 dom 渲染。
 
layout 之后
还记得在mutation之前阶段的这几行代码吗?
 // 创建任务并加入任务队列,会在layout阶段之后触发scheduleCallback(NormalPriority$1, function () {  flushPassiveEffects();  return null;});
   复制代码
 
这里就是在调度useEffect(),在layout阶段之后会执行这个回调函数,此时会处理useEffect的上次更新销毁函数和本次更新回调函数。
总结
看完这篇文章, 我们可以弄明白下面这几个问题:
- React 的渲染流程是怎样的? 
- React 的 beginWork 都做了什么? 
- React 的 completeWork 都做了什么? 
- React 的 commitWork 都做了什么? 
- useEffect 和 useLayoutEffect 的区别是什么? 
- useEffect 和 useLayoutEffect 的销毁函数和更新回调的调用时机? 
评论