翼度科技»论坛 编程开发 JavaScript 查看内容

React渲染机制超详细讲解

6

主题

6

帖子

18

积分

新手上路

Rank: 1

积分
18
准备工作

为了方便讲解,假设我们有下面这样一段代码:
  1. function App(){
  2.   const [count, setCount] = useState(0)
  3.   useEffect(() => {
  4.     setCount(1)
  5.   }, [])
  6.   const handleClick = () => setCount(count => count++)
  7.   return (
  8.     <div>
  9.         勇敢牛牛,        <span>不怕困难</span>
  10.         <span onClick={handleClick}>{count}</span>
  11.     </div>
  12.   )
  13. }
  14. ReactDom.render(<App />, document.querySelector('#root'))
复制代码
在React项目中,这种jsx语法首先会被编译成:
  1. React.createElement("App", null)
  2. or
  3. jsx("App", null)
复制代码
这里不详说编译方法,感兴趣的可以参考:
babel在线编译
新的jsx转换
jsx语法转换后,会通过
  1. creatElement
复制代码
  1. jsx
复制代码
的api转换为
  1. React element
复制代码
作为
  1. ReactDom.render()
复制代码
的第一个参数进行渲染。
在上一篇文章
  1. Fiber
复制代码
中,我们提到过一个React项目会有一个
  1. fiberRoot
复制代码
和一个或多个
  1. rootFiber
复制代码
  1. fiberRoot
复制代码
是一个项目的根节点。我们在开始真正的渲染前会先基于
  1. root
复制代码
DOM创建
  1. fiberRoot
复制代码
,且
  1. fiberRoot.current = rootFiber
复制代码
,这里的
  1. rootFiber
复制代码
就是
  1. current
复制代码
fiber树的根节点。
  1. if (!root) {
  2.     // Initial mount
  3.     root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
  4.     fiberRoot = root._internalRoot;
  5. }
复制代码
在创建好
  1. fiberRoot
复制代码
  1. rootFiber
复制代码
后,我们还不知道接下来要做什么,因为它们和我们的
  1. <App />
复制代码
函数组件没有一点关联。这时React开始创建
  1. update
复制代码
,并将
  1. ReactDom.render()
复制代码
的第一个参数,也就是基于
  1. <App />
复制代码
创建的
  1. React element
复制代码
赋给
  1. update
复制代码
  1. var update = {
  2.     eventTime: eventTime,
  3.     lane: lane,
  4.     tag: UpdateState,
  5.     payload: null,
  6.     callback: element,
  7.     next: null
  8.   };
复制代码
有了这个
  1. update
复制代码
,还需要将它加入到更新队列中,等待后续进行更新。在这里有必要讲下这个队列的创建流程,这个创建操作在React有多次应用。
  1. var sharedQueue = updateQueue.shared;
  2.   var pending = sharedQueue.pending;
  3.   if (pending === null) {   
  4.   // mount时只有一个update,直接闭环
  5.     update.next = update;
  6.   } else {   
  7.   // update时,将最新的update的next指向上一次的update, 上一次的update的next又指向最新的update形成闭环
  8.     update.next = pending.next;
  9.     pending.next = update;
  10.   }
  11.   // pending指向最新的update, 这样我们遍历update链表时, pending.next会指向第一个插入的update。
  12.   sharedQueue.pending = update;   
复制代码
我将上面的代码进行了一下抽象,更新队列是一个环形链表结构,每次向链表结尾添加一个
  1. update
复制代码
时,指针都会指向这个
  1. update
复制代码
,并且这个
  1. update.next
复制代码
会指向第一个更新:

上一篇文章也讲过,React最多会同时拥有两个
  1. fiber
复制代码
树,一个是
  1. current
复制代码
fiber树,另一个是
  1. workInProgress
复制代码
fiber树。
  1. current
复制代码
fiber树的根节点在上面已经创建,下面会通过拷贝
  1. fiberRoot.current
复制代码
的形式创建
  1. workInProgress
复制代码
fiber树的根节点。
到这里,前面的准备工作就做完了, 接下来进入正菜,开始进行循环遍历,生成
  1. fiber
复制代码
树和
  1. dom
复制代码
树,并最终渲染到页面中。相关参考视频讲解:进入学习

render阶段

这个阶段并不是指把代码渲染到页面上,而是基于我们的代码画出对应的
  1. fiber
复制代码
树和
  1. dom
复制代码
树。

workloopSync
  1. function workLoopSync() {
  2.   while (workInProgress !== null) {
  3.     performUnitOfWork(workInProgress);
  4.   }
  5. }
