likes
comments
collection
share

【React 18.2 源码学习】万字超详解 commit 流程

作者站长头像
站长
· 阅读数 40

经过前面的 render 流程,得到了一棵被标记完 flag (副作用)的 fiber 树,接下来会把这棵树交给 Renderer 处理,也就是 commit 阶段,在这个阶段会根据 flag 将对应的更新提交到页面上。

commit 流程

commit 阶段的工作可以分为三个子阶段:

  • beforeMuation(操作DOM之前的操作,比如类组件的 getSnapshotBeforeUpdate)
  • mutation (主要是对 DOM 的操作:更新、移动、新增)
  • layoutMuation(操作 DOM 之后的操作,比如useLayoutEffect) 下面是 commit 主要的流程 【React 18.2 源码学习】万字超详解 commit 流程 首先会判断有没有 passive 相关的副作用(也就是 useEffect 的回调),如果有就将执行 useEffect 的函数放入调度器中,后续执行。接下来判断是否有副作用,如果有就进入各个子阶段的执行,当 mutation 阶段对 DOM的操作执行结束之后,新老 fiber 树进行切换,然后进入 layoutMutation 阶段执行相关处理(componentDidUdate、useLayoutEffect)等。如果经过判断没有副作用,则直接进行新老 fiber 树的切换,接下来就是调度下一次的任务了——— 执行 useEffect 的回调。

为什么在 mutation 之后切换新老 fiber 树

个人觉得: 1、mutation 的时候页面还是老节点的、mutation之后页面更新。 2、mutation 阶段会执行 componentWillUnmount,可能会使用老的 fiber 相关的东西 3、layoutMutation 执行对应的副作用时需要能拿到对应的新节点

结合 commit 阶段源码看看生命周期和 effect 怎么执行的

function Fn(props) {
  useEffect(()=>{
    console.log('执行effect');
    return ()=>{
      console.log('卸载 effect');
    }
  })

  useLayoutEffect(()=>{
    console.log('执行LayoutEffect');
    return ()=>{
      console.log('卸载 LayoutEffect');
    }
  })

  return <div className="App">
    函数组件{props.num}
  </div>
}

class Lei extends Component {
  constructor(props) {
    super(props)
    console.log('constructor');
    this.state = {
      a:1
    }
  }

  static getDerivedStateFromProps(...arr) {
    console.log('getDerivedStateFromProps', arr);
    return null
  }

  componentDidMount(...arr) {
    console.log('componentDidMount',arr);
  }

  componentDidUpdate(...arr) {
    console.log('componentDidUpdate', arr);
  }

  shouldComponentUpdate(...arr) {
    console.log('shouldComponentUpdate', arr);
    return true
  }

  getSnapshotBeforeUpdate(...arr) {
    console.log('getSnapshotBeforeUpdate', arr);
    return 999
  }

  componentWillUnmount(...arr) {
    console.log('componentWillUnmount', arr);
  }

  render() {
  return ( <div>
      类组件{this.props.num}
    </div>)
  }
}

function App() {
  const [num, setNum] = useState(1);

  const clickHandler = () =>{
    setNum(num + 1);
  }

  return (
    <div className="App">
      <div onClick={clickHandler}>
        更新
      </div>
      <Fn num={num} />
      <Lei num={num} />
    </div>
  );
}

以上面这段代码为例,我们结合源码(commit 的代码在 commitRootImpl 函数中)分别从更新和初始化渲染来看看 commit 执行的流程以及对应 log 的日志。

初始化渲染过程

【React 18.2 源码学习】万字超详解 commit 流程 这一页的代码主要是对一些特殊情况的校验,处理优先级,标记 Root 任务完成。,接着往下走

【React 18.2 源码学习】万字超详解 commit 流程 这一段的代码是判断有没有 passive 相关的副作用,如果有就将执行 useEffect 的回调的函数(flushPassiveEffects)放入调度器中调度,下个任务执行。接着往下

【React 18.2 源码学习】万字超详解 commit 流程 上面这段代码中,subtreeHasEffects 是用来判断子树有没有副作用要处理,rootHasEffect 表示根节点有没有需要执行的副作用,对于当前的例子两个值都为 true,因此进入副作用处理环节。

