【JS】React源码 commit阶段详解

React源码 commit阶段详解

nero发布于 今天 13:38

点击进入React源码调试仓库。

render阶段完成后,意味着在内存中构建的workInProgress树所有更新工作已经完成,这包括树中fiber节点的更新、diff、effectTag的标记、effectList的收集。此时workInProgress树的完整形态如下:

【JS】React源码  commit阶段详解

和current树相比,它们的结构上固然存在区别,变化的fiber节点也存在于workInProgress树,但要将这些节点应用到DOM上却不会循环整棵树,而是通过循环effectList这个链表来实现,这样保证了只针对有变化的节点做工作。

所以循环effectList链表去将有更新的fiber节点应用到页面上是commit阶段的主要工作。

commit阶段的入口是commitRoot函数,它会告知scheduler以立即执行的优先级去调度commit阶段的工作。

function commitRoot(root) {

const renderPriorityLevel = getCurrentPriorityLevel();

runWithPriority(

ImmediateSchedulerPriority,

commitRootImpl.bind(null, root, renderPriorityLevel),

);

return null;

}

scheduler去调度的是commitRootImpl,它是commit阶段的核心实现,整个commit阶段被划分成三个部分。

commit流程概览

commit阶段主要是针对root上收集的effectList进行处理。在真正的工作开始之前,有一个准备阶段,主要是变量的赋值,以及将root的effect加入到effectList中。随后开始针对effectList分三个阶段进行工作:

  • before mutation:读取组件变更前的状态,针对类组件,调用getSnapshotBeforeUpdate,让我们可以在DOM变更前获取组件实例的信息;针对函数组件,异步调度useEffect。
  • mutation:针对HostComponent,进行相应的DOM操作;针对类组件,调用componentWillUnmount;针对函数组件,执行useLayoutEffect的销毁函数。
  • layout:在DOM操作完成后,读取组件的状态,针对类组件,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对函数组件填充useEffect 的 effect执行数组,并调度useEffect

before mutation和layout针对函数组件的useEffect调度是互斥的,只能发起一次调度

workInProgress 树切换到current树的时机是在mutation结束后,layout开始前。这样做的原因是在mutation阶段调用类组件的componentWillUnmount的时候,
还可以获取到卸载前的组件信息;在layout阶段调用componentDidMount/Update时,获取的组件信息更新后的。

function commitRootImpl(root, renderPriorityLevel) {

// 进入commit阶段,先执行一次之前未执行的useEffect

do {

flushPassiveEffects();

} while (rootWithPendingPassiveEffects !== null);

// 准备阶段-----------------------------------------------

const finishedWork = root.finishedWork;

const lanes = root.finishedLanes;

if (finishedWork === null) {

return null;

}

root.finishedWork = null;

root.finishedLanes = NoLanes;

root.callbackNode = null;

root.callbackId = NoLanes;

// effectList的整理,将root上的effect连到effectList的末尾

let firstEffect;

if (finishedWork.effectTag > PerformedWork) {

if (finishedWork.lastEffect !== null) {

finishedWork.lastEffect.nextEffect = finishedWork;

firstEffect = finishedWork.firstEffect;

} else {

firstEffect = finishedWork;

}

} else {

// There is no effect on the root.

firstEffect = finishedWork.firstEffect;

}

// 准备阶段结束,开始处理effectList

if (firstEffect !== null) {

...

// before mutation阶段--------------------------------

nextEffect = firstEffect;

do {...} while (nextEffect !== null);

...

// mutation阶段---------------------------------------

nextEffect = firstEffect;

do {...} while (nextEffect !== null);

// 将wprkInProgress树切换为current树

root.current = finishedWork;

// layout阶段-----------------------------------------

nextEffect = firstEffect;

do {...} while (nextEffect !== null);

nextEffect = null;

// 通知浏览器去绘制

requestPaint();

} else {

// 没有effectList,直接将wprkInProgress树切换为current树

root.current = finishedWork;

}

const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

// 获取尚未处理的优先级,比如之前被跳过的任务的优先级

remainingLanes = root.pendingLanes;

// 将被跳过的优先级放到root上的pendingLanes(待处理的优先级)上

markRootFinished(root, remainingLanes);

/*

* 每次commit阶段完成后,再执行一遍ensureRootIsScheduled,确保是否还有任务需要被调度。

* 例如,高优先级插队的更新完成后,commit完成后,还会再执行一遍,保证之前跳过的低优先级任务

* 重新调度

*

* */

ensureRootIsScheduled(root, now());

...

return null;

}