复制代码
在这个循环里,会不断根据workInProgress找到对应的child作为下次循环的workInProgress,直到遍历到叶子节点,即深度优先遍历。在
  1. performUnitOfWork
复制代码
会执行下面的
  1. beginWork
复制代码



beginWork

简单描述下
  1. beginWork
复制代码
的工作,就是生成
  1. fiber
复制代码
树。
基于
  1. workInProgress
复制代码
的根节点生成
  1. <App />
复制代码
  1. fiber
复制代码
节点并将这个节点作为根节点的
  1. child
复制代码
,然后基于
  1. <App />
复制代码
  1. fiber
复制代码
节点生成
  1. <div />
复制代码
  1. fiber
复制代码
节点并作为
  1. <App />
复制代码
  1. fiber
复制代码
节点的
  1. child
复制代码
,如此循环直到最下面的
  1. 牛牛
复制代码
文本。

注意, 在上面流程图中,
  1. updateFunctionComponent
复制代码
会执行一个
  1. renderWithHooks
复制代码
函数,这个函数里面会执行
  1. App()
复制代码
这个函数组件,在这里会初始化函数组件里所有的
  1. hooks
复制代码
,也就是上面实例代码的
  1. useState()
复制代码

当遍历到牛牛文本时,它的下面已经没有了
  1. child
复制代码
,这时
  1. beginWork
复制代码
的工作就暂时告一段落,为什么说是暂时,是因为在
  1. completeWork
复制代码
时,如果遍历的
  1. fiber
复制代码
节点有
  1. sibling
复制代码
会再次走到
  1. beginWork
复制代码


completeWork

当遍历到牛牛文本后,会进入这个
  1. completeWork
复制代码

在这里,我们再简单描述下
  1. completeWork
复制代码
的工作, 就是生成
  1. dom
复制代码
树。
基于
  1. fiber
复制代码
节点生成对应的
  1. dom
复制代码
节点,并且将这个
  1. dom
复制代码
节点作为父节点,将之前生成的
  1. dom
复制代码
节点插入到当前创建的
  1. dom
复制代码
节点。并会基于在
  1. beginWork
复制代码
生成的不完全的
  1. workInProgress
复制代码
fiber树向上查找,直到
  1. fiberRoot
复制代码
。在这个向上的过程中,会去判断是否有
  1. sibling
复制代码
,如果有会再次走
  1. beginWork
复制代码
,没有就继续向上。这样到了根节点,一个完整的
  1. dom
复制代码
树就生成了。

额外提一下,在
  1. completeWork
复制代码
中有这样一段代码
  1. if (flags > PerformedWork) {
  2.   if (returnFiber.lastEffect !== null) {
  3.     returnFiber.lastEffect.nextEffect = completedWork;
  4.   } else {
  5.     returnFiber.firstEffect = completedWork;
  6.   }
  7.   returnFiber.lastEffect = completedWork;
  8. }
复制代码
解释一下,
  1. flags > PerformedWork
复制代码
代表当前这个
  1. fiber
复制代码
节点是有副作用的,需要将这个
  1. fiber
复制代码
节点加入到父级
  1. fiber
复制代码
  1. effectList
复制代码
链表中。

commit阶段

这个阶段的主要工作是处理副作用。所谓副作用就是不确定操作,比如:插入,替换,删除DOM,还有
  1. useEffect()
复制代码
hook的回调函数都会被作为副作用。

commitWork

准备工作
  1. commitWork
复制代码
前,会将在
  1. workloopSync
复制代码
中生成的
  1. workInProgress
复制代码
fiber树赋值给
  1. fiberRoot
复制代码
  1. finishedWork
复制代码
属性。
  1. var finishedWork = root.current.alternate;  // workInProgress fiber树
  2. root.finishedWork = finishedWork;  // 这里的root是fiberRoot
  3. root.finishedLanes = lanes;
  4. commitRoot(root);
复制代码
在上面我们提到,如果一个
  1. fiber
复制代码
节点有副作用会被记录到父级
  1. fiber
复制代码
  1. lastEffect
复制代码
  1. nextEffect
复制代码

在下面代码中,如果
  1. fiber
复制代码
树有副作用,会将
  1. rootFiber.firstEffect
复制代码
节点作为第一个副作用
  1. firstEffect
复制代码
,并且将
  1. effectList
复制代码
形成闭环。
  1. 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之前的阶段,遍历
  1. effectList
复制代码
链表,执行
  1. commitBeforeMutationEffects
