Que's Blog

WebFrontEnd Development

0%

react-fiber-render

开头

因为最近阅读的代码从v15.6.2换到了v16.8.6了,这之间的版本发布时间大概有一年之久了。其间很多东西都发生了变化,尤其是React Fiber用链表替代了之前的树结构。所以这里的Render必须重新实现。

这里换了地图这块还是必须先看看。

Render线路-初始化

想了个办法一边展示调用,以便精简代码展示核心调用,这里算是入口级别的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ReactDOM.render
->legacyRenderSubtreeIntoContainer {
root = legacyCreateRootFromDOMContainer()
-> new ReactRoot -> createContainer -> createFiberRoot -> Object.assign(createHostRootFiber(), {
current: createHostRootFiber() -> createFiber()
})
root.render() -> ReactRoot.prototype.render() {
const work = new ReactWork();
updateContainer(children, root, null, work._onCommit)
-> updateContainerAtExpirationTime
-> scheduleRootUpdate () {
flushPassiveEffects();
enqueueUpdate(current, update) -> appendUpdateToQueue
scheduleWork(current, expirationTime)
-> requestWork -> performWorkOnRoot -> (renderRoot -> workLoop) || completeRoot
}
}
}

这里初次渲染的路径大体是:

  • 根据给定的DOM创建一个FiberRoot元素,然后将这个FiberRoot.current指向createHostRootFiber(),接下来调用ReactRoot上的render进行渲染。
  • ReactRoot.render一路走到renderRoot。

renderRoot是一个很复杂的函数,核心的调用workLoop里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until there's a higher priority event
while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}

这里暂时不考虑渲染过程的中断,所以核心就是对performUnitOfWork(nextUnitOfWork)的遍历,直到其值返回null为止。

遍历理论

这里遍历逻辑有点折腾。不精简的话实在有些不太好理解。

究其根本,个人认为基于链表的树遍历确实有些反人类直觉——不然React一开始也不会使用Tree结构而没有考虑用链表。

这里涉及4个API: performUnitOfWork|beginWork|completeUnitOfWork|completeWork。在遍历这个事情上,我用了参考文章里面提到的伪代码(没办法,这个看懂了在宏观上对遍历有了概念,理解相关事情就容易多了):

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
function workLoop() {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}

function performUnitOfWork(workInProgress) {
let next = beginWork(workInProgress);
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
return next;
}

function beginWork(workInProgress) {
log('work performed for ' + workInProgress.name);
return workInProgress.child;
}

function completeUnitOfWork(workInProgress) {
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;

nextUnitOfWork = completeWork(workInProgress);

if (siblingFiber !== null) {
// If there is a sibling, return it
// to perform work for this sibling
return siblingFiber;
} else if (returnFiber !== null) {
// If there's no more work in this returnFiber,
// continue the loop to complete the returnFiber.
workInProgress = returnFiber;
continue;
} else {
// We've reached the root.
return null;
}
}
}

function completeWork(workInProgress) {
log('work completed for ' + workInProgress.name);
return null;
}

简而言之,这是一套基于Fiber链表结构、但是针对树的遍历算法。这里就算法说一些理论上的东西:

  1. 对指定节点,递归进入第一个child节点(进入子节点 如果子节点还有子节点直接进入 如此类推)

  2. 直到child===null, 开始挨个查找sibling节点(如果该节点有child,重新执行整个1&2逻辑)

  3. 直到sibling节点===null,返回上一级节点

  4. 在上一节节点上找sibing,如果有child,递归child重复之前逻辑,没有直接返回上一级

  5. 最后如此重复,一直到返回根节点。

这套遍历算法可以将整棵树都遍历到。如果只是理论上的遍历,理论上它没有之前树遍历快,因为树遍历可以多节点并行遍历,而这里基于链表只能一个一个线性去遍历。