【React 18.2 源码学习】万字超详解 commit 流程

【React 18.2 源码学习】万字超详解 commit 流程 可以看到初始化渲染时,在 beforeMutation 阶段完成后并没有日志输出(此阶段对应commitBeforeMutationEffects 方法),也就是在初始化时这个阶段并没有做太多事情,页面这个时候也是空白,接着往下走

【React 18.2 源码学习】万字超详解 commit 流程

【React 18.2 源码学习】万字超详解 commit 流程

mutation 阶段(对应方法 commitMutationEffects )执行完成后也没有日志输出,但是在这个时候已经完成了对 DOM 的操作,页面已经更新,说明在这个函数进行的 DOM 操作。

【React 18.2 源码学习】万字超详解 commit 流程 切换当前 fiber 树为更新之后的

【React 18.2 源码学习】万字超详解 commit 流程 可以看到 layoutMutation 阶段(对应 commitLayoutEffects 方法)执行后,出现了日志:layoutEffect 以及 componentDidMount。说明初始化的时候在这个阶段会执行类组件的 componentDidMount 生命周期方法,以及函数组件的 useLayoutEffect 的回调函数,同时说明 useLayoutEffect 可以等价于类组件的 componentDidMount。

然后剩下的代码主要是和调度相关的,剩下的代码执行完成后,会进入下轮调度,此时调度的任务就是前面执行 useEffect 回调的方法(调度相关的东西后面专门写一篇)。最终会执行下面这个

【React 18.2 源码学习】万字超详解 commit 流程 执行完成后控制台输出 effect

【React 18.2 源码学习】万字超详解 commit 流程 到这整个初次渲染的过程就结束了。

更新过程

更新的执行过程和初次渲染差不多,只不过各个阶段内部处理不太相同,所以这里我们描述的简单一点。

1、还是先判断有没有未执行完的 effect 副作用 2、 3、判断有无副作用,对于我们的例子来说有副作用 4、执行 commitBeforeMutationEffects 方法,此方法执行完成后,控制台输出日志

【React 18.2 源码学习】万字超详解 commit 流程

5、执行 commitMutationEffects 方法,控制台输出如下,页面更新

【React 18.2 源码学习】万字超详解 commit 流程 【React 18.2 源码学习】万字超详解 commit 流程 可以看到还多了个卸载的操作,也就是 layoutEffect 返回的销毁函数,这个函数会在 DOM 更新之后执行。我理解在这个时机执行的原因是:此时 UI 已经更新,以前的 effect 中对应的值已经发生了改变和 UI 已经不匹配了,所以需要销毁。 6、新老 fiber 树切换 7、执行 commitLayoutEffects 方法,控制台输出 执行 LayoutEffect,componentDidUpdate,说明 这两个函数也是在 LayoutMutation 阶段执行 【React 18.2 源码学习】万字超详解 commit 流程 8、接下来 effect 执行和初次渲染有些不一样:如果当前更新更新的优先级是同步的,则立即执行,如果不是同步则和初次渲染一致 【React 18.2 源码学习】万字超详解 commit 流程 执行完成后日志输出如下: 【React 18.2 源码学习】万字超详解 commit 流程

各个阶段是怎么做的

下面我们从各个阶段的源码来看看 commit 的三个子阶段具体怎么做的

beforeMutation

这个阶段和前面 render 阶段的 “递” “归” 的过程类似,通过 DFS 的形式遍历 fiber 树,遍历到符合条件的节点时开始执行对应的操作,下面是具体的流程图,过程也比较简单。 【React 18.2 源码学习】万字超详解 commit 流程 接下来看看具体的代码

【React 18.2 源码学习】万字超详解 commit 流程 下面看看 beforeMutation 具体的处理逻辑

【React 18.2 源码学习】万字超详解 commit 流程 可以看到,在这个阶段只有类组件和根节点有对应的处理(有对应副作用执行的情况下)。类组件是执行 getSnapshotBeforeUpdate 方法、根节点是执行清空的逻辑(目前来初始化渲染的时候会执行清空根节点的操作,其余时候不会)

mutation

mutation 阶段也是通过 DFS 的形式遍历 fiber 树,和前面不同的是,前面我们遇到的 DFS 是通过迭代来实现的,而这个阶段是通过递归来实现的。以下面的代码来看看流程:

