点击进入React源码调试仓库。
当render阶段实现后,意味着在内存中构建的workInProgress树所有更新工作曾经实现,这包含树中fiber节点的更新、diff、effectTag的标记、effectList的收集。此时workInProgress树的残缺状态如下:
和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阶段的实现,也就意味着本次更新曾经完结。
欢送扫码关注公众号,发现更多技术文章