但是,就像之前基于树Tree里面有粗暴的直接替换整个下级一样,这里的线性遍历也可以借助类似方法进行判断跳过一些child的深入以实现相差无几的高效遍历。

这个遍历,是从底部往顶部、从左边向右边的遍历。

至于为什么这里先讲道理再后面说详细流程,说到底,根据代码逆向反推思路,真的是一件费时费力,还掉头发的事情。。。有上层思想指导,再去看就不会走弯路——实在是看v15时候吃了太多这方面的亏,这次有现成文章,就好好参考一下。😂

源码细节(render阶段)

workLoop及前置调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function updateContainerAtExpirationTime(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
expirationTime: ExpirationTime,
callback: ?Function,
) {
// TODO: If this is a nested container, this won't be the root.
const current = container.current;
// 回去最近的父祖节点context
// 如果自定义组件定义了childContextTypes则返回该组件context
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}

return scheduleRootUpdate(current, element, expirationTime, callback);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function scheduleRootUpdate(
current: Fiber,
element: ReactNodeList,
expirationTime: ExpirationTime,
callback: ?Function,
) {
const update = createUpdate(expirationTime);
// React DevTools依赖该变量 element
update.payload = {element};
callback = callback === undefined ? null : callback;
if (callback !== null) {
// callback必须为function 否则这里会报错
update.callback = callback;
}
flushPassiveEffects();
enqueueUpdate(current, update);
scheduleWork(current, expirationTime);

return expirationTime;
}

这里enqueueUpdate主要是调用appendUpdateToQueue。这个函数基本可以理解为向update数组push一个元素。

1
2
3
4
5
6
7
8
9
10
function appendUpdateToQueue(queue, update) {
// lastUpdate===null说明之前是空的队列
if (queue.lastUpdate === null) {
queue.firstUpdate = queue.lastUpdate = update;
} else {
// 否则将update放到链表队列尾部
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}

最后enqueueUpdate执行的结果是给fiber.updateQueue以及fiber.alternate.updateQueue进行了赋值。这里后面看看具体含义。

核心的调用是scheduleWork。

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

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) {
return;
}

markPendingPriorityLevel(root, expirationTime);
if (
// 如果是在render阶段 我们不需要规划update行为
// 因为必须在存在后才能再去做这些
!isWorking ||
isCommitting ||
// 直到这个root和我们正要render的不一样
nextRoot !== root
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime);
}
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
// Reset this back to zero so subsequent updates don't throw.
nestedUpdateCount = 0;
// 一些报错 无关分析 删
}
}
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
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
// 禁止递归调用 后面的任务在结束再重新开始
return;
}

if (isBatchingUpdates) { // 初始render这里为false
// 在批处理结束后开始清洗工作(针对脏组件|Fiber?)
if (isUnbatchingUpdates) {
// 除非被排除在unbatchedUpdates,否则现在需要开始进行清洗
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}

// TODO: Get rid of Sync and use current time?
// 这里是同步异步处理分支 这里暂时看同步逻辑
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
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
performSyncWork -> performWork(Sync, false);
function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {
// Keep working on roots until there's no more work, or until there's a higher
// priority event.
findHighestPriorityRoot();

if (isYieldy) {
// 异步逻辑 这里暂时不管
} else {
while (
nextFlushedRoot !== null && // 还有root没有处理完(含有child的节点)
nextFlushedExpirationTime !== NoWork && // 过期时间不为NoWork
minExpirationTime <= nextFlushedExpirationTime // 此时这两个变量相等
) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
findHighestPriorityRoot();
}
}
if (nextFlushedExpirationTime !== NoWork) {
scheduleCallbackWithExpirationTime(
((nextFlushedRoot: any): FiberRoot),
nextFlushedExpirationTime,
);
}
// Clean-up.
finishRendering();
}

findHighestPriorityRootperformWorkOnRoot函数可能是这里最核心的地方。

findHighestPriorityRoot函数命名已经说明它的作用: 「找到最高优先级根」。

