Que's Blog

WebFrontEnd Development

0%

react-v16-Update-commitPhase

前言

我们之前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
    7
    completeRoot
    -> 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
2
3
4
5
6
7
completeRoot
-> commitRoot () {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}

completeRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function completeRoot(
root: FiberRoot,
finishedWork: Fiber,
expirationTime: ExpirationTime,
): void {
// 检查是否存在与此到期时间匹配的批次。
const firstBatch = root.firstBatch;
if (firstBatch !== null && firstBatch._expirationTime >= expirationTime) {
if (completedBatches === null) {
completedBatches = [firstBatch];
} else {
completedBatches.push(firstBatch);
}
if (firstBatch._defer) {
// 这个root节点被阻止进入commitPhase. 取消它的调度直到收到其他更新。
root.finishedWork = finishedWork;
root.expirationTime = NoWork;
return;
}
}

// Commit the root.
// 清空finishedWork 只有finishedWork!==null才会执行进这个函数
// 当然 这里finishedWork作为参数传进来了 清不清空都不影响我们调用
root.finishedWork = null;

// 检查这是否是一个嵌套的更新(有一个同步的更新在commit阶段被纳入更新计划)
if (root === lastCommittedRootDuringThisBatch) {
// 如果新的root和旧的root相同。这事一个嵌套更新。为了防止陷入死循环。
// 对nestedUpdateCount变量进行累加
nestedUpdateCount++;
} else {
// 切换root时候 对nestedUpdateCount变量清零
lastCommittedRootDuringThisBatch = root;
nestedUpdateCount = 0;
}
runWithPriority(ImmediatePriority, () => {
commitRoot(root, finishedWork);
});
}

这里细节不算多。

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
}

var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = priorityLevel;
currentEventStartTime = getCurrentTime();

try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;

// Before exiting, flush all the immediate work that was scheduled.
// 退出之前,将所有规划好的立即执行任务执行完毕。
flushImmediateWork();
}
}

这个函数主要是设定currentPriorityLevel && currentEventStartTime,然后执行eventHandler(),完毕之后将这两个值还原。并执行flushImmediateWork()

在这里eventHandler === () => (commitRoot(root, finishedWork))。我们先继续这个路线。

commitRoot

这里代码有点长,做一些精简。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
isWorking = true;
isCommitting = true;
startCommitTimer();

// 缓存并清空root.pendingCommitExpirationTime属性
const committedExpirationTime = root.pendingCommitExpirationTime;
root.pendingCommitExpirationTime = NoWork;

// 更新优先级
// 更新待处理pending优先级以考虑我们即将提交的工作。
// 这需要在调用生命周期之前进行,因为他们可能会安排其他更新。
const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
// childExpirationTime 和 expirationTime对比
const earliestRemainingTimeBeforeCommit =
childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
? childExpirationTimeBeforeCommit
: updateExpirationTimeBeforeCommit;
markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit);

let prevInteractions: Set<Interaction> = (null: any);
if (enableSchedulerTracing) {// 略 }

// Reset this to null before calling lifecycles
// 重置ReactCurrentOwner.current防止生命周期函数被影响
ReactCurrentOwner.current = null;

// 根据effectTag 处理Effect List链表
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;
}
// 预备渲染新的节点。此时禁止事件的触发,统计后面需要autofocus的节点
prepareForCommit(root.containerInfo);

// Invoke instances of getSnapshotBeforeUpdate before mutation.
// 遍历执行实例中的getSnapshotBeforeUpdate
nextEffect = firstEffect;
startCommitSnapshotEffectsTimer();
while (nextEffect !== null) {
let didError = false;
let error;
if (__DEV__) {} else {
try {
commitBeforeMutationLifecycles();
} catch (e) {
didError = true;
error = e;
}
}
if (didError) { /* 错误捕获并将指针移动到下一个Effect */ }
}
stopCommitSnapshotEffectsTimer();

