好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

React渲染机制超详细讲解

准备工作

为了方便讲解,假设我们有下面这样一段代码:

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)
or
jsx("App", null)

这里不详说编译方法,感兴趣的可以参考:

babel在线编译

新的jsx转换

jsx语法转换后,会通过 creatElement 或 jsx 的api转换为 React element 作为 ReactDom.render() 的第一个参数进行渲染。

在上一篇文章 Fiber 中,我们提到过一个React项目会有一个 fiberRoot 和一个或多个 rootFiber 。 fiberRoot 是一个项目的根节点。我们在开始真正的渲染前会先基于 root DOM创建 fiberRoot ,且 fiberRoot.current = rootFiber ,这里的 rootFiber 就是 current fiber树的根节点。

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 树,一个是 current fiber树,另一个是 workInProgress fiber树。 current fiber树的根节点在上面已经创建,下面会通过拷贝 fiberRoot.current 的形式创建 workInProgress fiber树的根节点。

到这里,前面的准备工作就做完了, 接下来进入正菜,开始进行循环遍历,生成 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 生成的不完全的 workInProgress fiber树向上查找,直到 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 中生成的 workInProgress fiber树赋值给 fiberRoot 的 finishedWork 属性。

var finishedWork = root.current.alternate;  // workInProgress fiber树
root.finishedWork = finishedWork;  // 这里的root是fiberRoot
root.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 树插入到 root DOM节点中。

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阶段, 此时的 current fiber树还是指向更新前的 fiber 树, 这样在生命周期钩子内获取的DOM就是更新前的, 类似于 componentDidMount 和 compentDidUpdate 的钩子是在 layout 阶段执行的,这样就能获取到更新后的DOM进行操作。

layout

简单描述layout阶段的工作:

调用生命周期或hooks相关操作 赋值ref

和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的销毁函数和更新回调的调用时机?

到此这篇关于React渲染机制超详细讲解的文章就介绍到这了,更多相关React渲染机制内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

查看更多关于React渲染机制超详细讲解的详细内容...

  阅读:37次