这个函数和Fiber结构联系很紧密: Fibler.nextScheduledRoot是核心变量,它主要是对firstScheduledRoot(packages/react-reconciler/src/ReactFiberScheduler.js)变量(闭包变量)进行操作。这个变量记录我们要进行操作的节点根。

但是这个函数需要一个初始值,ReactFiberScheduler.js中它的初始值为null。那么它的常规值从哪儿来呢?从requestWork中的addRootToSchedule函数调用里。

这个函数这里不贴代码,认真看看,可以知道,当我们初次渲染时候,我们得到要操作的节点是根节点。而且根节点的nextScheduledRoot也设为了自身。

1
2
firstScheduledRoot = lastScheduledRoot = root;
root.nextScheduledRoot = root;
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
function findHighestPriorityRoot() {
let highestPriorityWork = NoWork;
let highestPriorityRoot = null;
if (lastScheduledRoot !== null) {
let previousScheduledRoot = lastScheduledRoot;
let root = firstScheduledRoot;
while (root !== null) {
const remainingExpirationTime = root.expirationTime;
if (remainingExpirationTime === NoWork) {
// 初始render不走这里 略
} else {
if (remainingExpirationTime > highestPriorityWork) {
// Update the priority, if it's higher
highestPriorityWork = remainingExpirationTime;
highestPriorityRoot = root;
}
if (root === lastScheduledRoot) {
break;
}
// 初始render后面被break 略
}
}
}

nextFlushedRoot = highestPriorityRoot;
nextFlushedExpirationTime = highestPriorityWork;
}

总之,通过这个函数,nextFlushedRoot这里被设置为rootFiber了。后面更新逻辑我们再来继续研究它更多分支。

performUnitOfWork

接下来就是performWorkOnRoot函数。这个函数之前有伪代码。这里还是要看看实际代码。

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
function performWorkOnRoot(
root: FiberRoot,
expirationTime: ExpirationTime,
isYieldy: boolean,
) {
isRendering = true;

// 检查同步还是异步
if (!isYieldy) {
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// 此时这个root已经处理好了,可以进入commit阶段
completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
// 如果有之前暂停的任务,重置超时时间
// 我们将会重新进行渲染
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
renderRoot(root, isYieldy); // renderRoot是一个链表遍历的过程
finishedWork = root.finishedWork;
if (finishedWork !== null) {
// 此时这个root已经处理好了,可以进入commit阶段
completeRoot(root, finishedWork, expirationTime);
}
}
} else {
// 异步Flush 略
}

isRendering = false;
}

renderRoot可能是最长的一个函数了。。。

这里保留核心代码

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 renderRoot(root: FiberRoot, isYieldy: boolean): void {
// 禁止renderRoot递归调用 一旦如此则需要抛出错误

isWorking = true; // 作为禁止被递归调用的Flag
const previousDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = ContextOnlyDispatcher;

const expirationTime = root.nextExpirationTimeToWorkOn;

// Check if we're starting from a fresh stack, or if we're resuming from
// previously yielded work.
// 判断是一个新的stack开始,还是从之前被中断的地方开始
if (
expirationTime !== nextRenderExpirationTime ||
root !== nextRoot ||
nextUnitOfWork === null
) {
// Reset the stack and start working from the root.
// 重置stack 从Root开始工作
resetStack();
nextRoot = root;
nextRenderExpirationTime = expirationTime;
// 创建nextUnitOfWork === workInProgress:Fiber;
// 如果进入了这个逻辑 这个nextUnitOfWork变量会被workLoop引用到
nextUnitOfWork = createWorkInProgress(
nextRoot.current,
null,
nextRenderExpirationTime,
);
root.pendingCommitExpirationTime = NoWork;


let didFatal = false;

startWorkLoopTimer(nextUnitOfWork);

do {
try {
workLoop(isYieldy);
} catch (thrownValue) {
// 错误处理
}
break;
} while (true);
// Ready to commit.
onComplete(root, rootWorkInProgress, expirationTime);
}

接下来是workLoop,在这个初始render环节里面,它就是一个while遍历,如下:

1
2
3
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}

