《深入浅出react》总结之 10.3 Commit阶段流程探秘

【学习目标】 看懂下面代码的执行顺序:

js 复制代码
function Son(){  
    React.useEffect(() =>{  
    console.log( --------Son useEffect------- )  
    })  
    React.useLayoutEffect(() =>{  
    console.log( --------Son useLayoutEffect------- )  
    })  
    React.useInsertionEffect(() =>{  
    console.log( --------Son useInsertionEffect------- )  
    })  
    return <div>子组件</div>
}  
function Father(){  
    React.useEffect(() =>{  
    console.log( --------Father useEffect------- )  
    })
    React.useLayoutEffect(() =>{  
    console.log( --------Father useLayoutEffect------- )  
    })  
    React.useInsertionEffect(() =>{  
    console.log( --------Father useInsertionEffect------- )  
    })  
    return <div>  
        <div>父组件</div>  
        <Son/>  
       </div>  
}

猜到打印顺序了吗?

js 复制代码
--------Son useInsertionEffect-------  
--------Father useInsertionEffect-------  
--------Son useLayoutEffect-------  
--------Father useLayoutEffect-------  
--------Son useEffect-------  
--------Father useEffect-------

这段打印结果反应了两个问题

第一, useInsertionEffect,useLayoutEffect,useEffect 的执行顺序都是先子后父

第二,为什么 useInsertionEffect,useLayoutEffect,useEffect 的顺序是这样的?

那我们 业务代码 联动 commit源码就懂了

