终于到了执行DOM操作的mutation阶段。
概览
类似before mutation阶段
,mutation阶段
也是遍历effectList
,执行函数。这里执行的是commitMutationEffects
。
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
commitMutationEffects
你可以在这里看到
commitMutationEffects
源码
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍历effectList
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 根据 ContentReset effectTag重置文字节点
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 更新ref
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 根据 effectTag 分别处理
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
// 插入DOM 并 更新DOM
case PlacementAndUpdate: {
// 插入
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 更新
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// SSR
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
// SSR
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新DOM
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除DOM
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitMutationEffects
会遍历effectList
,对每个Fiber节点
执行如下三个操作:
- 根据
ContentReset effectTag
重置文字节点 - 更新
ref
- 根据
effectTag
分别处理,其中effectTag
包括(Placement
|Update
|Deletion
|Hydrating
)
我们关注步骤三中的Placement
| Update
| Deletion
。Hydrating
作为服务端渲染相关,我们先不关注。
Placement effect
当Fiber节点
含有Placement effectTag
,意味着该Fiber节点
对应的DOM节点
需要插入到页面中。
调用的方法为commitPlacement
。
你可以在这里看到
commitPlacement
源码
该方法所做的工作分为三步:
- 获取父级
DOM节点
。其中finishedWork
为传入的Fiber节点
。
const parentFiber = getHostParentFiber(finishedWork);
// 父级DOM节点
const parentStateNode = parentFiber.stateNode;
- 获取
Fiber节点
的DOM
兄弟节点
const before = getHostSibling(finishedWork);
- 根据
DOM
兄弟节点是否存在决定调用parentNode.insertBefore
或parentNode.appendChild
执行DOM
插入操作。
// parentStateNode是否是rootFiber
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
值得注意的是,getHostSibling
(获取兄弟DOM节点
)的执行很耗时,当在同一个父Fiber节点
下依次执行多个插入操作,getHostSibling
算法的复杂度为指数级。
这是由于Fiber节点
不只包括HostComponent
,所以Fiber树
和渲染的DOM树
节点并不是一一对应的。要从Fiber节点
找到DOM节点
很可能跨层级遍历。
考虑如下例子:
function Item() {
return <li><li>;
}
function App() {
return (
<div>
<Item/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
对应的Fiber树
和DOM树
结构为:
// Fiber树
child child child child
rootFiber -----> App -----> div -----> Item -----> li
// DOM树
#root ---> div ---> li
当在div
的子节点Item
前插入一个新节点p
,即App
变为:
function App() {
return (
<div>
<p></p>
<Item/>
</div>
)
}
对应的Fiber树
和DOM树
结构为:
// Fiber树
child child child
rootFiber -----> App -----> div -----> p
| sibling child
| -------> Item -----> li
// DOM树
#root ---> div ---> p
|
---> li
此时DOM节点
p
的兄弟节点为li
,而Fiber节点
p
对应的兄弟DOM节点
为:
fiberP.sibling.child
即fiber p
的兄弟fiber
Item
的子fiber
li
Update effect
当Fiber节点
含有Update effectTag
,意味着该Fiber节点
需要更新。调用的方法为commitWork
,他会根据Fiber.tag
分别处理。
你可以在这里看到
commitWork
源码
这里我们主要关注FunctionComponent
和HostComponent
。
FunctionComponent mutation
当fiber.tag
为FunctionComponent
,会调用commitHookEffectListUnmount
。该方法会遍历effectList
,执行所有useLayoutEffect hook
的销毁函数。
你可以在这里看到
commitHookEffectListUnmount
源码
所谓“销毁函数”,见如下例子:
useLayoutEffect(() => {
// ...一些副作用逻辑
return () => {
// ...这就是销毁函数
}
})
你不需要很了解useLayoutEffect
,我们会在下一节详细介绍。你只需要知道在mutation阶段
会执行useLayoutEffect
的销毁函数。
HostComponent mutation
当fiber.tag
为HostComponent
,会调用commitUpdate
。
你可以在这里看到
commitUpdate
源码
最终会在updateDOMProperties
中将render阶段 completeWork
中为Fiber节点
赋值的updateQueue
对应的内容渲染在页面上。
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
// 处理 style
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
// 处理 DANGEROUSLY_SET_INNER_HTML
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
// 处理 children
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
// 处理剩余 props
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
Deletion effect
当Fiber节点
含有Deletion effectTag
,意味着该Fiber节点
对应的DOM节点
需要从页面中删除。调用的方法为commitDeletion
。
你可以在这里看到
commitDeletion
源码
该方法会执行如下操作:
- 递归调用
Fiber节点
及其子孙Fiber节点
中fiber.tag
为ClassComponent
的componentWillUnmount
生命周期钩子,从页面移除Fiber节点
对应DOM节点
- 解绑
ref
- 调度
useEffect
的销毁函数
总结
从这节我们学到,mutation阶段
会遍历effectList
,依次执行commitMutationEffects
。该方法的主要工作为“根据effectTag
调用不同的处理函数处理Fiber
。
下一节:该阶段之所以称为layout,因为该阶段的代码都是在DOM渲染完成(mutation阶段完成)后执行的。
该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段是可以参与DOM layout的阶段。