此时nextUnitOfWork会每次更新,直到nextUnitOfWork === null,此时意味着所有节点被遍历完毕了。不过还是看看里面逻辑。精简一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
const current = workInProgress.alternate;

startWorkTimer(workInProgress);

let next;

next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;

if (next === null) {
next = completeUnitOfWork(workInProgress);
}

ReactCurrentOwner.current = null;
return next;
}

beginWork

然后就是beginWork——有理由相信,这个函数以及涉及到的相关函数,是整个render环节里面最复杂的,因为它直接负责了N种实例的实例化、更新、挂载,并将fiber.child返回。

这N种实例包括:

  • IndeterminateComponent
  • LazyComponent
  • FunctionComponent
  • ClassComponent
  • HostRoot
  • HostComponent
  • HostText
  • SuspenseComponent
  • HostPortal
  • ForwardRef
  • Fragment
  • Mode
  • Profiler
  • ContextProvider
  • ContextConsumer
  • MemoComponent
  • SimpleMemoComponent
  • IncompleteClassComponent
  • DehydratedSuspenseComponent

它是具体负责将各种不同Fiber进行分类的函数,换句时髦的话,它负责『垃圾分类』。当我们初始render时候,走的是HostRoot分支。返回了一个return updateHostRoot(args)。这里用它做切入点,其余暂且不论。

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
function updateHostRoot(current, workInProgress, renderExpirationTime) {
pushHostRootContext(workInProgress);
const updateQueue = workInProgress.updateQueue;

// workInProgress.pendingProps|workInProgress.prevState
// workInProgress.prevState三个变量含义这里不多说
// 主要是追溯其赋值来源
// 回顾调用,workInProgress是都是一个个节点 或fiber.next,或fiber.child
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
// 关于这个函数看后面的分析 完了再跳回来
// 它主要是处理updateQueue队列,并更新了workInProgress.memoizedState
processUpdateQueue(
workInProgress,
updateQueue,
nextProps,
null,
renderExpirationTime,
);
const nextState = workInProgress.memoizedState;
// Caution: React DevTools currently depends on this property
// being called "element".
const nextChildren = nextState.element;
if (nextChildren === prevChildren) { // 这个分支初始render不会进来
// If the state is the same as before, that's a bailout because we had
// no work that expires at this time.
resetHydrationState();
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderExpirationTime,
);
}
const root: FiberRoot = workInProgress.stateNode;
if (
// 这个分支初始render不会进来
(current === null || current.child === null) &&
root.hydrate &&
enterHydrationState(workInProgress)
) {
workInProgress.effectTag |= Placement;
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// 初始render会进来
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
resetHydrationState();
}
return workInProgress.child;
}

关联调用(processUpdateQueue)

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
export function processUpdateQueue<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
props: any,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
hasForceUpdate = false;
// 一般直接返回queue 但是当workInProgress.alternate.updateQueue === queue
// 需要将workInProgress.updateQueue重新做个副本并赋值回去,再返回这个副本
queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);

// 这些值可能在后面迭代中被更改。注意是let不是const
let newBaseState = queue.baseState;
let newFirstUpdate = null;
let newExpirationTime = NoWork;