if (enableProfilerTimer) {// 略}

// commit tree中所有的side-effects。这里分两个步骤
// 这里是第一个步骤:执行所有host的插入、更新、删除和ref卸载(注意后面第二个步骤)
nextEffect = firstEffect;
startCommitHostEffectsTimer();
while (nextEffect !== null) {
let didError = false;
let error;
if (__DEV__) { } else {
try {
commitAllHostEffects();
} catch (e) {
didError = true;
error = e;
}
}
if (didError) { /* 错误捕获并将指针移动到下一个Effect */ }
}
stopCommitHostEffectsTimer();
// 在已经将真实的节点挂载后触发
resetAfterCommit(root.containerInfo);

// 此时work-in-progress tree 就是 current tree。
// 这必须发生在遍历执行side-effects第一个步骤之后,这样current树仍然是the previous tree
// componentWillUnmount(resetAfterCommit)此时执行
// 但是又必须发生在遍历执行side-effects第二个步骤之前,这样finishedWork树是current树
// 方便执行componentDidMount/Update(commitAllLifeCycles)
root.current = finishedWork;

// 在commit side-effects第二步骤,我们执行所有声明周期函数和ref回调
// 生命周期作为单独的阶段触发,如此整个tree上所有的替换、更新、删除都将被调用
// 在这个阶段,所有的renderer-specific initial effects也会被触发
nextEffect = firstEffect;
startCommitLifeCyclesTimer();
while (nextEffect !== null) {
let didError = false;
let error;
if (__DEV__) { } else {
try {
commitAllLifeCycles(root, committedExpirationTime);
} catch (e) {
didError = true;
error = e;
}
}
if (didError) { /* 错误捕获并将指针移动到下一个Effect */ }
}

if (firstEffect !== null && rootWithPendingPassiveEffects !== null) {
// This commit included a passive effect. These do not need to fire until
// after the next paint. Schedule an callback to fire them in an async
// event. To ensure serial execution, the callback will be flushed early if
// we enter rootWithPendingPassiveEffects commit phase before then.
let callback = commitPassiveEffects.bind(null, root, firstEffect);
if (enableSchedulerTracing) {
// TODO: Avoid this extra callback by mutating the tracing ref directly,
// like we do at the beginning of commitRoot. I've opted not to do that
// here because that code is still in flux.
callback = Scheduler_tracing_wrap(callback);
}
passiveEffectCallbackHandle = runWithPriority(NormalPriority, () => {
return schedulePassiveEffects(callback);
});
passiveEffectCallback = callback;
}

isCommitting = false;
isWorking = false;
stopCommitLifeCyclesTimer();
stopCommitTimer();
onCommitRoot(finishedWork.stateNode);
if (__DEV__ && ReactFiberInstrumentation.debugTool) { }

const updateExpirationTimeAfterCommit = finishedWork.expirationTime;
const childExpirationTimeAfterCommit = finishedWork.childExpirationTime;
const earliestRemainingTimeAfterCommit =
childExpirationTimeAfterCommit > updateExpirationTimeAfterCommit
? childExpirationTimeAfterCommit
: updateExpirationTimeAfterCommit;
if (earliestRemainingTimeAfterCommit === NoWork) {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
legacyErrorBoundariesThatAlreadyFailed = null;
}
onCommit(root, earliestRemainingTimeAfterCommit);

if (enableSchedulerTracing) {
// 开发工具追踪代码 这里可以不管
}
}

这里对宏观结构做一些总结, 细节一些的地方可以参见注释,为了方便理解根据自己理解给翻译为中文了。

1
2
3
4
5
6
7
8
9
function commitRoot(root: FiberRoot, finishedWork: Fiber): void {
// 1. 更新优先级 markCommittedPriorityLevels()
// 2. 根据effectTag 处理Effect List链表
// 3. 突变之前操作: commitBeforeMutationLifecycles->getSnapshotBeforeUpdate
// 4. 执行side-effects 这里分两个步骤
// 4.1 执行所有host的插入、更新、删除和ref卸载 commitAllHostEffects
// 4.2 执行其他所有sideEffect commitAllLifeCycles
// 5. onCommitRoot() & onCommit()
}

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate是新增的生命周期函数。它发生在componentWillUpdate、componentDidUpdate之前,它的触发时机是 React 进行修改前(通常是更新 DOM)的“瞬间”,它的返回值会作为第三个参数传入 componentDidUpdate 。这个值会被挂在instance.__reactInternalSnapshotBeforeUpdate上,当实例注销时候也会注销。

它由commitBeforeMutationLifecycles函数引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
recordEffect();
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}

