1.render阶段
内容:Fiber节点是如何被创建并构建成render树的
1.1 流程概览
在render的阶段中,根据是同步还是异步,执行performSyncWorkOnRoot 和 performConcurrentWorkOnRoot
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
区别:是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历
说明:
- workInProgress 代表当前已创建的wordInProgress fiber;
- performUnitOfWork 方法会创建下一个 Fiber 节点并赋值给 workInProgress,并将 workInProgress 与已创建的Fiber节点连接起来构成Fiber树;
虽然fiber reconciler是从stack reconciler重构而来,但都是通过遍历的方式实现可中断的异步递归。
- 递
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用 beginWork;
- 该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来;
- 当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段;
- 归
在“归”阶段会调用 completeWork 处理Fiber节点。
- 当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段;
- 如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段;
“递”和“归”阶段会交错执行直到“归”到rootFiber
function App() {
return (
<div>
i am
<span>text</span>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork
// 没有叶子节点是因为React针对只有单一文本子节点的Fiber节点做了性能优化
// 递归的格式
function performUnitOfWork(fiber) {
// 执行beginWork
if (fiber.child) {
performUnitOfWork(fiber.child);
}
// 执行completeWork
if (fiber.sibling) {
performUnitOfWork(fiber.sibling);
}
}
1.2 beginWork
beginWork的工作是传入当前Fiber节点,创建子Fiber节点
入参
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
- current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
- workInProgress:当前组件对应的Fiber节点
- renderLanes:优先级相关
可以根据current !== null,判断组件时mount还是update
- mount:首次渲染,当前组件的fiber节点为null;
- update:之前已经mount,fiber节点不为null;
基于此原因,beginWork的工作可以分为两部分:
- update时:如果current存在,在满足一定条件时可以复用current节点,(diff)这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child;
- mount时:除fiberRootNode以外,current === null。会根据fiber.tag 不同,创建不同类型的子Fiber节点;
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
// ...省略
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
- update时
-
- didReceiveUpdate = false:不需要新建fiber,可以直接复用
- !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
// 省略处理
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
- mount时
- 根据fiber.tag不同,进入不同逻辑的fiber创建
- 官网地址:github.com/facebook/re…
- 对于常见的组件(FunctionComponent、ClassComponent),会执行reconcileChildren
// mount时:根据tag不同,创建不同的Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
- reconcileChildren
- mount组件:创建新的子Fiber节点;
- update组件:将当前组件与该组件在上次更新时对应的Fiber节点比较(Diff),将比较的结果生成新Fiber节点;
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
- reconcileChildren
- mount组件:创建新的子Fiber节点;
- update组件:将当前组件与该组件在上次更新时对应的Fiber节点比较(Diff),将比较的结果生成新Fiber节点;
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
- mountChildFibers & reconcileChildFibers
- 都会生成新的fiber节点返回给workInProgress.child,作为本次beginWork的返回值,在下次performUnitOfWork执行时workInProgress的入参
- effectTag
-
render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中
-
1.3 completeWork
- 作用:针对不同的fiber.tag调用不同的处理逻辑
- 地址:github.com/facebook/re…
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
return null;
}
// ...省略
- 判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update的情况
// ...省略
} else {
// mount的情况
// ...省略
}
return null;
}
- update
当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:
- onClick、onChange等回调函数的注册
- 处理style prop
- 处理DANGEROUSLY_SET_INNER_HTML prop
- 处理children prop
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
- updateHostComponent git地址:github.com/facebook/re…
- 在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上,其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value
workInProgress.updateQueue = (updatePayload: any);
- mount时
-
- 为Fiber节点生成对应的DOM节点
- 将子孙DOM节点插入刚生成的DOM节点中
- 与update逻辑中的updateHostComponent类似的处理props的过程
// mount的情况
// ...省略服务端渲染相关逻辑
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
- effectList
Q:作为DOM操作的数据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?
A: completeWork在上层函数compelteUnitWork上维护了一个单向链表
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。
类似appendAllChildren,在“归”阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
- 最后
在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。
commitRoot(root);
2. commit阶段
2.1 流程概览
commitRoot(root);
在rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的单向链表effectList,这些Fiber节点的updateQueue中保存了变化的props
这些副作用对应的DOM操作在commit阶段执行。
除此之外,一些生命周期钩子(比如componentDidXXX)、hook(比如useEffect)需要在commit阶段执行。
commit阶段的主要工作(即Renderer的工作流程)分为三部分:
- before mutation阶段(执行DOM操作前)
- mutation阶段(执行DOM操作)
- layout阶段(执行DOM操作后)
- before mutation
do {
// 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// root指 fiberRootNode
// root.finishedWork指当前应用的rootFiber
const finishedWork = root.finishedWork;
// 凡是变量名带lane的都是优先级相关
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 重置Scheduler绑定的回调函数
root.callbackNode = null;
root.callbackId = NoLanes;
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 重置优先级相关变量
markRootFinished(root, remainingLanes);
// 清除已完成的discrete updates,例如:用户鼠标点击触发的更新。
if (rootsWithPendingDiscreteUpdates !== null) {
if (
!hasDiscreteLanes(remainingLanes) &&
rootsWithPendingDiscreteUpdates.has(root)
) {
rootsWithPendingDiscreteUpdates.delete(root);
}
}
// 重置全局变量
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
}
// 将effectList赋值给firstEffect
// 由于每个fiber的effectList只包含他的子孙节点
// 所以根节点如果有effectTag则不会被包含进来
// 所以这里将有effectTag的根节点插入到effectList尾部
// 这样才能保证有effect的fiber都在effectList中
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点没有effectTag
firstEffect = finishedWork.firstEffect;
}
总结: before mutation 之前主要做一些变量赋值,状态重置的工作。
- layout
主要包括三点内容:
- useEffect相关的处理
- 性能追踪相关:代码里有很多和interaction相关的变量。他们都和追踪React渲染时间、性能相关,在Profiler API和DevTool中使用,你可以在这里看到interaction的定义
- 在commit阶段会触发一些生命周期钩子(如 componentDidXXX)和hook(如useLayoutEffect、useEffect)。
在这些回调方法中可能触发新的更新,新的更新会开启新的render-commit流程。
2.2 before mutation(执行DOM前)
遍历effectList并调用commitBeforeMutationEffects函数处理。
// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);
// 将当前上下文标记为CommitContext,作为commit阶段的标志
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 处理focus状态
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
// beforeMutation阶段的主函数
commitBeforeMutationEffects(finishedWork);
focusedInstanceHandle = null;
主要讲一下 commitBeforeMutationEffects
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// ...focus blur相关
}
const effectTag = nextEffect.effectTag;
// 调用getSnapshotBeforeUpdate
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
- 处理DOM节点渲染、删除后的autoFocus、blur等操作;
- 调用getSnapshotBeforeUpdate
- 调用 useEffect
- 调用 getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名,在该方法内会调用getSnapshotBeforeUpdate。
因为在V16版本后,componentWillXXX钩子为UNSAFE_,所以,React提供了替代的生命周期钩子getSnapshotBeforeUpdate,getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题。
Q:为什么从React 16开始,componentWillXXX钩子前增加了UNSAFE_前缀?
从React15升级为React16后,源码改动如此之大,说React被重构可能更贴切些。正是由于变动如此之大,使得一些特性在新旧版本React中表现不一致。
为了让开发者能平稳从旧版本迁移到新版本,React推出了三个模式:
- legacy模式 -- 通过ReactDOM.render创建的应用会开启该模式。这是当前React使用的方式。这个模式可能不支持一些新功能。
- blocking模式 -- 通过ReactDOM.createBlockingRoot创建的应用会开启该模式。开启部分concurrent模式特性,作为迁移到concurrent模式的第一步。
- concurrent模式 -- 通过ReactDOM.createRoot创建的应用会开启该模式。面向未来的开发模式。
但是在从legacy迁移到concurrent模式时,可中断的异步更新还替代了同步更新
在Stack Reconciler重构为Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。
这种行为和Reactv15不一致,所以标记为UNSAFE_。
- componentWillMount -- componentDidMount
- componentWillRecieveProps -- getDerivedStateFromProps
- componentWillUpdate -- getDerivedStateFromProps
在React更新里,每次发起更新都会创建一个Update对象,同一组件的多个Update,会以链表的形式保存在updateQueue中。
- update
const update: Update<*> = {
// ...省略当前不需要关注的字段
lane, // 表示调度优先级
payload: null, // 更新挂载的数据,对于this.setState创建的更新,payload为this.setState的传参
next: null // 与其他update形成链表
};
- updateQueue
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState, // 更新基于哪个state开始
firstBaseUpdate: null, // 更新开始和结束的update
lastBaseUpdate: null,
shared: {
pending: null, // 更新的单个或多个update形成的链表
},
// 其他参数省略...
};
// baseUpdate + shared.pending会作为本次更新需要执行的Update
假设,某个组件updateQueue 存在4个update,数字代表优先级
baseState = '';
A1 - B2 - C1 - D2
// 为了保证更新的连贯性,第一个被跳过的update(B)和后面的update会作为第二次渲染的baseUpdate
// 为BCD
// 首次渲染后
baseState: ''
Updates: [A1, C1]
Result state: 'AC'
// 第二次渲染,B在第一次渲染时被跳过,所以在他之后的C造成的渲
// 染结果不会体现在第二次渲染的baseState中。所以baseState为A而不是上次渲染的Result state AC
// 。这也是为了保证更新的连贯性
baseState: 'A' // 为了保证一致性,C不在
Updates: [B2, C1, D2]
Result state: 'ABCD'
// Updates里出现了两次C
- 调度useEffect
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => { // scheduler提供,调度优先级的回调
// 触发useEffect
flushPassiveEffects(); // 具体见后文hooks
return null;
});
}
}
在 flushPassiveEffects 方法内部会从全局变量 rootWithPendingPassiveEffects 获取effectList,就是会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。
Q:为什么要异步调度:
zh-hans.reactjs.org/docs/hooks-…
与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
防止同步执行时阻塞浏览器渲染
2.3 mutation (执行DOM中)
类似 before mutation,mutation遍历effectList执行函数。这里执行的是commitMutationEffects地址:github.com/facebook/re…
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);
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;
}
}
执行内容:
- 根据ContentReset effectTag重置文字节点
- 更新ref
- 根据effectTag分别处理,其中effectTag包括(Placement | Update | Deletion | Hydrating),hydrate是SSR,不考虑
- placement effect:插入DOM
调用: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);
}
Q:渲染DOM中时间复杂度最高的操作是?
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树
child child child child
rootFiber -----> App -----> div -----> Item -----> li
// DOM树
#root ---> div ---> li
// 在div的子节点Item前加一个p
function App() {
return (
<div>
<p></p>
<Item/>
</div>
)
}
// Fiber树
child child child
rootFiber -----> App -----> div -----> p
| sibling child
| -------> Item -----> li
// DOM树
#root ---> div ---> p
|
---> li
// 此时dom中p的兄弟节点是li
// fiber中fiberP的兄弟节点是fiberItem,fiberItem的子节点才是li
- update effect
调用的方法为commitWork,他会根据Fiber.tag分别处理。
主要关注:FunctionComponent和HostComponent
- FunctionComponent mutation 地址
当fiber.tag为FunctionComponent,会调用commitHookEffectListUnmount。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数
- HostComponent mutation 地址
当fiber.tag为HostComponent,会调用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。
- 递归调用Fiber节点及其子孙Fiber节点中fiber.tag为ClassComponent的componentWillUnmount生命周期钩子,从页面移除Fiber节点对应DOM节点
- 解绑ref
- 调度useEffect的销毁函数
2.4 layout(执行DOM后)
之所以称为layout,因为该阶段的代码都是在DOM渲染完成(mutation阶段完成)后执行的。该阶段触发的生命周期钩子和hook可以直接访问到已经改变后的DOM,即该阶段是可以参与DOM layout的阶段。
- layout阶段也是遍历effectList
root.current = finishedWork;
nextEffect = firstEffect;
do {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
nextEffect = null;
- commitLayoutEffects地址
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 调用生命周期钩子和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// 赋值ref
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
- commitLayoutEffectOnFiber(调用生命周期钩子和hook相关操作)
- commitAttachRef(赋值)
- commitLayoutEffectOnFiber源码地址
- 对于ClassComponent
- 通过current === null?区分是mount还是update,调用componentDidMount 或者componentDidUpdate
- 触发状态更新的this.setState如果赋值了第二个参数回调函数,也会在此时调用
this.setState({ xxx: 1 }, () => { console.log("i am update~"); });
- 对于FunctionComponent及相关类型(如ForwardRef、React.memo或者HOC),他会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数
switch (finishedWork.tag) {
// 以下都是FunctionComponent及相关类型
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的回调函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 调度useEffect的销毁函数与回调函数
schedulePassiveEffects(finishedWork);
return;
}
- commitAttachRef
获取DOM实例,更新Ref
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
// 获取DOM实例
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
if (typeof ref === "function") {
// 如果ref是函数形式,调用回调函数
ref(instanceToUse);
} else {
// 如果ref是ref实例形式,赋值ref.current
ref.current = instanceToUse;
}
}
}
- current fiber切换
root.current = finishedWork;
因为双缓存策略,workInProgress Fiber 树在commit阶段完成渲染后变成current Fiber树。这行代码的作用就是切换fiberRootNode指向的current Fiber树。
Q:双缓存切换执行时间
mutation阶段结束后,layout阶段开始前
所以
- componentWillUnmount在mutation阶段执行。此时current Fiber树还指向前一次更新的Fiber树,在生命周期钩子内获取的DOM还是更新前的;
- componentDidMount和componentDidUpdate会在layout阶段执行。此时current Fiber树已经指向更新后的Fiber树,在生命周期钩子内获取的DOM就是更新后的;