// Iterate through the list of updates to compute the result.
let update = queue.firstUpdate;
let resultState = newBaseState;
while (update !== null) {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) { // 这里分支暂时可以忽略
// 此更新没有足够的优先级。跳过它。
if (newFirstUpdate === null) {
// 这是第一次跳过的更新.下次更新列表中它将排第一个
newFirstUpdate = update;
// 由于这是第一次跳过的更新, 当前结果是'the new base state'.
newBaseState = resultState;
}
// 由于此更新将保留在列表中,因此请更新剩余的到期时间。
if (newExpirationTime < updateExpirationTime) {
newExpirationTime = updateExpirationTime;
}
} else {
// 此更新确实具有足够的优先级。处理它并计算新结果。

// 这个分支做了一下这些事:
// 1.根据update.tag等参数,得到目标update.payload的处理后的State值
// 2.根据update.callback设置queue的firstEffect、lastEffect属性
// 以及queue.lastEffect.nextEffect属性
// 3.对queue.next执行上面操作 如此重复

// getStateFromUpdate还是值得看看. update.tag有4个Enum值:
// ReplaceState|CaptureUpdate|UpdateState|ForceUpdate
// 这里初始render对应的是UpdateState 这里针对Function Component和partialState有特殊处理
// 好吧 这里不管那么多分支 常规的ClassComponent返回的就是一个:
// update.payload。
// 简单点理解: (queue.firstUpdate.tag) => { ...queue.firstUpdate.payload }
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
const callback = update.callback;
if (callback !== null) {
workInProgress.effectTag |= Callback;
// Set this to null, in case it was mutated during an aborted render.
update.nextEffect = null;
if (queue.lastEffect === null) { // 如果这个值为null这queue链表是空
queue.firstEffect = queue.lastEffect = update;
} else {
// 否则将原来的lastEffect的下个Effect设为update
// 然后将lastEffect指向update;这个操作实质上类似链表版本的Array.push
// 是将update放到queue链表最后一个位置上
queue.lastEffect.nextEffect = update;
queue.lastEffect = update;
}
}
}
// Continue to the next update.
update = update.next;
}

// 迭代list模拟事件捕获 这里不管它只看正常开发用到的冒泡
// 代码略

// 如果有跳过的update 这里会进入分支处理queue.lastUpdate值
if (newFirstUpdate === null) {
queue.lastUpdate = null;
}
if (newFirstCapturedUpdate === null) {
queue.lastCapturedUpdate = null;
} else {
workInProgress.effectTag |= Callback;
}
if (newFirstUpdate === null && newFirstCapturedUpdate === null) {
// We processed every update, without skipping. That means the new base
// state is the same as the result state.
// 我们处理了每次更新,没有跳过。这意味着新的基本状态与结果状态相同。
newBaseState = resultState;
}

queue.baseState = newBaseState;
queue.firstUpdate = newFirstUpdate;
queue.firstCapturedUpdate = newFirstCapturedUpdate;

// Set the remaining expiration time to be whatever is remaining in the queue.
// This should be fine because the only two other things that contribute to
// expiration time are props and context. We're already in the middle of the
// begin phase by the time we start processing the queue, so we've already
// dealt with the props. Context in components that specify
// shouldComponentUpdate is tricky; but we'll have to account for
// that regardless.
workInProgress.expirationTime = newExpirationTime;
workInProgress.memoizedState = resultState;

}

reconcileChildren

关联调用(reconcileChildren),这函数名让我想起了v15版本的树结构的递归mount children节点。然而这里我们不是由它做递归,递归是由workLoop做的。它真的就是只处理父子两个节点的事情。

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
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderExpirationTime: ExpirationTime,
) {
if (current === null) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
// 如果是一个新鲜的没有渲染过的组件 我们不会通过最小side-effects更新它的child
// 相反 我们会在渲染之前将它们全部添加到子节点
// 这意味着不需要对side-effects进行跟踪 以优化这个reconciliation过程
workInProgress.child = mountChildFibers( // -> ChildReconciler(false)
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
workInProgress.child = reconcileChildFibers( // -> ChildReconciler(true)
workInProgress,
current.child, <-- 更新回传入child 以跟踪side-effects
nextChildren,
renderExpirationTime,
);
}
}

