Que's Blog

WebFrontEnd Development

0%

react-v16-ChildReconciler

初步整理

这里还需要回顾一下,FiberNode遍历DOM树的数据结构和遍历基础。

遍历理论在上一篇v16-render里面有简略伪代码可以供参考。它依托FiberNode得三个属性:child,return,sibling来实现。如果对这个理论不了解必须回上一篇看看理论。

链表构建

这里链表的构建主要就是child,return,sibling三个属性的构建。

这个构建过程在reconcileChildren函数及其调用函数中。

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
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(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
// 这部分是更新相关
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderExpirationTime,
);
}
}

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

可以看到不管如何,最终都会调用到ChildReconciler。两者区别是初始更新和Update的区别。

接下来是ChildReconciler函数代码:

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
function ChildReconciler(shouldTrackSideEffects) {
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}

// Handle object types
const isObject = typeof newChild === 'object' && newChild !== null;

if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
case REACT_PORTAL_TYPE:
return placeSingleChild(
reconcileSinglePortal(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
}
}
// 如果child是字符串或者数字
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 处理文本节点逻辑
}

if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
);
}

if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
);
}

if (isObject) {
throwOnInvalidObjectType(returnFiber, newChild);
}

if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
// newChild如果为空 做一些初始化操作
}

// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}

return reconcileChildFibers;
}

这里对应的大场景有两个:

  • 一个父节点有单一的子节点
  • 一个父节点有多个子节点

单一子节点构建

这其中不管是那种情况,都必然会有child & return的设置。我们来看看调用栈。

首先是常规的单一的子节点。此时面对的情况是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
expirationTime,
),
);
case REACT_PORTAL_TYPE:
// PORTAL组件略
}
}
// 调用栈
placeSingleChild(reconcileSingleElement(args))
->reconcileSingleElement(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
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
expirationTime: ExpirationTime,
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// 初始化渲染这里可以不管
}

if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
element.key,
);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(
element,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}

此时配合这里的created.return赋值语句,整个具体场景里面的return和child就做好了配对。

多节点配对

多节点配对的核心代码是:

1
2
3
4
5
6
7
8
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
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
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
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
expirationTime: ExpirationTime,
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;

let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// Element Diff算法实现
// 这里是更新相关的Diff算法 可以参考之前的旧文章 虽然实现变了 算法思想没变
}
// 标记批量删除 如果新的children.length === 0 就整个标记删除 和之前算法思路一致
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}

if (oldFiber === null) {
// If we don't have any more existing children we can choose a fast path
// since the rest will all be insertions.
// 遍历newChildren,缓存上一次的childrenItem,标记当前newFiber为它的sibling
// 并将最上方的childrenItem缓存以便遍历后返回
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime,
);
if (!newFiber) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}

// 后方代码略 初始render只到上一个return就结束了
}

关键地方的构建这里都在注释里面有写了。

当children是数组,那么遍历,看时机将sibling绑定到下一个同级元素。结束后将firstChild返回。此时workInProgress.child就被绑定到了这个firstChild上了。

那么新的childrenItem上的return呢?它们在createChild里面被绑定好了。买个元素的return都指定到了workInProgress节点。

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
function createChild(
returnFiber: Fiber,
newChild: any,
expirationTime: ExpirationTime,
): Fiber | null {
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes don't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
const created = createFiberFromText(
'' + newChild,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}

if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
const created = createFiberFromElement(
newChild,
returnFiber.mode,
expirationTime,
);
created.ref = coerceRef(returnFiber, null, newChild);
created.return = returnFiber;
return created;
}
case REACT_PORTAL_TYPE: {
const created = createFiberFromPortal(
newChild,
returnFiber.mode,
expirationTime,
);
created.return = returnFiber;
return created;
}
}

if (isArray(newChild) || getIteratorFn(newChild)) {
const created = createFiberFromFragment(
newChild,
returnFiber.mode,
expirationTime,
null,
);
created.return = returnFiber;
return created;
}