function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork);
return;
}
case ClassComponent: {
if (finishedWork.effectTag & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
startPhaseTimer(finishedWork, 'getSnapshotBeforeUpdate');
const instance = finishedWork.stateNode;

const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
stopPhaseTimer();
}
}
return;
}
// 略
}
}

这里针对FunctionComponent有个单独的分支。主要操作是执行effect.create() || effect.destroy();代码这里逻辑是进行destroy()。更全面点讲,这里遍历finishedWork.updateQueue链表,执行了上面的每个effect.destroy()。

commitAllHostEffects

这个函数名称真的足够简练,没有一个字是多余的。。。

All这里提现在对effect的遍历上,Host则提现在对HostComponent的处理上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function commitAllHostEffects() {
while (nextEffect !== null) {
recordEffect();
const effectTag = nextEffect.effectTag;

if (effectTag & ContentReset) {
commitResetTextContent(nextEffect); // 把节点的文字内容设为空字符串
}

if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current); // 清空ref属性 后面要重新设置
}
}

// 这里swtich/case逻辑只关心替换、更新、删除逻辑。为了避免其他可能值,这里就没有写次要effect值
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
// 清除效果标记中的“placement”,以便在调用componentDidMount之类的生命周期之前
// 我们知道这是插入的
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;

// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(nextEffect);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}

这里有4个case,但是实质上只有3种操作,这三种操作分别是替换、更新、删除。多出来的PlacementAndUpdate实质上是替换+更新的组合。

另外,注意这里的遍历,它遍历了所有的nextEffect。

再次,nextEffect这个链表的构建,在completeUnitOfWork函数里面。

commitPlacement - 替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}

// 根据fiber.return向上爬,直到找到一个HostComponent|HostRoot|HostPortal返回
const parentFiber = getHostParentFiber(finishedWork);

// Note: 必须同时被更新
let parent;
let isContainer;

switch (parentFiber.tag) {
case HostComponent: // 对应v15 ReactDomComponent
parent = parentFiber.stateNode;
isContainer = false;
break;
case HostRoot: // fiberRoot fiber链表第一个元素
parent = parentFiber.stateNode.containerInfo;
isContainer = true;
break;
case HostPortal: // Portal
parent = parentFiber.stateNode.containerInfo;
isContainer = true;
break;
default:
}
if (parentFiber.effectTag & ContentReset) {
// 在做插入操作前 清空parnet里面text文本
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.effectTag &= ~ContentReset;
}

const before = getHostSibling(finishedWork);
// 我们只有顶部的fiber被插入了。我们需要递归下级节点以找到终端节点
let node: Fiber = finishedWork;
while (true) {
if (node.tag === HostComponent || node.tag === HostText) {
// 如果它有nextSibling 那么底层使用insertBefore插入节点
if (before) {
if (isContainer) {
insertInContainerBefore(parent, node.stateNode, before);
} else {
insertBefore(parent, node.stateNode, before);
}
} else { // 否则底层使用appendChild追加到尾部
if (isContainer) {
appendChildToContainer(parent, node.stateNode);
} else {
appendChild(parent, node.stateNode);
}
}
} else if (node.tag === HostPortal) {
// 如果操作的是一个Portal组件 我们就不遍历它的子节点 直接对其本身进行插入操作
} else if (node.child !== null) {
// 如果child节点不为空 那么将firstChild设为node重新遍历
node.child.return = node;
node = node.child;
continue;
}
if (node === finishedWork) { // 此时没有变化 不做操作
return;
}
while (node.sibling === null) { // 当前节点没有nextSibling了 返回上级节点
if (node.return === null || node.return === finishedWork) {
return;
}
node = node.return;
}
// 开始遍历上级节点的sibing
node.sibling.return = node.return;
node = node.sibling;
}
}

位与运算符前端不太好理解,不过这里也就懒得说里面的过程。ContentReset = 0b000000010000 =16。当运行parentFiber.effectTag & ContentReset时候,明显可见的规律是,当effectTag除以ContentReset结果取整进行Math.floor操作,如果这个值是奇数,它的值是16,否则为0。

而parentFiber.effectTag &= ContentReset在这里基本等同于parentFiber.effectTag -= ContentReset。不过它和减法还是有很大区别。它对015不会执行减法——但是0~15显然进不来这个分支。

其他的不妨看看注释,这里主线是对finishWork:fiber进行遍历,核心操作是执行insertBefore或者appendChild操作。