ChildReconciler是个巨折腾的函数。v16好像就突然一下从基于class的编程转换成了基于function的编程偏好。如果说beginWork是在root处处理了N种不同打Fiber节点,那么ChildReconciler就是在child处处理了这些节点。好在这里遵循常规线路,初始render也不需要太过于深入这些,常规的classComponent这里回一路走到(建议跟着断点调试)

1
return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, expirationTime));

placeSingleChild这个在初始render里面没啥用,接到什么返回什么不做处理。

reconcileSingleElement这个函数感觉相对简单,但是它是给初次没渲染过得Fiber链表构建一个完整链表『树』到关键。简单提一下 忽略无关分支。但是可以想见,这个函数肯定是一个递归的调用,毕竟他要构建一棵树。

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
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) { // 此时child === null 不会进来
// 略
}

if (element.type === REACT_FRAGMENT_TYPE) { // 此时常规情况不满足分支进入条件
// 略
} else {
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}

关于这个createFiberFormElement环节的核心是:

1
2
3
4
fiber = createFiber(fiberTag, pendingProps, key, mode);
fiber.elementType = type;
fiber.type = resolvedType;
fiber.expirationTime = expirationTime;

大致结果就是:

1
2
3
4
5
6
7
8
9
10
11
{
...DefaultFiberVals,
tag: fiberTag
elementType: ClassComponet(),
pendingProps,
key,
mode,
elementType,
type,
expirationTime
}

此时type和ElementType没什么区别,不过后面特殊的functionComponent等情况肯定会有变化。

初始render闭环

这里需要一个闭环分析。我们生成了一个模拟树的fiber链表,但是这里过于深入细节,需要跳出来,完成一场宏观层面的调用观摩,来看看这个『树』结构是如何完成闭环的——这个闭环这里是指fiber节点的递归生成和链接。

但是这里还不够,我们还有一个环节没有讲到,所以闭环暂时无法完成,那就是completeUnitOfWork函数还没有说完。

所以,稍候。

completeUnitOfWork

这个函数因为太过于深入beginWork所以导致印象缺失有些,我们回头看看beginWork上面提到的workLoop相关的代码。当可以有一些印象以便继续分析阅读。这里关注点,放到performUnitOfWork函数。我们把相关东西合并来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function workLoop () {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}

function performUnitOfWork(workInProgress: Fiber): Fiber | null {
const current = workInProgress.alternate;

startWorkTimer(workInProgress);

let next;

next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;

if (next === null) {
next = completeUnitOfWork(workInProgress);
}

ReactCurrentOwner.current = null;
return next;
}

假如说,performUnitOfWork,beginWork是对firstChild的遍历,那么completeUnitOfWork则是对nextSibling的遍历。原本想将这个函数仔细剖析一下,不过这里终究功力尚且不够,最终简化出来的居然和理论那块几乎一样。所以就只扯一扯主要脉络。

分析preformUnitOfWork这块的设计理念。

preformUnitOfWork函数的设计思路是: 通过beginWork对每个给定的root节点进行firstChild深挖,然后完成操作返回对应的最深层级的firstChild,接下来使用completeUnitOfWork处理这一层的nextSibling节点,如果nextSibling存在,递归调用preformUnitOfWork处理child(为了方便理解,最好假设下面没有child了),否则这一层级已经处理完毕,返回上一级节点。

返回上一级节点这里还有一些门道,因为这个返回走的completeUnitOfWork逻辑,它这里有判断返回的上一级节点是否有nextSibling节点,如果有就返回,如果没有回直接继续往上回溯直到找到有nextSibling的更上层级的节点。

然后继续child处理流程。

最后根据这套逻辑,所有的节点都会被遍历到直到最后返回nextUnitOfWork === null

渲染闭环(commit阶段)