function Index() { 
    return <p>函数组件 <span>123</span> </p> 
} 
function App() { 
    return ( 
    <div className="App">
        <h1>子节点</h1> 
        <Index /> 
    </div> ) 
}

【React 18.2 源码学习】万字超详解 commit 流程

  • delete 处理需要删除的节点
  • placement 处理新增或是移动的节点
  • update 处理节点更新

在 mutation 阶段每个节点都会走一遍删除更新及新增的流程(是否执行对应逻辑需要根据 flag 判断)。如图中一样:会从根节点开始深度遍历的处理需要删除的节点,一直到叶子节点开始处理更新和新增移动(此时叶子节点处理完成)。然后处理这个处理完成的叶子节点的更新和新增移动一直到根节点。如果叶子节点有兄弟节点则从兄弟节点开始执行流程(和从根节点开始类型,可以把这个兄弟节点类比为一个新的根节点开始处理 mutation 流程)

递归实现

【React 18.2 源码学习】万字超详解 commit 流程 通过 recursivelyTraverseMutationEffects 、 commitMutationEffectsOnFiber 方法进行递归处理。 在 commitMutationEffectsOnFiber 中会根据不同的元素类型进行不同的操作(以常见的类组件和函数组件原生DOM为例):

函数组件

执行 useInsertionEffect 的销毁函数和回调函数,以及 useLayoutEffect 的销毁函数。这也是前面 useLayoutEffect 的销毁函数在 mutation 阶段执行的原因。

【React 18.2 源码学习】万字超详解 commit 流程

类组件

对 ref 进行处理,在 safelyDetachRef 函数中把 ref 的值重置为 null。 【React 18.2 源码学习】万字超详解 commit 流程

原生DOM(HostComponent)

和类组件一样,也会重置 ref 【React 18.2 源码学习】万字超详解 commit 流程 然后是对 DOM 的操作 【React 18.2 源码学习】万字超详解 commit 流程

删除操作

对应代码在 commitDeletionEffects、commitDeletionEffectsOnFiber 中。对应不同类型也是有不同的操作,我们还是以类组件和函数组件、原生 DOM 为例来看看。 【React 18.2 源码学习】万字超详解 commit 流程 注释在代码中了,看下面的截图 【React 18.2 源码学习】万字超详解 commit 流程 需要注意的是:DOM元素(HostComponent) 和 文本节点(HostText)的操作只差了一个对 ref 重置清空的过程。因此 DOM 元素和文本都会走到文本的处理逻辑中,对 DOM 的移除也在这个逻辑中。

新增节点

【React 18.2 源码学习】万字超详解 commit 流程

更新 DOM 节点

这里主要是对 DOM 的操作,以及对 DOM 属性的变更 【React 18.2 源码学习】万字超详解 commit 流程 【React 18.2 源码学习】万字超详解 commit 流程 【React 18.2 源码学习】万字超详解 commit 流程 【React 18.2 源码学习】万字超详解 commit 流程

新旧 fiber 树切换

【React 18.2 源码学习】万字超详解 commit 流程

layoutMutation

layoutMutation 阶段和 beforeMutation 阶段流程一样,这里我们直接来看具体的执行逻辑(还是以类组件和函数组件、原生 DOM 为例)

类组件

【React 18.2 源码学习】万字超详解 commit 流程 这个 commitUpdateQueue 函数在这是干啥的我也不是很清楚,有知道的麻烦指点一下

函数组件

【React 18.2 源码学习】万字超详解 commit 流程

原生 DOM

【React 18.2 源码学习】万字超详解 commit 流程

总结

上面总结了 commit 阶段的详细执行流程。我们从源码的层次知道了各个 Effect 和 commit 阶段生命周期的执行时机。以及 useLayoutEffect 和 componentDidUpdate 以及 componentDidMount 等价。函数组件的 effect 每次执行之前都会先执行销毁函数然后再执行回调。 最后,感谢大家的阅读,有不对的地方也欢迎大家指出来。

参考资料

React设计原理-卡颂 React18.2.0源码

转载自:https://juejin.cn/post/7243748398567161912
评论
请登录