复制代码
方法。
  1. do {  // mutation之前
  2.   invokeGuardedCallback(null, commitBeforeMutationEffects, null);
  3. } while (nextEffect !== null);
复制代码
我们进到
  1. commitBeforeMutationEffects
复制代码
方法,我将代码简化一下:
  1. function commitBeforeMutationEffects() {
  2.   while (nextEffect !== null) {
  3.     var current = nextEffect.alternate;
  4.     // 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑;
  5.     if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null){...}
  6.     var flags = nextEffect.flags;
  7.     // 调用getSnapshotBeforeUpdate,fiberRoot和ClassComponent会走这里
  8.     if ((flags & Snapshot) !== NoFlags) {...}
  9.     // 调度useEffect(异步)
  10.     if ((flags & Passive) !== NoFlags) {
  11.       // rootDoesHavePassiveEffects变量表示当前是否有副作用
  12.       if (!rootDoesHavePassiveEffects) {
  13.         rootDoesHavePassiveEffects = true;
  14.         // 创建任务并加入任务队列,会在layout阶段之后触发
  15.         scheduleCallback(NormalPriority$1, function () {
  16.           flushPassiveEffects();
  17.           return null;
  18.         });
  19.       }
  20.     }
  21.     // 继续遍历下一个effect
  22.     nextEffect = nextEffect.nextEffect;
  23.     }
  24. }
复制代码
按照我们示例代码,我们重点关注第三件事,调度useEffect(注意,这里是调度,并不会马上执行)。
  1. scheduleCallback
复制代码
主要工作是创建一个
  1. task
复制代码
  1. var newTask = {
  2.     id: taskIdCounter++,
  3.     callback: callback,  //上面代码传入的回调函数
  4.     priorityLevel: priorityLevel,
  5.     startTime: startTime,
  6.     expirationTime: expirationTime,
  7.     sortIndex: -1
  8. };
复制代码
它里面有个逻辑会判断
  1. startTime
复制代码
  1. currentTime
复制代码
, 如果
  1. startTime > currentTime
复制代码
,会把这个任务加入到定时任务队列
  1. timerQueue
复制代码
,反之会加入任务队列
  1. taskQueue
复制代码
,并
  1. task.sortIndex = expirationTime
复制代码


mutation

简单描述mutation阶段的工作就是负责dom渲染。
区分
  1. fiber.flags
复制代码
,进行不同的操作,比如:重置文本,重置ref,插入,替换,删除dom节点。
和mutation之前阶段一样,也是遍历
  1. effectList
复制代码
链表,执行
  1. commitMutationEffects
复制代码
方法。
  1. do {    // mutation  dom渲染
  2.   invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);
  3. } while (nextEffect !== null);
复制代码
看下
  1. commitMutationEffects
复制代码
的主要工作:
  1. function commitMutationEffects(root, renderPriorityLevel) {
  2.   // TODO: Should probably move the bulk of this function to commitWork.
  3.   while (nextEffect !== null) {     // 遍历EffectList
  4.     setCurrentFiber(nextEffect);
  5.     // 根据flags分别处理
  6.     var flags = nextEffect.flags;
  7.     // 根据 ContentReset flags重置文字节点
  8.     if (flags & ContentReset) {...}
  9.     // 更新ref
  10.     if (flags & Ref) {...}
  11.     var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
  12.     switch (primaryFlags) {
  13.       case Placement:   // 插入dom
  14.         {...}
  15.       case PlacementAndUpdate:    //插入dom并更新dom
  16.         {
  17.           // Placement
  18.           commitPlacement(nextEffect);
  19.           nextEffect.flags &= ~Placement; // Update
  20.           var _current = nextEffect.alternate;
  21.           commitWork(_current, nextEffect);
  22.           break;
  23.         }
  24.       case Hydrating:     //SSR
  25.         {...}
  26.       case HydratingAndUpdate:      // SSR
  27.         {...}
  28.       case Update:      // 更新dom
  29.         {...}
  30.       case Deletion:    // 删除dom
  31.         {...}
  32.     }
  33.     resetCurrentFiber();
  34.     nextEffect = nextEffect.nextEffect;
  35.   }
  36. }
复制代码
按照我们的示例代码,这里会走
  1. PlacementAndUpdate
复制代码
,首先是
  1. commitPlacement(nextEffect)
复制代码
方法,在一串判断后,最后会把我们生成的
  1. dom
复制代码
树插入到
  1. root
复制代码
DOM节点中。
  1. function appendChildToContainer(container, child) {
  2.   var parentNode;
  3.   if (container.nodeType === COMMENT_NODE) {
  4.     parentNode = container.parentNode;
  5.     parentNode.insertBefore(child, container);
  6.   } else {
  7.     parentNode = container;
  8.     parentNode.appendChild(child);    // 直接将整个dom作为子节点插入到root中
  9.   }
  10. }