不妨回到开头再看看「Render线路-初始化」小结的调用栈。我们顺着之前整理的调用栈的末节点,加上后来的源码分析,来继续展开:

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
workLoop () {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork) {
next = beginWork()
->updateHostRoot(){
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
nextChildren = nextState.element;
->reconcileChildren()
=>workInProgress.child = mountChildFibers() {
->ChildReconciler.reconcileChildFibers()
=>placeSingleChild(
reconcileSingleElement( returnFiber, currentFirstChild, newChild, expirationTime)
)
=>reconcileSingleElement( returnFiber, currentFirstChild, newChild, expirationTime)
=>createFiberFromElement( element, returnFiber.mode, expirationTime)
->createFiber() -> new FiberNode(tag, pendingProps, key, mode)
=>FiberNode
}
}
if (next === null) {
next = completeUnitOfWork(workInProgress);
}
}
}
}
onComplete(root, rootWorkInProgress, expirationTime)

这里具体了很多。但是实质上,我们并没有逃脱最前面提到的遍历理论部分的伪代码。

细节虽然深入了一些。但是反而更多细节没有没有暴露出来。这一篇的前置的理论是必须理解链表,以及基于链表理论构建的Fiber节点。

如果说v15基础节点是ReactNode,那么v16的基础节点就是Fiber。ReactNode有children,children不断伸展就是一个完整的树。但是v16里面采用了Fiber做基础,有sibling,child,return属性,基于这些属性,它可以以链表的形式完成遍历。

在v15里面,如果要完成一个root挂载,只需要最外层compositeComponent进行了mount后面就会递归执行mount所有的children,最后执行patch完成vdom到dom过程。

但是v16这里,需要走一遍链表的遍历理论才能完成整体挂载,最后执行类似的patch实现vdom->dom。

这里commit阶段的调用在performWorkOnRoot函数里面后面的renderRoot后面的completeRoot里面。

当我们将Fiber节点处理好,就可以开始进行commit了。

completeRoot

关于这块的调用栈:

1
2
3
4
5
6
7
completeRoot
-> commitRoot () {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}

commitBeforeMutationLifecycles

1
2
3
4
5
6
7
8
9
10
11
12
function commitBeforeMutationLifecycles() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & Snapshot) {
recordEffect();
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);
}

nextEffect = nextEffect.nextEffect;
}
}

这个函数会在遍历nextEffect链表过程中,针对每个Side-Effect执行commitBeforeMutationLifeCycles。这个函数就常规的ClassComponent来说就是调用getSnapshotBeforeUpdate函数。

commitAllHostEffects

这个函数会在遍历nextEffect链表过程中,针对Side-Effect执行它。它主要是对React执行DOM更新。将渲染好的DOM放到指定位置去。这里可能是整个patch核心的地方。

commitAllLifecycles

生命周期处理。关于生命周期处理其实挺无趣的,参考之前的文章也可以明白。

它确实让API变得好用,但是实质上也确实就是在不同时机进行了不同调用。仅此而已。

其他补充

这里着重补充一下可能被忽略掉基础性细节。主要是FiberNode节点及其相关。

FiberNode.updateQueue: 更新队列链表

FiberNode.alternate: 节点副本,nextUnitOfWork经常会指向它,他也是FiberNode。

FiberNode.memoizedState: 当前节点已经生效显示到UI上的state

FiberRoot.finishedWork是变化节点树。如果root.finishedWork不等于null,那么说明render阶段完成,可以进入commit阶段。

★workInProgress会组成由链表组成的一棵树。如果对这个树如何构建,可以多仔细揣摩reconcileChildren&ChildReconciler,它主要是通过workInProgress.child = createFibler(args),createFibler对应的前置调用reconcileSingleElement函数里面做了return配对,来实现父子链接。至于sibling也在ChildReconciler有处理。(当然这也只是其中一部分逻辑分支,感觉这个树的构建值得再写一篇,后面再看看吧)。

参考

这篇参考文章为个人提供了很重要的参考。如果个人这篇文章看不太懂建议看看大佬的。将我这篇作为补充也未尝不可。

[Inside Fiber: in-depth overview of the new reconciliation algorithm in React