前言
我们之前render篇,我们有比较简略的对commitPhase环节进行分析。这里在它的基础上对Update的commitPhase进行分析。它们实际上是同一个环节。
但是这里随着对Update环节的认知深入,所以这里commit环节也可以进一步深入了解。
总之,这里是承了之前的render篇上一篇update篇来对commit进行深入的分析。他不仅仅像是标题提到的Update环节下的commit,实质上也是render环节下的commit。
这里会牵扯到Diff算法。我们也一并看看,不再像前面那边简单略过。
调用栈确认
之前提到update和render里面的commit环节是同一个。但是这里依然要做一个确认。
这里首先当然是Render环节下的调用栈。
回顾一下render篇,performWorkOnRoot函数里面renderRoot执行之后会执行completeRoot。这里调用栈就是之前提到的:1
2
3
4
5
6
7completeRoot
-> commitRoot () {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}Update之setState部分
这里在之前「略过的Event」小结里面提到了
performWorkOnRoot -> renderRoot & completeRoot
。所以整体和前面一致Update之useState部分
这里在文章最尾部的「commit」小结里面有明显分析。最终也是completeRoot函数。
所以这里可以明确一下,它们都commitPhase都是走的completeRoot。
细节
实际上,必须提前说一下,completeRoot和renderRoot在workLoop过程中,往往是二选一的调用。如果root.finishedWork为空执行renderRoot,否则执行completeRoot。
1 | completeRoot |
completeRoot
1 | function completeRoot( |
这里细节不算多。
completedBatches作为Array<Batch>
数组。它主要是保存root.firstBatch
,如果batch有阻止,这个保存也依然会进入数组保存起来。当render完毕,finishRendering函数后面会清空它的同时,遍历Batch上的_onComplete函数并执行。
lastCommittedRootDuringThisBatch变量用来缓存每次执行completeRoot传入的root。以便后面的scheduleWork出错捕获。
Tips:
lastCommittedRootDuringThisBatch会被finishRendering函数清空为null。
另外scheduleWork这里说到捕获,是因为在ReactNative中可能存在多个fiberRoot,而ReactDom中只有一个。
nestedUpdateCount变量用来标记相同root更新次数,如果超出50次,那么很有可能就是出现了代码错误,导致组件无限更新了——这个在scheduleWork函数中,当然,只是警告,同时它还是会将其清空为0。
接下来是runWithPriority
函数这块,这块是其核心中的核心。看看源代码:
1 | function unstable_runWithPriority(priorityLevel, eventHandler) { |
这个函数主要是设定currentPriorityLevel && currentEventStartTime,然后执行eventHandler(),完毕之后将这两个值还原。并执行flushImmediateWork()
。
在这里eventHandler === () => (commitRoot(root, finishedWork))
。我们先继续这个路线。
commitRoot
这里代码有点长,做一些精简。
1 | function commitRoot(root: FiberRoot, finishedWork: Fiber): void { |
这里对宏观结构做一些总结, 细节一些的地方可以参见注释,为了方便理解根据自己理解给翻译为中文了。
1 | function commitRoot(root: FiberRoot, finishedWork: Fiber): void { |
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate是新增的生命周期函数。它发生在componentWillUpdate、componentDidUpdate之前,它的触发时机是 React 进行修改前(通常是更新 DOM)的“瞬间”,它的返回值会作为第三个参数传入 componentDidUpdate
。这个值会被挂在instance.__reactInternalSnapshotBeforeUpdate
上,当实例注销时候也会注销。
它由commitBeforeMutationLifecycles函数引用。
1 | function commitBeforeMutationLifecycles() { |
这里针对FunctionComponent有个单独的分支。主要操作是执行effect.create() || effect.destroy();代码这里逻辑是进行destroy()。更全面点讲,这里遍历finishedWork.updateQueue链表,执行了上面的每个effect.destroy()。
commitAllHostEffects
这个函数名称真的足够简练,没有一个字是多余的。。。
All这里提现在对effect的遍历上,Host则提现在对HostComponent的处理上。
1 | function commitAllHostEffects() { |
这里有4个case,但是实质上只有3种操作,这三种操作分别是替换、更新、删除。多出来的PlacementAndUpdate实质上是替换+更新的组合。
另外,注意这里的遍历,它遍历了所有的nextEffect。
再次,nextEffect这个链表的构建,在completeUnitOfWork函数里面。
commitPlacement - 替换
1 | function commitPlacement(finishedWork: Fiber): void { |
位与运算符前端不太好理解,不过这里也就懒得说里面的过程。ContentReset = 0b000000010000 =16
。当运行parentFiber.effectTag & ContentReset
时候,明显可见的规律是,当effectTag除以ContentReset结果取整进行Math.floor操作,如果这个值是奇数,它的值是16,否则为0。
而parentFiber.effectTag &= ~ContentReset在这里基本等同于parentFiber.effectTag -= ContentReset
。不过它和减法还是有很大区别。它对0~15不会执行减法——但是0~15显然进不来这个分支。
其他的不妨看看注释,这里主线是对finishWork:fiber进行遍历,核心操作是执行insertBefore或者appendChild操作。
commitWork - 更新
1 | function commitWork(current: Fiber | null, finishedWork: Fiber): void { |
这里加了若干注释,并对干扰代码进行了删除。可以看到,这里核心的处理分支是HostComponent,这也比较符合之前的认知。这里猜测可以进行部分Diff对比了: 先对比instance,然后直接进行HostComponent替换(ComponentDiff)或者子节点的ElementDiff操作。核心是commitUpdate。
1 | export function commitUpdate( |
主要的逻辑已经给出了。这里看看重点的updateDOMProperties函数。
1 | function updateDOMProperties( |
这个函数对比v15.6版本又做了封装。但是不得不说清爽太多了。但是总体的逻辑倒也没什么变化。主要是区分了样式、Text、Property进行处理。另外就是特例API处理dangerouslySetInnerHTML
。
这里最复杂的应该是setValueForProperty:
1 | export function setValueForProperty( |
这里分支多。但是说白了,其实最后也就是setAttribute & removeAttribute
这套。
总结一下就是: 它真的只是apply the diff。而不包含Diff计算了。
commitDeletion-删除
1 | function commitDeletion(current: Fiber): void { |
这里有个commitUnmount函数。得讲一讲。它是commitNestedUnmounts的核心,整个umount环节,在粒度上,它应该是最小的情况了。
commitNestedUnmounts主要是对current节点进行下层级递归调用commitUnmount。
而commitUnmount主要是对ref进行清除、递归调用componentWillUnmount、或者是useEffect的清除。特例情况是HostPortal则需要进步遍历下级节点然后递归调用自身。
1 | function commitUnmount(current: Fiber): void { |
然后就是unmountHostComponents函数。其他函数都说了,这个最后还是得看看。
1 | function unmountHostComponents(current): void { |
commitAllLifeCycles
1 | function commitAllLifeCycles( |
这里commitLifeCycles函数内容比较多。但是核心的地方还是针对几个类型的组件进行分支操作
- 正常的ClassComponent:执行componentDidMount && componentDidUpdate声明周期勾子函数。调用commitUpdateQueue
- FunctionComponent:commitHookEffectList
- HostComponent:commitMount
- HostRoot:commitUpdateQueue
这里commitUpdateQueue遍历UpdateQueue链表,执行每个sideEffect上的effect.callback.bind(instance)
commitMount函数主要是处理焦点获取。没有什么其他操作
commitHookEffectList之前遇到了很多,也提到过一点,这里小改一下: 主要操作是执行effect.create() || effect.destroy();代码根据参数来说这里逻辑晾着都有。更全面点讲,这里遍历finishedWork.updateQueue链表,执行了上面的每个effect.create() & effect.destroy()。当然这里主要还是看effect.tag值来决定。关于这个计算暂时还没搞懂,后面再来补充。