复制代码
到这里,代码终于真正的渲染到了页面上。下面的
  1. commitWork
复制代码
方法是执行和
  1. useLayoutEffect()
复制代码
有关的东西,这里不做重点,后面文章安排,我们只要知道这里是执行上一次更新的
  1. effect unmount
复制代码


fiber树切换

在讲
  1. layout
复制代码
阶段之前,先来看下这行代码
  1. root.current = finishedWork  // 将`workInProgress`fiber树变成`current`树
复制代码
这行代码在mutation和layout阶段之间。在mutation阶段, 此时的
  1. current
复制代码
fiber树还是指向更新前的
  1. fiber
复制代码
树, 这样在生命周期钩子内获取的DOM就是更新前的, 类似于
  1. componentDidMount
复制代码
  1. compentDidUpdate
复制代码
的钩子是在
  1. layout
复制代码
阶段执行的,这样就能获取到更新后的DOM进行操作。

layout

简单描述layout阶段的工作:

  • 调用生命周期或hooks相关操作
  • 赋值ref
和mutation之前阶段一样,也是遍历
  1. effectList
复制代码
链表,执行
  1. commitLayoutEffects
复制代码
方法。
  1. do {   // 调用生命周期和hook相关操作, 赋值ref
  2.    invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
  3. } while (nextEffect !== null);
复制代码
来看下
  1. commitLayoutEffects
复制代码
方法:
  1. function commitLayoutEffects(root, committedLanes) {
  2.   while (nextEffect !== null) {
  3.     setCurrentFiber(nextEffect);
  4.     var flags = nextEffect.flags;
  5.     // 调用生命周期或钩子函数
  6.     if (flags & (Update | Callback)) {
  7.       var current = nextEffect.alternate;
  8.       commitLifeCycles(root, current, nextEffect);
  9.     }
  10.     {
  11.       // 获取dom实例,更新ref
  12.       if (flags & Ref) {
  13.         commitAttachRef(nextEffect);
  14.       }
  15.     }
  16.     resetCurrentFiber();
  17.     nextEffect = nextEffect.nextEffect;
  18.   }
  19. }
复制代码
提一下,
  1. useLayoutEffect()
复制代码
的回调会在
  1. commitLifeCycles
复制代码
方法中执行,而
  1. useEffect()
复制代码
的回调会在
  1. commitLifeCycles
复制代码
中的
  1. schedulePassiveEffects
复制代码
方法进行调度。从这里就可以看出
  1. useLayoutEffect()
复制代码
  1. useEffect()
复制代码
的区别:

    1. useLayoutEffect
    复制代码
    的上次更新销毁函数在
    1. mutation
    复制代码
    阶段销毁,本次更新回调函数是在dom渲染后的
    1. layout
    复制代码
    阶段同步执行;
    1. useEffect
    复制代码
    1. mutation之前
    复制代码
    阶段会创建调度任务,在
    1. layout
    复制代码
    阶段会将销毁函数和回调函数加入到
    1. pendingPassiveHookEffectsUnmount
    复制代码
    1. pendingPassiveHookEffectsMount
    复制代码
    队列中,最终它的上次更新销毁函数和本次更新回调函数都是在
    1. layout
    复制代码
    阶段后异步执行; 可以明确一点,他们的更新都不会阻塞dom渲染。

layout之后

还记得在
  1. mutation之前
复制代码
阶段的这几行代码吗?
  1. // 创建任务并加入任务队列,会在layout阶段之后触发
  2. scheduleCallback(NormalPriority$1, function () {
  3.   flushPassiveEffects();
  4.   return null;
  5. });
复制代码
这里就是在调度
  1. useEffect()
复制代码
,在
  1. layout
复制代码
阶段之后会执行这个回调函数,此时会处理
  1. useEffect
复制代码
的上次更新销毁函数和本次更新回调函数。

总结

看完这篇文章, 我们可以弄明白下面这几个问题:

  • React的渲染流程是怎样的?
  • React的beginWork都做了什么?
  • React的completeWork都做了什么?
  • React的commitWork都做了什么?
  • useEffect和useLayoutEffect的区别是什么?
  • useEffect和useLayoutEffect的销毁函数和更新回调的调用时机?
到此这篇关于React渲染机制超详细讲解的文章就介绍到这了,更多相关React渲染机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

来源:https://www.jb51.net/article/266712.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具