下面的部分,是对这三个阶段分别进行的详细讲解。

before Mutation

beforeMutation阶段的入口函数是commitBeforeMutationEffects

nextEffect = firstEffect;

do {

try {

commitBeforeMutationEffects();

} catch (error) {

...

}

} while (nextEffect !== null);

它的作用主要是调用类组件的getSnapshotBeforeUpdate,针对函数组件,异步调度useEffect。

function commitBeforeMutationEffects() {

while (nextEffect !== null) {

const current = nextEffect.alternate;

...

const flags = nextEffect.flags;

if ((flags & Snapshot) !== NoFlags) {

// 通过commitBeforeMutationEffectOnFiber调用getSnapshotBeforeUpdate

commitBeforeMutationEffectOnFiber(current, nextEffect);

}

if ((flags & Passive) !== NoFlags) {

// 异步调度useEffect

if (!rootDoesHavePassiveEffects) {

rootDoesHavePassiveEffects = true;

scheduleCallback(NormalSchedulerPriority, () => {

flushPassiveEffects();

return null;

});

}

}

nextEffect = nextEffect.nextEffect;

}

}

commitBeforeMutationEffectOnFiber代码如下

function commitBeforeMutationLifeCycles(

current: Fiber | null,

finishedWork: Fiber,

): void {

switch (finishedWork.tag) {

...

case ClassComponent: {

if (finishedWork.flags & Snapshot) {

if (current !== null) {

const prevProps = current.memoizedProps;

const prevState = current.memoizedState;

const instance = finishedWork.stateNode;

// 调用getSnapshotBeforeUpdate

const snapshot = instance.getSnapshotBeforeUpdate(

finishedWork.elementType === finishedWork.type

? prevProps

: resolveDefaultProps(finishedWork.type, prevProps),

prevState,

);

// 将返回值存储在内部属性上,方便componentDidUpdate获取

instance.__reactInternalSnapshotBeforeUpdate = snapshot;

}

}

return;

}

...

}

}

mutation

mutation阶段会真正操作DOM节点,涉及到的操作有增、删、改。入口函数是commitMutationEffects

    nextEffect = firstEffect;

do {

try {

commitMutationEffects(root, renderPriorityLevel);

} catch (error) {

...

nextEffect = nextEffect.nextEffect;

}

} while (nextEffect !== null);

由于过程较为复杂,所以我写了三篇文章来说明这三种DOM操作,如果想要了解细节,可以看一下。文章写于17还未正式发布的时候,所以里面的源码版本取自17.0.0-alpha0。

React和DOM的那些事-节点新增算法

React和DOM的那些事-节点删除算法

React和DOM的那些事-节点更新

layout阶段

layout阶段的入口函数是commitLayoutEffects

nextEffect = firstEffect;

do {

try {

commitLayoutEffects(root, lanes);

} catch (error) {

...

nextEffect = nextEffect.nextEffect;

}

} while (nextEffect !== null);

我们只关注classComponent和functionComponent。针对前者,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对后者,填充useEffect 的 effect执行数组,并调度useEffect(具体的原理在我的这篇文章:梳理useEffect和useLayoutEffect的原理与区别中有讲解)。