js 复制代码
function Index(){  
    const [color, setColor ]= React.useState( #000 )  
    React.useEffect(() =>{  
    console.log( --------useEffect------- )  
    })  
    React.useLayoutEffect(() =>{  
    console.log( --------useLayoutEffect------- )  
    })  
    React.useInsertionEffect(() =>{  
    console.log( --------useInsertionEffect------- )  
    })  
    return <div>  
        <div id ="text" style ={{ color }}> hello,react </div>
        <button onClick ={() => setColor( red )} >点击改变颜色</button>  
    </div>  
}
js 复制代码
function commitRootImpl(){  
    if ((finishedWork.subtreeFlags & PassiveMask)! = = NoFlags || (finishedWork.flags & PassiveMask)! = = NoFlags) {  
        /* 通过异步的方式处理 useEffect */  
        scheduleCallback $1(NormalPriority, function () {  
            flushPassiveEffects();  
            return null;  
        });  
    }  
    /* BeforeMutation 阶段执行 */  
    const text = document.getElementById( text )  
    console.log( -----BeforeMutation 执行------- )  
    commitBeforeMutationEffects(root, finishedWork);  
    console.log( -----BeforeMutation 执行完毕------ )  
    /* Mutation 阶段执行 */  
    console.log( -----Mutation 执行----- )  
    if(text) console.log( 颜色获取:,window.getComputedStyle(text).color)  
    commitMutationEffects(root, finishedWork, lanes);  
    console.log( -----Mutation 执行完毕----- )  
    if(text) console.log( 颜色获取:,window.getComputedStyle(text).color)  
    /* Layout 阶段执行 */  
    console.log( -----Layout 执行----- )  
    commitLayoutEffects(finishedWork, root, lanes);  
    console.log( -----Layout 执行完毕----- )  
}

打印顺序如下

js 复制代码
-----BeforeMutation 执行-------
-----BeforeMutation 执行完毕------
-----Mutation 执行-----
颜色获取:rgb(0,0,0)
 --------useInsertionEffect-------
 -----Mutation 执行完毕-----
 颜色获取:rgb(255,0,0)
 -----Layout 执行-----
 --------useLayoutEffect-------
 -----Layout 执行完毕-----
 --------useEffect-------

从源码和打印顺序我们看到:

  • commit阶段 分为三个阶段 :BeforeMutation,Mutation,Layout

  • useInsertionEffect 是在 mutation 阶段执行的,虽然 mutation 是更新 DOM,但是 useInsertionEffect 是 在更新 DOM 之前。

  • useLayoutEffect 是在 layout 阶段执行,此时 DOM 已经更新了。

  • useEffect 是在浏览器绘制之后异步执行的

  • 真实 DOM 的改变确实是在 mutation 阶段执行的, 在 mutation 前后的两次打印,可以看出打印颜色的变化

更新标志

还记得上一篇 # 《深入浅出react》总结之 10.2 渲染阶段流程探秘-completeWork 我们提到的 subtreeFlags, subtreeFlags 用来标记子树中的副作用操作

这些 subtreeFlags 可不是随便打的,要标记是删除还是更新或者插入等操作,react会给不同的副作用subtreeFlags分门别类,怎么分类呢?刚才我们看到各种副作用操作 分别在 commit 不同阶段触发,所以 就有下面的分类:

js 复制代码
/* Before Mutation 阶段标志*
/var BeforeMutationMask = Update | Snapshot
/* Mutation 阶段标志*
/var MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Visibility;
/* Layout 阶段标志 */
var LayoutMask = Update |Callback |Ref |Visibility;
/* useEffect 阶段标志 */
var PassiveMask = Passive | ChildDeletion;

subtreeFlags 打上什么标记,就在什么阶段触发

beforeMutation 阶段

上代码

js 复制代码
function commitBeforeMutationEffects_begin() {
 while (nextEffect ! = = null) {
     var fiber = nextEffect;
     var child = fiber.child;
     if ((fiber.subtreeFlags & BeforeMutationMask) ! = = NoFlags && child ! = = null) {
         /* 这里如果子代 fiber 树有 Before Mutation 的标志,那么把 nextEffect 赋值给子代 fiber */
             nextEffect = child;
         } else {
             /* 找到最底层有 Before Mutation 标志的 fiber,执行 complete */
             commitBeforeMutationEffects_complete();
         }
     }
}

commitBeforeMutationEffects_begin 流程主要做了两件事,

  • 如果子代 fiber 树有 Before Mutation 的标志,那么把nextEffect 赋值给子代 fiber。这里可以理解 begin 会向下递归,找到最底部并且有此标志的 fiber。
  • 找到最底层有 Before Mutation 标志的 fiber,执行 complete。
js 复制代码
function commitBeforeMutationEffects_complete(){
 while (nextEffect ! = = null) {
     var fiber = nextEffect;
     try{
         /* 真正的处理 Before Mutation 需要做的事情。*/
         commitBeforeMutationEffectsOnFiber(fiber);
     }
     /* 优先处理兄弟节点上的 Before Mutation */
     var sibling = fiber.sibling;
     if (sibling ! = = null) {
         nextEffect = sibling;
         return;
     }
      /* 如果没有兄弟节点,那么返回父级节点,继续进行如上流程。*/
     nextEffect = fiber.return;
 }
}

complete 的流程是向上归并的流程,

  • 首先会执行 commitBeforeMutationEffectsOnFiber 真正处理Before Mutation 需要做的事情。
  • 在向上归并的过程中,会先处理兄弟节点上的 Before Mutation,如果没有兄弟节点,那么返回父 级节点,继续进行如上流程

最后再来看看 commitBeforeMutationEffectsOnFiber

js 复制代码
function commitBeforeMutationEffectsOnFiber(){
 if ((flags & Snapshot) ! = = NoFlags) { /* 如果有 Snapshot 标志 */
     switch (finishedWork.tag) {
     case ClassComponent:
         var snapshot = instance.getSnapshotBeforeUpdate(finishedWork.elementType = = =
        finishedWork.type ? prevProps : resolveDefaultProps(finishedWork.type, prevProps), prevState);
         instance.__reactInternalSnapshotBeforeUpdate = snapshot;
     }
 }
}

commitBeforeMutationEffectsOnFiber就是 执行 Before Mutation 阶段标志的副作用函数: Snapshot

看完这个整遍历体流程,有没有感觉很像 performUnitOfWork过程(beginWork 递归到 最底层fiber,completeWork 一路向上) 我可以说真的太像了,所以都叫 begin 和 complete, 只不过 commitBeforeMutationEffects_begin不会做其他工作,只是向下遍历

mutation 阶段

到了 mutation,其实Mutation begin 做的事情,除了和 BeforeMutation 一样,找到最底层有 Mutation 标志的 fiber,执行 complete 外,

还有一件事情就是通过调用 commitDeletion 来执行删除元素操作, 并执行 componentWillUnmount,useLayoutEffect 的 destroy函数(从 父 到 子)

而 Mutation complete阶段呢?

会根据不同的 subTreeFlags 标记,处理下面的情况:

var MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Visibility;

ContentReset: 重置节点内容

Ref:重置alternate属性上的ref

Placement:插入新节点

并且会执行 :useInsertionEffect

在 effect 的执行特点上,所有的 effect hooks 会先执行上一次的 destroy 函数,然后调用本次的,create 函数,比如在 effect 里面绑定事件监听器,如果绑定新的监听器,需要先解绑老的监听器。

layout 阶段

这里不再赘述了

我们直接上commit 整体流程图吧

回到开头

所以我们再看一下开头就会明白,为什么是从子到父 触发这些 effect?

因为 这些 effect 都是在 各个commit complete阶段 向上遍历时触发的

从 设计理念 上也说的通:

1. DOM 操作安全性

  • 父组件操作可能依赖子组件的 DOM 结构(如计算位置/尺寸)
  • 子组件先完成 DOM 更新,父组件才能安全访问子组件的 DOM
js 复制代码
// 父组件在 useLayoutEffect 中访问子元素
useLayoutEffect(() => {
  // 子元素必须已插入 DOM 才能正确获取
  const childHeight = document.getElementById("child").offsetHeight; 
}, []);

2. Effect 依赖链保证

  • 子组件的副作用可能影响父组件状态
  • 子组件优先执行可确保父组件获得最新数据
js 复制代码
// 子组件
useLayoutEffect(() => {
  updateParentData(data); // 先更新父组件依赖的数据
}, [data]);

// 父组件
useLayoutEffect(() => {
  console.log("父组件使用子组件更新的数据"); 
}, [parentData]);
相关推荐
李明卫杭州1 分钟前
前端实现多标签页通讯
前端·javascript
前端领航者1 分钟前
国际化LTR&RTL布局实战
前端·css
FanetheDivine3 分钟前
解决@ant-design/icons导致的样式异常
react.js·ant design
贝加尔湖Pan4 分钟前
图片预加载和懒加载
前端
在钱塘江6 分钟前
《你不知道的JavaScript-上卷》第二部分-this和对象原型-笔记-6-行为委托
前端·javascript
Point6 分钟前
[ahooks] useControllableValue源码阅读
前端·javascript
独立开阀者_FwtCoder14 分钟前
踩坑无数后,我终于总结出这份最全的 Vue3 组件通信实战指南
前端·javascript·vue.js
天天扭码14 分钟前
很全面的前端面试题——CSS篇(下)
前端·css·面试
然我43 分钟前
react-router-dom 完全指南:从零实现动态路由与嵌套布局
前端·react.js·面试
一_个前端1 小时前
Vite项目中SVG同步转换成Image对象
前端