commitWork - 更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
if (!supportsMutation) { // 这里暂时不理解MemoComponent 略过它
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
return;
}
}
commitContainer(finishedWork);
return;
}

switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: { // 这里暂时不理解MemoComponent 略过它
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
return;
}
case ClassComponent: {
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
// 更新文本 这里暂时略过
return;
}
case HostRoot: {
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
// 错误捕获 这里略
return;
}
case IncompleteClassComponent: {
return;
}
default: { }
}
}

这里加了若干注释,并对干扰代码进行了删除。可以看到,这里核心的处理分支是HostComponent,这也比较符合之前的认知。这里猜测可以进行部分Diff对比了: 先对比instance,然后直接进行HostComponent替换(ComponentDiff)或者子节点的ElementDiff操作。核心是commitUpdate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// 将新的Props挂到domElement上(此时它还挂了一个FiberNode)
updateFiberProps(domElement, newProps);
// 它主要还是调用updateDOMProperties
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
export function updateFiberProps(node, props) {
node[internalEventHandlersKey] = props;
}
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// 略过兼容特例处理
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
updateDOMProperties( // 这个函数就很眼熟了 v15里面它也是非常重要的函数
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// 略过兼容特例处理
}

主要的逻辑已经给出了。这里看看重点的updateDOMProperties函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue); // 可以视为innerHTML
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue); // 赋值给textContent|nodeValue
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}

这个函数对比v15.6版本又做了封装。但是不得不说清爽太多了。但是总体的逻辑倒也没什么变化。主要是区分了样式、Text、Property进行处理。另外就是特例API处理dangerouslySetInnerHTML

这里最复杂的应该是setValueForProperty:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export function setValueForProperty(
node: Element,
name: string,
value: mixed,
isCustomComponentTag: boolean,
) {
const propertyInfo = getPropertyInfo(name);
if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
return;
}
if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
value = null;
}
// If the prop isn't in the special list, treat it as a simple attribute.
if (isCustomComponentTag || propertyInfo === null) {
if (isAttributeNameSafe(name)) {
const attributeName = name;
if (value === null) {
node.removeAttribute(attributeName);
} else {
node.setAttribute(attributeName, '' + (value: any));
}
}
return;
}
const {mustUseProperty} = propertyInfo;
if (mustUseProperty) {
const {propertyName} = propertyInfo;
if (value === null) {
const {type} = propertyInfo;
(node: any)[propertyName] = type === BOOLEAN ? false : '';
} else {
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
(node: any)[propertyName] = value;
}
return;
}
// The rest are treated as attributes with special cases.
const {attributeName, attributeNamespace} = propertyInfo;
if (value === null) {
node.removeAttribute(attributeName);
} else {
const {type} = propertyInfo;
let attributeValue;
if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
attributeValue = '';
} else {
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
attributeValue = '' + (value: any);
}
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
} else {
node.setAttribute(attributeName, attributeValue);
}
}
}

这里分支多。但是说白了,其实最后也就是setAttribute & removeAttribute这套。

总结一下就是: 它真的只是apply the diff。而不包含Diff计算了。

commitDeletion-删除

1
2
3
4
5
6
7
8
9
10
11
12
function commitDeletion(current: Fiber): void {
if (supportsMutation) {
// 从current节点上递归删除所有host nodes
// 移除ref,并调用所有的componentWillUnmount
unmountHostComponents(current);
} else {
// 移除ref,并调用所有的componentWillUnmount
commitNestedUnmounts(current); // 递归fiber子节点调用commitUnmount卸载
}
// 清空current节点和current.alternate节点(return child updateQueue memoizedState)
detachFiber(current);
}

这里有个commitUnmount函数。得讲一讲。它是commitNestedUnmounts的核心,整个umount环节,在粒度上,它应该是最小的情况了。

commitNestedUnmounts主要是对current节点进行下层级递归调用commitUnmount。