function commitLifeCycles(

finishedRoot: FiberRoot,

current: Fiber | null,

finishedWork: Fiber,

committedLanes: Lanes,

): void {

switch (finishedWork.tag) {

case FunctionComponent:

case ForwardRef:

case SimpleMemoComponent:

case Block: {

// 执行useLayoutEffect的创建

commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

// 填充useEffect的effect执行数组

schedulePassiveEffects(finishedWork);

return;

}

case ClassComponent: {

const instance = finishedWork.stateNode;

if (finishedWork.flags & Update) {

if (current === null) {

// 如果是初始挂载阶段,调用componentDidMount

instance.componentDidMount();

} else {

// 如果是更新阶段,调用componentDidUpdate

const prevProps =

finishedWork.elementType === finishedWork.type

? current.memoizedProps

: resolveDefaultProps(finishedWork.type, current.memoizedProps);

const prevState = current.memoizedState;

instance.componentDidUpdate(

prevProps,

prevState,

// 将getSnapshotBeforeUpdate的结果传入

instance.__reactInternalSnapshotBeforeUpdate,

);

}

}

// 调用setState的回调

const updateQueue: UpdateQueue<

*,

> | null = (finishedWork.updateQueue: any);

if (updateQueue !== null) {

commitUpdateQueue(finishedWork, updateQueue, instance);

}

return;

}

...

}

}

总结

commit阶段将effectList的处理分成三个阶段保证了不同生命周期函数的适时调用。相对于同步执行的useEffectLayout,useEffect的异步调度提供了一种不阻塞页面渲染的副作用操作入口。另外,标记root上还未处理的优先级和调用ensureRootIsScheduled使得被跳过的低优先级任务得以再次被调度。commit阶段的完成,也就意味着本次更新已经结束。

欢迎扫码关注公众号,发现更多技术文章

【JS】React源码  commit阶段详解

javascript前端react.js

阅读 88发布于 今天 13:38

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议


React的秘密

最平实的语言,解读复杂React

avatar

nero

前端工程师

3.6k 声望

1.8k 粉丝

0 条评论

得票时间

avatar

nero

前端工程师

3.6k 声望

1.8k 粉丝

宣传栏

点击进入React源码调试仓库。

当render阶段完成后,意味着在内存中构建的workInProgress树所有更新工作已经完成,这包括树中fiber节点的更新、diff、effectTag的标记、effectList的收集。此时workInProgress树的完整形态如下:

【JS】React源码  commit阶段详解

和current树相比,它们的结构上固然存在区别,变化的fiber节点也存在于workInProgress树,但要将这些节点应用到DOM上却不会循环整棵树,而是通过循环effectList这个链表来实现,这样保证了只针对有变化的节点做工作。

所以循环effectList链表去将有更新的fiber节点应用到页面上是commit阶段的主要工作。

commit阶段的入口是commitRoot函数,它会告知scheduler以立即执行的优先级去调度commit阶段的工作。

function commitRoot(root) {

const renderPriorityLevel = getCurrentPriorityLevel();

runWithPriority(

ImmediateSchedulerPriority,

commitRootImpl.bind(null, root, renderPriorityLevel),

);

return null;

}

scheduler去调度的是commitRootImpl,它是commit阶段的核心实现,整个commit阶段被划分成三个部分。

commit流程概览

commit阶段主要是针对root上收集的effectList进行处理。在真正的工作开始之前,有一个准备阶段,主要是变量的赋值,以及将root的effect加入到effectList中。随后开始针对effectList分三个阶段进行工作:

  • before mutation:读取组件变更前的状态,针对类组件,调用getSnapshotBeforeUpdate,让我们可以在DOM变更前获取组件实例的信息;针对函数组件,异步调度useEffect。
  • mutation:针对HostComponent,进行相应的DOM操作;针对类组件,调用componentWillUnmount;针对函数组件,执行useLayoutEffect的销毁函数。
  • layout:在DOM操作完成后,读取组件的状态,针对类组件,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对函数组件填充useEffect 的 effect执行数组,并调度useEffect