throwOnInvalidObjectType(returnFiber, newChild);
}

return null;
}

这里分支有点多,但是只需要关注return的赋值即可。

递归&串联

我们就仅有两层的树进行了分析。那么我们如何将多个层级递归串联起来,让它们一直从顶部走到终点呢?答案当然是workLoop!

如果对workLoop还有陌生感觉,那么不妨重新区看看。workLoop循环结束的唯一条件是FiberNode.child===null。只要child不为null,他会按照算法涉及一路next或者child遍历下去。

小总结

到这里,我们的链表构建就结束了。我们清晰的看到了,这个链表是如何完整架构的。但是这里还是不太够,我们对current和workInProgress树的流程还缺乏一个整体认知。

Fiber里面的树

workInProgress树

这棵树是有迹可循的。

实质上,构建workInProgress树其实就是链表构建的更深入的探究。它和上一节其实密不可分。

换句话说,上一节其实也只是构建workInProgress过程之一。所以,两部分参考着看是很有意义的。

创建

第一个创建是在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
nextUnitOfWork = createWorkInProgress(
nextRoot.current,
null,
nextRenderExpirationTime,
);
export function createWorkInProgress(
current: Fiber,
pendingProps: any,
expirationTime: ExpirationTime,
): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;

current.alternate = workInProgress;
} else {
// 更新分支 略
}

workInProgress.childExpirationTime = current.childExpirationTime;
workInProgress.expirationTime = current.expirationTime;

workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
workInProgress.contextDependencies = current.contextDependencies;

workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;

if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}

return workInProgress;
}

关于这个源头我们穷根究底一下。

  • ReactDom.render(App, container)首先获取到的root是一个App经过createElement函数返回的结果。举个栗子:

    1
    2
    3
    4
    5
    6
    7
    8
    {
    $$typeof: Symbol(react.element)
    key: null
    props: {}
    ref: null
    type: ƒ App()
    _owner: null
    }
  • 后续引用到的ReactRoot构造器根据给出的container元素构造了一个Fiber节点。这事所有Fiber节点的root。前面提到的nextRoot也是它。

    1
    2
    var root = createContainer(container, isConcurrent, hydrate);
    this._internalRoot = root;

    这个createContainer设置了current,current由createHostRootFiber函数创建了一个FiberNode,除了tag==3之外和空的FiberNode没有什么区别

  • createWorkInProgress环节。

    此时,workInProgress.alternate = current.alternate = current = createHostRootFiber()基本都是空Fiber,只不过设置了tag===3。

初始化render里的变更

接下是workLoop+performUnitOfWork了。performUnitOfWork引用了beginWork。

根节点

当根节点进入这个beginWork分支。触发return updateHostRoot(current, workInProgress, renderExpirationTime),也就是nextUnitOfWork变量初始化后第一次获得返回值。

这个函数中有这样一个调用栈:

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

const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState !== null ? prevState.element : null;
// processUpdateQueue这个函数非常重要 它可能是将FiberNode树和ReactElement联系起来的核心入口
processUpdateQueue(
workInProgress,
updateQueue,
nextProps,
null,
renderExpirationTime,
);
const nextState = workInProgress.memoizedState;
const nextChildren = nextState.element;
if (nextChildren === prevChildren) {
// 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 (
(current === null || current.child === null) &&
root.hydrate &&
enterHydrationState(workInProgress)
) {
workInProgress.effectTag |= Placement;

workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
} else {
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
resetHydrationState();
}
return workInProgress.child;
}

这里processcessUpdateQueue函数在上一篇render里面有相对详细的解读。这里关注点是内部引用:

1
2
3
4
5
6
7
8
9
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
workInProgress.memoizedState = resultState;

这里getStateFromUpdate函数为空白的workInProgress注入了props数据。它更新了workInProgress.memoizedState,就这里而言,它将{element: reactElement}的数据结构赋值过去了。

关于这个变量,参考scheduleRootUpdate函数里面的:

1
2
const update = createUpdate(expirationTime);
update.payload = {element};