而commitUnmount主要是对ref进行清除、递归调用componentWillUnmount、或者是useEffect的清除。特例情况是HostPortal则需要进步遍历下级节点然后递归调用自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function commitUnmount(current: Fiber): void {
onCommitUnmount(current);

switch (current.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: { // useEffect相关 hook注销。理解这里得先熟悉Hook使用
const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
if (updateQueue !== null) {
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const destroy = effect.destroy;
if (destroy !== undefined) {
safelyCallDestroy(current, destroy);
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
break;
}
case ClassComponent: {
safelyDetachRef(current); // 清除ref。ref为函数则传入null执行清空
const instance = current.stateNode;
// 安全执行componentWillUnmount
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(current, instance);
}
return;
}
case HostComponent: {
safelyDetachRef(current); // 清除ref。ref为函数则传入null执行清空
return;
}
case HostPortal: {
if (supportsMutation) {
unmountHostComponents(current);
} else if (supportsPersistence) {
emptyPortalContainer(current);
}
return;
}
}
}

然后就是unmountHostComponents函数。其他函数都说了,这个最后还是得看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
function unmountHostComponents(current): void {
// We only have the top Fiber that was deleted but we need to recurse down its
// children to find all the terminal nodes.
let node: Fiber = current;

// Each iteration, currentParent is populated with node's host parent if not
// currentParentIsValid.
let currentParentIsValid = false;

// Note: these two variables *must* always be updated together.
let currentParent;
let currentParentIsContainer;

while (true) {
if (!currentParentIsValid) {
let parent = node.return;
// 向上遍历node.return, 直到node.tag对应以下三种类型的组件为止 才继续往下走
// 此时currentParentIsValid赋值为true 后面再进上一个while就不会进入此处
// 或者说 不会更新currentParent && currentParentIsContainer
findParent: while (true) {
switch (parent.tag) {
case HostComponent:
currentParent = parent.stateNode;
currentParentIsContainer = false;
break findParent;
case HostRoot:
currentParent = parent.stateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
case HostPortal:
currentParent = parent.stateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
}
parent = parent.return;
}
currentParentIsValid = true;
}

if (node.tag === HostComponent || node.tag === HostText) { // HostComponent&Text直接设法移除
commitNestedUnmounts(node);
// umount所有children之后,开始将node从tree中移除
if (currentParentIsContainer) { // 如果当前parent是一个容器组件 比如HostRoot&HostPortal
removeChildFromContainer( // 这个函数几类于: currentParent.removeChild(node.stateNode) 但是多了个注释节点的处理
((currentParent: any): Container),
(node.stateNode: Instance | TextInstance),
);
} else { // 如果是另外一个HostComponent(原ReactDomComponent)
removeChild( // 等同:currentParent.removeChild(node.stateNode)
((currentParent: any): Instance),
(node.stateNode: Instance | TextInstance),
);
}
// Don't visit children because we already visited them.
} else if (
enableSuspenseServerRenderer &&
node.tag === DehydratedSuspenseComponent
) {
// 服务端渲染 这里不管 略过
} else if (node.tag === HostPortal) { // 递归HostPortal 对其脱壳处理 转换成普通组件处理
if (node.child !== null) {
// When we go into a portal, it becomes the parent to remove from.
// We will reassign it back when we pop the portal on the way up.
currentParent = node.stateNode.containerInfo;
currentParentIsContainer = true;
// Visit children because portals might contain host components.
node.child.return = node;
node = node.child;
continue;
}
} else { // 遇到FunctionComponent|ClassComponent什么的直接卸载
commitUnmount(node);
// 递归node.child
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
// 以下都是遍历Fiber树需要的代码
if (node === current) {
return;
}
while (node.sibling === null) {
if (node.return === null || node.return === current) {
return;
}
node = node.return;
if (node.tag === HostPortal) {
// When we go out of the portal, we need to restore the parent.
// Since we don't keep a stack of them, we will search for it.
currentParentIsValid = false;
}
}
node.sibling.return = node.return;
node = node.sibling;
}
}

commitAllLifeCycles

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function commitAllLifeCycles(
finishedRoot: FiberRoot,
committedExpirationTime: ExpirationTime,
) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;

if (effectTag & (Update | Callback)) {
recordEffect();
const current = nextEffect.alternate;
commitLifeCycles(
finishedRoot,
current,
nextEffect,
committedExpirationTime,
);
}

if (effectTag & Ref) {
recordEffect();
commitAttachRef(nextEffect);
}

if (effectTag & Passive) {
rootWithPendingPassiveEffects = finishedRoot;
}

nextEffect = nextEffect.nextEffect;
}
}

这里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值来决定。关于这个计算暂时还没搞懂,后面再来补充。