before mutation和layout针对函数组件的useEffect调度是互斥的,只能发起一次调度

workInProgress 树切换到current树的时机是在mutation结束后,layout开始前。这样做的原因是在mutation阶段调用类组件的componentWillUnmount的时候,
还可以获取到卸载前的组件信息;在layout阶段调用componentDidMount/Update时,获取的组件信息更新后的。

function commitRootImpl(root, renderPriorityLevel) {

// 进入commit阶段,先执行一次之前未执行的useEffect

do {

flushPassiveEffects();

} while (rootWithPendingPassiveEffects !== null);

// 准备阶段-----------------------------------------------

const finishedWork = root.finishedWork;

const lanes = root.finishedLanes;

if (finishedWork === null) {

return null;

}

root.finishedWork = null;

root.finishedLanes = NoLanes;

root.callbackNode = null;

root.callbackId = NoLanes;

// effectList的整理,将root上的effect连到effectList的末尾

let firstEffect;

if (finishedWork.effectTag > PerformedWork) {

if (finishedWork.lastEffect !== null) {

finishedWork.lastEffect.nextEffect = finishedWork;

firstEffect = finishedWork.firstEffect;

} else {

firstEffect = finishedWork;

}

} else {

// There is no effect on the root.

firstEffect = finishedWork.firstEffect;

}

// 准备阶段结束,开始处理effectList

if (firstEffect !== null) {

...

// before mutation阶段--------------------------------

nextEffect = firstEffect;

do {...} while (nextEffect !== null);

...

// mutation阶段---------------------------------------

nextEffect = firstEffect;

do {...} while (nextEffect !== null);

// 将wprkInProgress树切换为current树

root.current = finishedWork;

// layout阶段-----------------------------------------

nextEffect = firstEffect;

do {...} while (nextEffect !== null);

nextEffect = null;

// 通知浏览器去绘制

requestPaint();

} else {

// 没有effectList,直接将wprkInProgress树切换为current树

root.current = finishedWork;

}

const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;

// 获取尚未处理的优先级,比如之前被跳过的任务的优先级

remainingLanes = root.pendingLanes;

// 将被跳过的优先级放到root上的pendingLanes(待处理的优先级)上

markRootFinished(root, remainingLanes);

/*

* 每次commit阶段完成后,再执行一遍ensureRootIsScheduled,确保是否还有任务需要被调度。

* 例如,高优先级插队的更新完成后,commit完成后,还会再执行一遍,保证之前跳过的低优先级任务

* 重新调度

*

* */

ensureRootIsScheduled(root, now());

...

return null;

}

下面的部分,是对这三个阶段分别进行的详细讲解。

before Mutation

beforeMutation阶段的入口函数是commitBeforeMutationEffects

nextEffect = firstEffect;

do {

try {

commitBeforeMutationEffects();

} catch (error) {

...

}

} while (nextEffect !== null);

它的作用主要是调用类组件的getSnapshotBeforeUpdate,针对函数组件,异步调度useEffect。

function commitBeforeMutationEffects() {

while (nextEffect !== null) {

const current = nextEffect.alternate;

...

const flags = nextEffect.flags;

if ((flags & Snapshot) !== NoFlags) {

// 通过commitBeforeMutationEffectOnFiber调用getSnapshotBeforeUpdate

commitBeforeMutationEffectOnFiber(current, nextEffect);

}

if ((flags & Passive) !== NoFlags) {

// 异步调度useEffect

if (!rootDoesHavePassiveEffects) {

rootDoesHavePassiveEffects = true;

scheduleCallback(NormalSchedulerPriority, () => {

flushPassiveEffects();

return null;

});

}

}

nextEffect = nextEffect.nextEffect;

}

}

commitBeforeMutationEffectOnFiber代码如下