让我们把关注点继续返回updateHostRoot函数,此时这个函数结束之后,正常情况会进入以下分支:

1
2
3
4
5
6
7
8
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
resetHydrationState();
return workInProgress.child;

因为返回的是workInProgress.child,所以这里重点关注一下reconcileChildren对workInProgress.child的处理。这里可以重新返回之前的「链表构建」来看。

子孙节点

结合我们到链表构建这一小节。到此为止,我们可以做出这样的论断:

nextUnitOfWork实质上就是当前遍历过程中要处理的节点,通过我们的workLoop遍历,当这个工作到最终节点的时候,我们就完成了整个workProgeress树的构建。

不过这里稍微往下走走,当我们FiberRoot处理完毕,beginWork第二次运行时候应该是怎样的场景呢?此时返回的第二个FiberNode节点就是App组件对应的节点。这里关注点核心属性:

1
2
3
4
5
6
{
effectTag: 2
elementType: ƒ App()
type: ƒ App()
return: FiberRoot
}

此时走到beginWork.就会进入如下分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case IndeterminateComponent: {
const elementType = workInProgress.elementType;
return mountIndeterminateComponent(
current,
workInProgress,
elementType,
renderExpirationTime,
);
}
function mountIndeterminateComponent () {
// 此时核心调用如下 省略多数无关代码
// 它返回了一个ReactElement,这是一颗完整的VDOM树,下级已经全部展开为ReactElement
// 就常规ClassComponent初始render,它相当于return Component(props, refOrContext)
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderExpirationTime,
);
reconcileChildren(null, workInProgress, value, renderExpirationTime);
return workInProgress.child;
}

接下来就是reconcileChildren。关于这个函数可以看看上面提过的「单一子节点构建」部分。总之这里对workProgress有一些修改。最后它将child设为了下面分支返回值:

1
2
3
4
5
6
const created = createFiberFromFragment(
element.props.children,
returnFiber.mode,
expirationTime,
element.key,
);

最后就是返回了FiberNode。这个FiberNode根据参数设定了以下非null值:

1
2
3
4
5
pendingProps: children 完整VDOM树
tag: Fragment = 7
expirationTime: expirationTime
key: element.key
mode: returnFiber.mode

这里贯彻一个重点,那就是这个FiberNode是下一个nextUnitOfWork变量,如果对这个概念不熟,那么上一篇需要回看一下。总之,这里脉络是reconcileChildren函数为当前FiberNode构建了child值并返回这个child。

这个child上的pendingProps是workLoop能够持续加下去的核心所在。假设一下,当App里面内容是这样的:

1
2
3
4
5
6
7
8
9
function App() {
return (
<div className="App">
<header className="App-header">
<div>Test</div>
</header>
</div>
);
}

beginWork这里分支判断主要靠fiberNode.tag判断。我们这里思考一下。这个App构建下来,会有多少个FiberNode, tag分别是?

答案是: 这里一共会有五个FiberNode, 组成一个workProgress链表树。对应的tag只分别是3、2、5、5、5。换成Enum值,那就是: HostRoot、IndeterminateComponent、HostComponent、HostComponent、HostComponent。很显然,ReactDomComponent都被设置为tag=HostComponent了。

那么问题来了,这里需要将tag和children(ReactElement)联结起来。这里是通过一系列的createFiberFromXXX函数做到的(FiberRoot除外)。而具体采用哪个函数,则取决于ReactElement.type。这里常规(ReactDomComponent)是使用createFiberFromElement函数。

就常规来说,如果type === REACT_FRAGMENT_TYPE则使用createFiberFromFragment,否则使用createFiberFromElement,后者是常规情况。

搞明白前面这些,现在我们可以认认真真看看,children是怎样一路层层拆包,只到遍历完毕为止的。这事workLoop和VDOM的关联脉络所在。