function commitBeforeMutationLifeCycles(

current: Fiber | null,

finishedWork: Fiber,

): void {

switch (finishedWork.tag) {

...

case ClassComponent: {

if (finishedWork.flags & Snapshot) {

if (current !== null) {

const prevProps = current.memoizedProps;

const prevState = current.memoizedState;

const instance = finishedWork.stateNode;

// 调用getSnapshotBeforeUpdate

const snapshot = instance.getSnapshotBeforeUpdate(

finishedWork.elementType === finishedWork.type

? prevProps

: resolveDefaultProps(finishedWork.type, prevProps),

prevState,

);

// 将返回值存储在内部属性上,方便componentDidUpdate获取

instance.__reactInternalSnapshotBeforeUpdate = snapshot;

}

}

return;

}

...

}

}

mutation

mutation阶段会真正操作DOM节点,涉及到的操作有增、删、改。入口函数是commitMutationEffects

    nextEffect = firstEffect;

do {

try {

commitMutationEffects(root, renderPriorityLevel);

} catch (error) {

...

nextEffect = nextEffect.nextEffect;

}

} while (nextEffect !== null);

由于过程较为复杂,所以我写了三篇文章来说明这三种DOM操作,如果想要了解细节,可以看一下。文章写于17还未正式发布的时候,所以里面的源码版本取自17.0.0-alpha0。

React和DOM的那些事-节点新增算法

React和DOM的那些事-节点删除算法

React和DOM的那些事-节点更新

layout阶段

layout阶段的入口函数是commitLayoutEffects

nextEffect = firstEffect;

do {

try {

commitLayoutEffects(root, lanes);

} catch (error) {

...

nextEffect = nextEffect.nextEffect;

}

} while (nextEffect !== null);

我们只关注classComponent和functionComponent。针对前者,调用生命周期componentDidMount和componentDidUpdate,调用setState的回调;针对后者,填充useEffect 的 effect执行数组,并调度useEffect(具体的原理在我的这篇文章:梳理useEffect和useLayoutEffect的原理与区别中有讲解)。

function commitLifeCycles(

finishedRoot: FiberRoot,

current: Fiber | null,

finishedWork: Fiber,

committedLanes: Lanes,

): void {

switch (finishedWork.tag) {

case FunctionComponent:

case ForwardRef:

case SimpleMemoComponent:

case Block: {

// 执行useLayoutEffect的创建

commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);

// 填充useEffect的effect执行数组

schedulePassiveEffects(finishedWork);

return;

}

case ClassComponent: {

const instance = finishedWork.stateNode;

if (finishedWork.flags & Update) {

if (current === null) {

// 如果是初始挂载阶段,调用componentDidMount

instance.componentDidMount();

} else {

// 如果是更新阶段,调用componentDidUpdate

const prevProps =

finishedWork.elementType === finishedWork.type

? current.memoizedProps

: resolveDefaultProps(finishedWork.type, current.memoizedProps);

const prevState = current.memoizedState;

instance.componentDidUpdate(

prevProps,

prevState,

// 将getSnapshotBeforeUpdate的结果传入

instance.__reactInternalSnapshotBeforeUpdate,

);

}

}

// 调用setState的回调

const updateQueue: UpdateQueue<

*,

> | null = (finishedWork.updateQueue: any);

if (updateQueue !== null) {

commitUpdateQueue(finishedWork, updateQueue, instance);

}

return;

}

...

}

}

总结

commit阶段将effectList的处理分成三个阶段保证了不同生命周期函数的适时调用。相对于同步执行的useEffectLayout,useEffect的异步调度提供了一种不阻塞页面渲染的副作用操作入口。另外,标记root上还未处理的优先级和调用ensureRootIsScheduled使得被跳过的低优先级任务得以再次被调度。commit阶段的完成,也就意味着本次更新已经结束。

欢迎扫码关注公众号,发现更多技术文章

【JS】React源码  commit阶段详解

以上是 【JS】React源码 commit阶段详解 的全部内容, 来源链接: utcz.com/a/108570.html

回到顶部