认真观察createFiberFromElement

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
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
expirationTime: ExpirationTime,
): Fiber {
let owner = null;
const type = element.type;
const key = element.key;
const pendingProps = element.props; // 关注点1
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
expirationTime,
);
return fiber;
}
// 这里补充updateHostComponent函数简略代码以加深了解
function updateHostComponent(current, workInProgress, renderExpirationTime) {
let nextChildren = nextProps.children; // 关注点2
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}

可以看到,这里我们通过这个函数里面这一句const pendingProps = element.props将整个VDOM树层层解包。最终完成了整个FiberNode树的构建&链接。

同时,VDOM和FiberNode的关系这里也有了清晰认知。

finishedWork树 && current树

renderRoot函数在workLoop结束后有以下处理:

1
2
3
4
5
6
7
8
9
10
11
12
const rootWorkInProgress = root.current.alternate;
onComplete(root, rootWorkInProgress, expirationTime);

// 关联引用
function onComplete(
root: FiberRoot,
finishedWork: Fiber,
expirationTime: ExpirationTime,
) {
root.pendingCommitExpirationTime = expirationTime;
root.finishedWork = finishedWork;
}

root.current.alternate显然是root.current的一个副本。

关于这个root上下文逻辑。不妨在本文内搜索workInProgress.alternate = current.alternate = current = createHostRootFiber()看看。当renderRoot结束后,它的child指向了我们的App节点。最终的情况就是: 当初始化渲染render阶段完成后,finishedWork == current == FiberRoot。

就整个流程来说。React在初次完成后,都会拥有一个current树,它的内部数据对应到整个UI上。如果后续有更新rootWorkInProgress树会被重新构建。

updateQueue树

updateQueue也是一个链表,不过它和FiberNode不一样,只有next一个链接属性。

updateQueue的脉络还是得从render这里入手。回顾之前的render文章,看看调用栈。我们可以发现,初始render第一次处理它,是在scheduleRootUpdate函数中的enqueueUpdate(current, update)

这里针对FiberRoot设置的updateQueue是一个初始值。里面的callback值来源于上级调用。它在ReactRoot.prototype.render函数中。

1
2
const work = new ReactWork();
updateContainer(children, root, null, work._onCommit);

配合processUpdateQueue函数,可以很容易知道fiberRoot上的初始updateQueue是一个callback设为work._onCommit的初始值。

Side-Effect树

我们可以将 React 中的一个组件视为一个使用 state 和 props 来计算 UI 表示的函数。改变 DOM 或调用生命周期方法,被视为Side-Effect。

在v15版本中DOM树通过它的_owner属性是否为null来判断是否是一个自定义组件。它的更新纯粹是判断props是否有变更来处理后续生命周期&渲染。v16这里则是在FiberNode上记录了一个nextEffect属性来标记下一个自定义组件——v16叫类组件,它里面有生命周期之类的Side-Effect操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enqueueSetState(inst, payload, callback) {
// 此时inst为App实例 payload={text: 'Hello World'} callback=undefined
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);

const update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},

这里关系到update的实质上就是enqueueUpdate(fiber, update),说白了,这里update主要还是设置一个expirationTime, fiber节点上的更新队列才是实质核心。

这里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;
}
}

仔细想想,在v15进行Diff阶段,实质上是进行Diff对比中的ComponentDiff环节。这个环节如果props变化不会直接生成新的替换下级所有旧的节点。这里换了链表,但是这个对比环节还是需要依据。这里想来就是nextEffect属性存在的意义了。它定位了所有ClassComponent方便进行ComponentDiff。

但是需要注意的是: 这个链表的顺序,它不是按层级做顺序链接的。而是基于我们的FiberNode遍历理论,在这个遍历过程中,遇到ClassComponent时候,按照先后顺序进行连接、遍历。这里偷懒偷个图。。。

这里关于组件对比的想法仅作为猜想了,后面看Update环节时候再做确认。这里疑问是,当这个next不再以DOM层级作为依据,底部组件可能比上层组件更先遍历到,那么问题在于如果下级和上级同时变化了,如果下级先处理一些事情会不会造成浪费?

这里问题暂且留到Update环节分析。

image-20190726144234693