前言
在前面的文章中,我们分析初次渲染的具体流程
接下来我们将着重于分析各种触发渲染更新的操作、更新时的diff流程、更新时联动hooks刷新的逻辑
文章内容概述
- 分析
useReducer
的相关源码,了解任务的创建以及更新相关流程 - 分析
useState
的相关源码,了解任务的创建以及更新相关流程 - 以
useState
为基础,对整个更新流程进行简单的分析 - 更新流程中的diff算法进行简单描述,侧重于各种
flags
的标记以及对应的更新方法 - 分析其它常见的
useXXX
的相关源码
workInProgress全局变量的赋值情况??很多地方都有current以及workInProgress,它们的关系是怎么样的?
文章要解决的问题
update
、lane
、task
之间的关系,它们是如何配合进行调度更新的?
有多种更新?元素更新?function更新?还有state更新??
1. useReducer
const [reducerState, dispatch] = React.useReducer(reducer, {age: 42});
1.1 初始化mountReducer
在FunctionComponent
类型fiber
的beginWork()
中,我们会触发
mountIndeterminateComponent()
renderWithHooks()
在renderWithHooks()
我们会设置全局变量currentlyRenderingFiber$1
为当前的fiber
function beginWork(current, workInProgress, renderLanes) {
didReceiveUpdate = false;
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case IndeterminateComponent: {
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
}
}
}
function mountIndeterminateComponent(...) {
value = renderWithHooks(...);
//...
}
function renderWithHooks() {
renderLanes = nextRenderLanes;
currentlyRenderingFiber$1 = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// function App(_ref) {
// var _React$useReducer = React.useReducer(reducer, { age: 42 })
// return React.createElement(
// "div",
// ...
// );
// }
var children = Component(props, secondArg); // workInProgress.type
renderLanes = NoLanes;
currentlyRenderingFiber$1 = null;
return children;
}
然后触发Component()
,也就是FunctionComponent()
中实际的内容,全部执行一遍,然后return
对应的React.createElement(...)
作为fiber
赋值给children
根据当前
current
去切换HooksDispatcherOnMount
/HooksDispatcherOnUpdate
,对应不同的方法,因此初始化React.useReducer
=mountReducer
,而更新时React.useReducer
=updateReducer
当我们在代码中有React.useReducer()
时,会触发mountReducer()
,如下面代码所示,在我们示例中,传入的
initialArg
:{age: 42}
init
:undefined
因此我们会根据initialArg
初始化对应的值,然后根据赋值hook
相关属性,包括
memoizedState
baseState
queue
:包括pending
、lanes
、dispatch
、lastRenderedReducer
、lastRenderedState
function useReducer(reducer, initialArg, init) {
var dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArg, init);
}
function mountReducer(reducer, initialArg, init) {
var hook = mountWorkInProgressHook();
var initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg;
}
hook.memoizedState = hook.baseState = initialState;
var queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState,
};
hook.queue = queue;
var dispatch = (queue.dispatch = dispatchReducerAction.bind(
null,
currentlyRenderingFiber$1,
queue
));
return [hook.memoizedState, dispatch];
}
1.1.1 fiber.memoizedState单链表结构存储hooks
从初始化mountWorkInProgressHook()
方法可以知道,hook
本身有一个memoizedState
属性,fiber
本身也有一个memoizedState
属性,不同的是
hook.memoizedState
存储的是state
当前的值fiber.memoizedState
存储的是当前fiber
(比如一个FunctionComponent
类型的fiber
)中的所有hook
的第一个节点
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
最终fiber.memoizedState
=代码中第1个useReducer
+代码中第2个useReducer
+...
注:
workInProgressHook
表示当前正在初始化的hook
,不是workInProgress
!!是两个不同的变量
1.1.2 hook 的更新方法初始化
当前currentlyRenderingFiber$1
为FunctionComponet
代表的fiber
,queue
代表的是当前fiber
中其中一个hook
的queue
var queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState,
};
hook.queue = queue;
var dispatch = (queue.dispatch = dispatchReducerAction.bind(
null,
currentlyRenderingFiber$1,
queue
));
function dispatchReducerAction(fiber, queue, action) {
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null,
};
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
1.2 dispatch创建更新
当我们在onClick()
方法中触发dispatch()
的时候,我们会进行reducer
的调用,触发更新操作
我们触发了两次
dispatch1
,因此创建了两个update
!!
function reducer(state, {type}) {
if (type === "incremented_age") {
return {
age: state.age + 1
};
}
throw Error('Unknown action.');
}
const reducerJsx = (
<React.Fragment>
<span>reducer现在是:{reducerState.age}</span>
<div>
<button onClick={() => {
dispatch1({type: "incremented_second"});
dispatch1({type: "incremented_second"});
}}>
reducerState点击增加1
</button>
</div>
</React.Fragment>
);
从
hook的更新方法初始化
可以知道,dispatch()
实际上就是dispatchReducerAction()
,因此涉及到两个问题
dispatchReducerAction()
执行了什么?- 什么时候调用
reducer()
方法?以及发生了什么?
1.2.1 创建update并触发调度
从下面代码可以知道,主要分为几个步骤:
- 创建
update
- 将
update
放入到队列中:enqueueConcurrentHookUpdate()
- 处理队列中的
update
:scheduleUpdateOnFiber()
function dispatchReducerAction(fiber, queue, action) {
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null,
};
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
1.2.1.1 传入值fiber、queue、action分析
fiber
:当前FunctionComponet
代表的fiber
queue
:当前hook.queue
action
:当前hook
所产生更新传入的参数,比如外部调用setState({type: "a"})
,那么action
={type: a}
1.2.1.2 创建update
使用requestUpdateLane()
获取当前fiber
的lane
,然后构建update
对象
1.2.1.3 将update放入队列中enqueueConcurrentHookUpdate()
将当前hook
创建的update
压入concurrentQueues
队列中,然后返回HostRoot
这里的
queue
是上面初始化mountReducer()
构建dispatch
更新方法时创建的hook.queue
,对于同一个hook
的dispatch
更新方法多次调用,拿到的都是同一个fiber
和queue
,由于示例创建了两个update
,这里压入了两次队列
注意:此时的 update 产生的 lane 已经合并到对应的 fiber 数据中
function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
var concurrentQueue = queue;
var concurrentUpdate = update;
enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
return getRootForUpdatedFiber(fiber);
}
function enqueueUpdate(fiber, queue, update, lane) {
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;
concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane);
fiber.lanes = mergeLanes(fiber.lanes, lane);
var alternate = fiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
}
function mergeLanes(a, b) {
return a | b;
}
function getRootForUpdatedFiber(sourceFiber) {
var node = sourceFiber;
var parent = node.return;
while (parent !== null) {
node = parent;
parent = node.return;
}
return node.tag === HostRoot ? node.stateNode : null;
}
1.2.1.4 开始调度scheduleUpdateOnFiber()
在我们首次渲染的文章中,我们已经详细分析了scheduleUpdateOnFiber()
的流程,就是触发
function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
//...
markRootUpdated(root, lane, eventTime);
ensureRootIsScheduled(root, eventTime);
}
function ensureRootIsScheduled(root, currentTime) {
var existingCallbackNode = root.callbackNode;
markStarvedLanesAsExpired(root, currentTime);
var nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
if (nextLanes === NoLanes) {
//...没有任务直接返回
return;
}
// getHighestPriorityLane = 16
var newCallbackPriority = getHighestPriorityLane(nextLanes);
if (existingCallbackNode != null) {
cancelCallback$1(existingCallbackNode);
}
var newCallbackNode;
var schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DefaultEventPriority:
schedulerPriorityLevel = NormalPriority;
break;
//...
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
1.2.2 调度中处理队列finishQueueingConcurrentUpdates()
从下面三段代码可以看出,renderRootSync()
->prepareFreshStack()
->finishQueueingConcurrentUpdates()
而finishQueueingConcurrentUpdates()
做了两件事:
- 从
concurrentQueues
取出queue
、update
、lane
,将queue
与update
进行关联! - 触发
markUpdateLaneFromFiberToRoot()
将lane
向rootFiber
冒泡
?????后续我们需要根据root.childLanes
取出优先级最高的lane
,创建对应的task
进行
我们从下面可以知道,最终
update
放入到queue.pending
中,如果有多个update
(相同hook
触发),那么会形成一个循环单链表数据(尾部节点指向头部节点)
function renderRootSync(root, lanes) {
var prevExecutionContext = executionContext;
executionContext |= RenderContext;
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
//...
workInProgressTransitions = getTransitionsForLanes(); // enableTransitionTracing=false,返回null
prepareFreshStack(root, lanes);
}
workLoopSync();
executionContext = prevExecutionContext;
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
function prepareFreshStack(root, lanes) {
//...
workInProgressRoot = root;
var rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes =
subtreeRenderLanes =
workInProgressRootIncludedLanes =
lanes;
//...
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
本质就是将某一个 fiber 产生的所有更新操作都整理为一个链表结构,然后赋值到 queue 中,而 queue 就是 fiber.memoizedState 中的某一个属性!
因此本质还是 将 某一个 fiber 产生的所有更新与 fiber 进行互相关联
function finishQueueingConcurrentUpdates() {
var endIndex = concurrentQueuesIndex;
concurrentQueuesIndex = 0;
concurrentlyUpdatedLanes = NoLanes;
var i = 0;
while (i < endIndex) {
var fiber = concurrentQueues[i];
concurrentQueues[i++] = null;
var queue = concurrentQueues[i];
concurrentQueues[i++] = null;
var update = concurrentQueues[i];
concurrentQueues[i++] = null;
var lane = concurrentQueues[i];
concurrentQueues[i++] = null;
if (queue !== null && update !== null) {
var pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
if (lane !== NoLane) {
markUpdateLaneFromFiberToRoot(fiber, update, lane);
}
}
}
function markUpdateLaneFromFiberToRoot(sourceFiber, update, lane) {
// Update the source fiber's lanes
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
var alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
//...
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
//...
node = parent;
parent = parent.return;
}
//...
}
1.3 触发全量渲染
处理fiber-updateFunctionComponent()
当scheduleCallback()
触发调度时,会从root
开始遍历所有节点触发重新渲染,从而触发FunctionComponent
的beginWork()
此时beginWork()
触发的是updateFunctionComponent()
,从而再次触发renderWithHooks()
function beginWork(current, workInProgress, renderLanes) {
didReceiveUpdate = false;
workInProgress.lanes = NoLanes;
switch (workInProgress.tag) {
case FunctionComponent: {
var Component = workInProgress.type;
//...
return updateFunctionComponent(...);
}
}
}
function updateFunctionComponent() {
nextChildren = renderWithHooks(...);
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
而此时的useReducer()
不再是mountReducer
,而是updateReducer()
1.3.1 updateReducer()
- 先使用
updateWorkInProgressHook()
构建出hook
对象,从hook
中取出queue
- 将
queue.pending
,也就是update
对象(根据finishQueueingConcurrentUpdates()
分析)赋值到baseQueue
中 - 由于
queue.pending
拿到的是hook
更新(多个更新update操作会形成一个循环单向链表
)的最后一个节点,因此baseQueue.next
可以拿到头节点,从头节点开始遍历整个链表,不断拿出action
(也就是示例中dispatch1({type: "incremented_second"})
的{type: "incremented_second"}
),触发reducrer(上一次reducer计算出来的state,传入的数据)
来获取新的state
值
当
update
===first
时,说明已经update
从first
遍历到first
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
queue.lastRenderedReducer = reducer;
var current = currentHook;
var baseQueue = current.baseQueue;
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
var first = baseQueue.next;
var newState = current.baseState;
var update = first;
do {
var action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
if (baseQueue === null) {
// 不需要更新
queue.lanes = NoLanes;
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
1.3.1.1 updateWorkInProgressHook()
- 先使用
currentlyRenderingFiber$1.memoizedState
获取当前渲染tree的头节点,如果该头节点不为空,则不断重用复用以该头节点构建的链表,不断nextWorkInProgressHook
=workInProgressHook.next
workInProgressHook
=nextWorkInProgressHook
注:这里不是一个循环while,很多局部变量用一次就废弃了,比如
nextWorkInProgressHook
,每次进入updateWorkInProgressHook()
都要重新赋值的,因此下面代码对比源码去除了无用的代码赋值
- 而
currentlyRenderingFiber$1.memoizedState
可能为空,因此我们可以复用current
=currentlyRenderingFiber$1.alternate
- 使用
current.memoizedState
所代表的链表去复制一个新的newHook
,然后赋值给currentlyRenderingFiber$1.memoizedState
和workInProgressHook
,此时currentHook
代表着两棵tree相同位置对应的hook
代码(useXXX()
) - 在下一次触发
updateWorkInProgressHook()
时,如果currentlyRenderingFiber$1.memoizedState
所代表的链表还是为空,则继续复用alternate
,也就是currentHook.next
去复制出新的newHook
,然后workInProgressHook.next
=newHook
currentlyRenderingFiber$1.memoizedState
可能为空发生在第一次更新??因为双缓冲树只是在mount构建了其中的一棵。然后第一个更新,会切换到新的tree,此时memoizedState
为空currentlyRenderingFiber$1.memoizedState不为空则发生在第二次~以后的更新??
经过多次更新尝试,每次currentlyRenderingFiber$1.memoizedState都为空!!每次都需要构建新的newHook,太奇怪了...暂时放下,再找找资料
function updateWorkInProgressHook() {
var nextCurrentHook;
if (currentHook === null) {
var current = currentlyRenderingFiber$1.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
currentHook = nextCurrentHook;
} else {
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
上面的代码过于繁琐和难以看懂,我们可以简化为下面代码
因为是更新阶段,一般alternate是存在的(可能存在之前没有hook,更新后又有hook,那么alternate就不存在)
这里考虑一般情况下=>可以简化代码逻辑,容易理解
由于是更新阶段,因此currentlyRenderingFiber$1.alternate
必定存在,复用当前alternate
的memoizedState
构建链表数据,主要是 头节点的赋值 + 剩余节点的赋值 两个步骤
涉及到三个全局变量的赋值:
currentHook
:代表的是当前renderFiber.alternate
对应的hook
currentlyRenderingFiber.memoizedState
:当前renderFiber.alternate
复制的单链表workInProgressHook
:当前renderFiber
对应的hook
fiber.memoizedState
跟hook.memoizedState
是不一样的!!
function updateWorkInProgressHook() {
const current = currentlyRenderingFiber.alternate;
// 更新模式会存在current
currentlyRenderingFiber.memoizedState = current.memoizedState;
if (workInProgressHook === null) {
// 头节点还没赋值
workInProgressHook = currentlyRenderingFiber.memoizedState;
currentHook = current.memoizedState;
} else {
workInProgressHook = workInProgressHook.next;
currentHook = currentHook.next;
}
return workInProgressHook;
}
2. useState
2.1 初始化mountState
从上面mountReducer()
的分析可以知道,我们会从ClassComponent
的beginWork()
开始触发,然后进行useState()
的执行,初始化阶段useState()
就是mountState()
,与mountReducer()
一样
- 使用
mountWorkInProgressHook()
构建一个hook
对象 - 然后进行
initialState
的初始化,因为可能是function
,因此执行function()
获取初始的state
值 - 然后初始化
hook.queue
以及hook.dispatch
方法
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === "function") {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
var dispatch = (queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber$1,
queue
));
return [hook.memoizedState, dispatch];
}
2.2 dispatchSetState创建更新
2.2.1 创建update->压入队列->触发调度
- 创建
update
对象 - 判断是否有上一次更新的值,如果有旧的值,比对两个值,如果没有变化则不会将
update
加入到队列中,也就是阻止没有改变的值重复进行渲染更新,当然lane
也不会更新到root
节点中 - 如果有变化,则加入队列中
enqueueConcurrentHookUpdate
- 然后开始调用
scheduleUpdateOnFiber()
上面的流程跟
useReducer()
相比较,只是多了一步新旧值的比对,其他核心逻辑几乎是一致的!
function dispatchSetState(fiber, queue, action) {
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null,
};
var alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
var lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action);
if (objectIs(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return;
}
}
}
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
var eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
}
2.2.2 调度中处理队列finishQueueingConcurrentUpdates()
function renderRootSync(root, lanes) {
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
//...
workInProgressTransitions = getTransitionsForLanes();
prepareFreshStack(root, lanes);
}
workLoopSync();
return workInProgressRootExitStatus;
}
function prepareFreshStack(root, lanes) {
//...
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
function finishQueueingConcurrentUpdates() {
var endIndex = concurrentQueuesIndex;
concurrentQueuesIndex = 0;
concurrentlyUpdatedLanes = NoLanes;
var i = 0;
while (i < endIndex) {
var fiber = concurrentQueues[i];
concurrentQueues[i++] = null;
var queue = concurrentQueues[i];
concurrentQueues[i++] = null;
var update = concurrentQueues[i];
concurrentQueues[i++] = null;
var lane = concurrentQueues[i];
concurrentQueues[i++] = null;
if (queue !== null && update !== null) {
var pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
if (lane !== NoLane) {
markUpdateLaneFromFiberToRoot(fiber, update, lane);
}
}
}
2.3 触发全量渲染-函数组件beginWork()
经过beginWork()
->updateFunctionComponent()
,从而再次触发renderWithHooks()
而此时的useState()
不再是mountState
,而是updateState()
,从下面代码可以知道,本质也是updateReducer()
,只是为useState()
自动设置了一个reducer
方法=basicStateReducer
function updateState(initialState) {
return updateReducer(basicStateReducer, initialState);
}
function basicStateReducer(state, action) {
return typeof action === "function" ? action(state) : action;
}
我们传入的reducer
是basicStateReducer
,然后进行hook.queue.pending
->baseQueue
,如果baseQueue
为空,说明该hook
没有更新,那么不触发reducer()
执行以及hook.memoziedState
的重新赋值!
由于我们在 dispatchSetState()
传入的值为 action
,所以这里本质就是判断 action
是否为 function
,如果不是 function
,直接返回传入的值
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
queue.lastRenderedReducer = reducer;
var current = currentHook;
var baseQueue = current.baseQueue;
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
var first = baseQueue.next;
var newState = current.baseState;
var update = first;
do {
var action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first);
hook.memoizedState = newState;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
if (baseQueue === null) {
// 不需要更新
queue.lanes = NoLanes;
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
3. 多种更新类型分析
更新的时候主要分为3种类型:
- 删除节点
- 新增节点
- 移动节点
我们接下来将根据上面3种类型进行分析
比如删除节点,我们在什么阶段进行fiber删除的flag标记的?我们在什么阶段进行删除flag标记对应的dom节点的删除的?
在reconcileChildFibers()
中,我们需要判断当前是否是初次渲染的阶段,如果是初次渲染,则不用触发对应的删除逻辑
如果是渲染更新,分为两种情况
- 新的数据是单个元素:触发
reconcileSingleElement()
- 旧的数据是单个元素:根据
key
+type
判断是否可以复用,否则删除 - 旧的数据是Array:根据
key
+type
找到可以复用的数据,其余都删除
- 旧的数据是单个元素:根据
- 新的数据是Array元素:触发
reconcileChildrenArray()
=> 涉及到多个元素的diff算法 - 新的数据为空时,直接触发
deleteRemainingChildren()
删除所有的旧节点数据
3.1 删除逻辑
- 新的数据是单个元素:触发
reconcileSingleElement()
- 旧的数据是单个元素:根据
key
+type
判断是否可以复用,否则删除 - 旧的数据是Array:根据
key
+type
找到可以复用的数据,其余都删除
- 旧的数据是单个元素:根据
function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
var isUnkeyedTopLevelFragment =
typeof newChild === "object" &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
//...
}
if (isArray(newChild)) {
//...新的数据是Array元素 => 触发`reconcileChildrenArray()`
}
}
//...处理文本
return deleteRemainingChildren(returnFiber, currentFirstChild); //...新的数据为空,直接删除旧的所有数据
}
当新的children
是一个singleElement
时,我们会触发reconcileSingleElement()
进行处理
- 如果
key
不同,则触发deleteChild()
直接删除旧的节点数据 - 如果
key
相同,type
又不相同,说明可以复用的节点数据的类型已经改变,所有旧的数据都无法复用,直接触发deleteRemainingChildren()
删除所有的旧节点数据 - 如果
key
相同,type
相同,则说明可以复用,保留当前的节点,触发deleteRemainingChildren()
删除其余的旧节点数据
注:当
child.tag === Fragment
时,需要提取element.props.children
而不是element.props
function reconcileSingleElement(
returnFiber,
currentFirstChild, // 旧的数据
element, // 新的数据
lanes
) {
var key = element.key;
var child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
var elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// 因为新的element数据是单节点,如果旧的数据也是同样的fragment,那么旧的数据的剩余节点都可以直接删除
deleteRemainingChildren(returnFiber, child.sibling);
var existing = useFiber(child, element.props.children);
existing.return = returnFiber;
return existing;
}
} else {
if (child.elementType === elementType) {
deleteRemainingChildren(returnFiber, child.sibling);
var _existing = useFiber(child, element.props);
_existing.ref = coerceRef(returnFiber, child, element);
_existing.return = returnFiber;
return _existing;
}
} // Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
// ...新的数据的fiber创建逻辑
}
3.1.1 fiber标记:deleteChild标记单个子节点删除
获取当前fiber对应的deletions
,然后将当前fiber想要删除的childFiber
添加到deletions
中
注意:是将要删除的子fiber添加到父fiber的
deletions
中!! 并且给当前父fiber打上ChildDeletion
的flags
function deleteChild(returnFiber, childToDelete) {
if (!shouldTrackSideEffects) {
return;
}
var deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
3.1.2 fiber标记:deleteRemainingChildren标记多个节点删除
function deleteRemainingChildren(returnFiber, currentFirstChild) {
if (!shouldTrackSideEffects) {
return null;
}
var childToDelete = currentFirstChild;
while (childToDelete !== null) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
return null;
}
3.1.3 fiber标记处理:commit阶段处理ChildDeletion
根据fiber.ChildDeletion进行对应真实DOM的删除
在之前的分析中,我们知道commitMutationEffectsOnFiber()
会触发
recursivelyTraverseMutationEffects()
commitReconciliationEffects()
在commitMutationEffectsOnFiber()
中高频出现的recursivelyTraverseMutationEffects()
是为了
- 处理当前
fiber.deletions
,在reconcileChildFibers()
中进行fiber.deletions
数据的添加(也就是fiber.children
中已经被移除的数据) - 然后触发
fiber.child
进行commitMutationEffectsOnFiber()
=>fiber.child
处理完成,就处理fiber.child.sibling
,触发commitMutationEffectsOnFiber()
总结:处理fiber.childrenDeletion
集合 + 往下遍历fiber.child
+ fiber.child.sibling
进行递归调用commitMutationEffectsOnFiber()
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
//...
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
commitHookEffectListUnmount(Insertion | HasEffect, ...);
commitHookEffectListMount(Insertion | HasEffect, finishedWork);
commitHookEffectListUnmount(Layout | HasEffect, ...);
}
return;
}
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
//...
}
return;
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
//...
}
if (finishedWork.flags & ContentReset) {
//...
}
if (flags & Update) {
//...
}
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
//...
}
return;
}
case HostRoot: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
//...
}
return;
}
case OffscreenComponent: {
//...
}
//...还有多种类型
default: {
recursivelyTraverseMutationEffects(root, finishedWork);
commitReconciliationEffects(finishedWork);
return;
}
}
}
而在recursivelyTraverseMutationEffects()
中,我们直接获取当前fiber的deletions
,也就是下面的parentFiber.deletions
的数据,然后直接处理当前fiber的所有需要删除的children
因为这里是深度遍历,会先处理
children
->children.sibling
->parent
,因此我们先把当前fiber的所有需要删除的children处理了,那么就不需要深度遍历需要删除的children了
function recursivelyTraverseMutationEffects(root: FiberRoot, parentFiber: Fiber) {
var deletions = parentFiber.deletions;
if (deletions !== null) {
for (var i = 0; i < deletions.length; i++) {
var childToDelete = deletions[i];
commitDeletionEffects(root, parentFiber, childToDelete);
}
}
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
commitMutationEffectsOnFiber(child, root);
child = child.sibling;
}
}
}
从上面代码知道,处理逻辑主要集中在commitDeletionEffects()
中
- 先使用一个
while()
获取目前要删除的fiber的parent的真实DOMhostParent
hostParent
是一个全局变量!!!
- 然后触发
commitDeletionEffectsOnFiber()
- 然后触发
detachFiberMutation()
function commitDeletionEffects(root, returnFiber, deletedFiber) {
var parent = returnFiber;
findParent: while (parent !== null) {
switch (parent.tag) {
case HostComponent: {
hostParent = parent.stateNode;
hostParentIsContainer = false;
break findParent;
}
case HostRoot: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
case HostPortal: {
hostParent = parent.stateNode.containerInfo;
hostParentIsContainer = true;
break findParent;
}
}
parent = parent.return;
}
if (hostParent === null) {
throw new Error(
"Expected to find a host parent. This error is likely caused by " +
"a bug in React. Please file an issue."
);
}
commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
hostParent = null;
hostParentIsContainer = false;
detachFiberMutation(deletedFiber);
}
3.1.3.1 commitDeletionEffectsOnFiber()
在commitDeletionEffectsOnFiber()
中,根据deletedFiber.tag
进行了不同类型的繁杂处理
虽然代码量非常多,但是我们仔细观察就会发现,其本质逻辑就是:
- 非
HostComponent&HostText
类型会触发recursivelyTraverseDeletionEffects()
,基本没做什么其他处理 HostComponent
类型会先触发safelyDetachRef
,由于没有break
,因此后续会触发HostText
的处理HostText
类型先触发recursivelyTraverseDeletionEffects()
,然后触发removeChild()
进行原生DOM的removeChild()
移除DOM操作
function commitDeletionEffectsOnFiber(
finishedRoot,
nearestMountedAncestor,
deletedFiber
) {
//...
switch (deletedFiber.tag) {
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
}
case HostText: {
{
var prevHostParent = hostParent;
var prevHostParentIsContainer = hostParentIsContainer;
hostParent = null;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber
);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
if (hostParent !== null) {
// Now that all the child effects have unmounted, we can remove the
// node from the tree.
if (hostParentIsContainer) {
removeChildFromContainer(hostParent, deletedFiber.stateNode);
} else {
removeChild(hostParent, deletedFiber.stateNode);
}
}
}
return;
}
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
//...
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber
);
return;
}
case ClassComponent: {
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
var instance = deletedFiber.stateNode;
if (typeof instance.componentWillUnmount === "function") {
safelyCallComponentWillUnmount(
deletedFiber,
nearestMountedAncestor,
instance
);
}
}
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber
);
return;
}
default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber
);
return;
}
}
}
因此
recursivelyTraverseDeletionEffects()
到底执行了什么呢?
recursivelyTraverseDeletionEffects()
的逻辑也非常简单,就是取出要删除的fiber的children,然后触发commitDeletionEffectsOnFiber()
而commitDeletionEffectsOnFiber()
就是根据不同类型进行处理的方法:
- 遇到
HostComponent&HostText
类型时会触发removeChild()
进行原生DOM的removeChild()
移除DOM操作 - 遇到非
HostComponent&HostText
类型时则会继续调用recursivelyTraverseDeletionEffects()
取出要删除的fiber的children,然后触发commitDeletionEffectsOnFiber()
如果当前类型是HostComponent,直接删除parentDom.removeChild(childDom)即可
如果当前类型是FunctionComponent,我们需要触发对应的effect,然后拿到fiber.child(因为FunctionComponent这个fiber是不具备DOM的)才是它的DOM,甚至有可能这个fiber.child是一个数组,也就是多个DOM,我们需要遍历所有DOM进行removeChild()
function recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
parent
) {
var child = parent.child;
while (child !== null) {
commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child);
child = child.sibling;
}
}
3.1.3.2 detachFiberMutation()
由于当前fiber已经被删除,因此可以直接切断当前fiber与双缓冲树的联系
function detachFiberMutation(fiber) {
var alternate = fiber.alternate;
if (alternate !== null) {
alternate.return = null;
}
fiber.return = null;
}
3.2 移动/新增逻辑
节点复用的三个条件:同一层级下 + key相同 + type相同(也就是
html
的tag
相同,都是<div>
,都是<span>
)
新的数据是Array元素:触发reconcileChildrenArray()
=> 涉及到多个元素的diff算法
function reconcileChildrenArray(...) {
// 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
//...
}
// 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
//...
return resultingFirstChild;
}
// 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
if (oldFiber === null) {
//...
}
// 3.2.3 新的节点和旧的节点还有,进行diff复用
var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
//...
}
// 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
if (shouldTrackSideEffects) {
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
return resultingFirstChild;
}
3.2.1 fiber标记:从左边到右边,从index=0
不断递增,比较是否可以直接复用,减少diff的范围
比如
旧:
[1, 2, 3, 4]
新:
[1, 2, 3, 5, 6]
我们可以比较得到前三位的数据是可以直接复用的,那么我们diff的范围就缩小到
旧:
[4]
新:
[5, 6]
从newIdx = 0
开始遍历,我们要缩减的是左边的边界
- 如果
oldFiber.index > newIdx
,说明能够左边的边界已经不能缩减了,那么我们就将oldFiber
设置为null,最终得到的newFiber
肯定为null
,那么最终会触发break
,同时使用oldFiber = nextOldFiber
恢复数据
比如旧的
[1, 2, 3, 4]
和新的[1, 3, 3, 4]
,当旧的的3
为oldFiber.index=2
,新的3
为newIdx=1
- 如果
oldFiber.index <= newIdx
,说明还可以缩减左边的边界,继续单链表的下一个数据nextOldFiber = oldFiber.sibling
- 使用
updateSlot()
检测旧的fiber是否被复用
updateSlot()
先进行key
的判断,如果key
不一样,则肯定无法直接复用,直接返回null
=> 可能目前数据是移动了,还可以被其他节点复用,不删除旧的fiber!!!!如果
key
一样,说明是原来对应的数据,也不可能被其他节点复用了,只是可能<tag>
变了,那么就直接创建新的fiber,然后删除掉旧的fiber!!!
- 如果旧的fiber能够被复用(
key
和tag
都相同),则直接更新旧的fiber =>newFiber
- 如果旧的fiber存在但是
key
相同,说明是原来对应的数据,如果旧的fiber为NULL
/旧的fiber的tag不一致
,直接创建新的fiber =>newFiber
,然后后续逻辑删除旧的fiber - 如果旧的fiber存在但是
key
不同,说明不是原来对应的数据 => 返回newFiber=null
=> 中断左边边界的缩减
下面的小节会展开对
updateSlot()
源码的详细分析
- 如果
newFiber=null
,说明无法缩减左边的边界了,直接break
当前的循环 - 如果
newFiber不为null
,说明是oldFiber
和newFiber
的key
相同,是原来对应的数据,可能可以复用- 通过
oldFiber && newFiber.alternate === null
,说明还是不能复用,是直接创建了新的fiber
=> 删除旧的fiber
- 否则就是可以复用 => 通过
lastPlacedIndex
判断是否是move
/insertion
,然后打上newFiber.flags |= Placement
- 通过
- 最终构建单链表结构:
resultingFirstChild
代表头节点,previousNewFiber
代表前节点,可以不断previousNewFiber.sibling = newFiber
function reconcileChildrenArray() {
// 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
var newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
//...
return resultingFirstChild;
}
// 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
if (oldFiber === null) {
//...
}
// 3.2.3 新的节点和旧的节点还有,进行diff复用
var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
//...
}
// 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
if (shouldTrackSideEffects) {
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
return resultingFirstChild;
}
3.2.1.1 updateSlot
主要分为3种情况进行处理:文本格式、Object格式(包括各种FunctionComponent、ClassComponent等等)、Array格式(Fragment)
先进行
key
的判断,如果key
不一样,则肯定无法直接复用,直接返回null
=> 可能目前数据是移动了,还可以被其他节点复用,不删除旧的fiber!!!! 如果key
一样,说明是原来对应的数据,也不可能被其他节点复用了,只是可能<tag>
变了,那么就直接创建新的fiber,然后删除掉旧的fiber!!!
- 先处理
newChild
是否是文本格式,由于文本数据没有key
,因此旧的数据存在key
时,说明是非文本数据
->文本数据
,无法复用,直接返回null
- 然后处理
newChild
的object
格式,只有key
相同才有可能复用,一定不能复用时直接返回null
,然后进入updateTextNode()
尝试复用,如果无法复用则直接返回新创建的fiber
- 然后处理
newChild
的array
格式,新数据没有key
,如果旧数据存在key
,则一定不能复用,一定不能复用时直接返回null
,然后进入updateFragment()
尝试复用,如果无法复用则直接返回新创建的fiber
function updateSlot(returnFiber, oldFiber, newChild, lanes) {
var key = oldFiber !== null ? oldFiber.key : null;
if (
(typeof newChild === "string" && newChild !== "") ||
typeof newChild === "number"
) {
if (key !== null) {
return null;
}
return updateTextNode(returnFiber, oldFiber, "" + newChild, lanes);
}
if (typeof newChild === "object" && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {}
case REACT_LAZY_TYPE: {}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
if (key !== null) {
return null;
}
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
throwOnInvalidObjectType(returnFiber, newChild);
}
return null;
}
3.2.1.2 updateTextNode()
- 如果当前旧的fiber为空或者当前fiber不是文本数据,直接创建新的fiber文本数据
- 如果当前旧的fiber可以被复用,使用
useFiber()
复用fiber并且更新数据,返回新的fiber文本数据
function updateTextNode(returnFiber, current, textContent, lanes) {
if (current === null || current.tag !== HostText) {
// Insert
var created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
} else {
// Update
var existing = useFiber(current, textContent);
existing.return = returnFiber;
return existing;
}
}
3.2.1.3 updateFragment()
跟上面updateTextNode()
逻辑基本一致
- 如果为空或者当前的
<tag>
已经改变,则创建新的fiber,只是使用的方法从createFiberFromText()
->createFiberFromFragment()
- 如果可以复用,则使用
useFiber()
进行数据的更新
function updateFragment(returnFiber, current, fragment, lanes, key) {
if (current === null || current.tag !== Fragment) {
// Insert
var created = createFiberFromFragment(...);
created.return = returnFiber;
return created;
} else {
// Update
var existing = useFiber(current, fragment);
existing.return = returnFiber;
return existing;
}
}
3.2.1.4 updateElement()
- 如果当前新的数据是
fragment
,取出element.props.children
数组数据去触发updateFragment()
- 检查旧的fiber和新的fiber的
type
是否相同,相同则进行复用,更新旧的fiber数据 - 如果
type
不相同或者当前旧的fiber为空,则直接新建新的fiber数据
function updateElement(returnFiber, current, element, lanes) {
var elementType = element.type;
if (elementType === REACT_FRAGMENT_TYPE) {
return updateFragment(
returnFiber,
current,
element.props.children,
lanes,
element.key
);
}
if (current !== null) {
if (
current.elementType === elementType ||
(typeof elementType === "object" &&
elementType !== null &&
elementType.$$typeof === REACT_LAZY_TYPE &&
resolveLazy(elementType) === current.type)
) {
// Move based on index
var existing = useFiber(current, element.props);
existing.ref = coerceRef(returnFiber, current, element);
existing.return = returnFiber;
return existing;
}
} // Insert
var created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, current, element);
created.return = returnFiber;
return created;
}
3.2.1.5 placeChild()
如果是初次渲染,即shouldTrackSideEffects=false
,那么直接返回lastPlacedIndex
即可
如果不是初次渲染,那么我们需要根据判断是move
还是insert
数据,如果是move
,还需要根据lastPlaceIndex
进行位置判断
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
newFiber.flags |= Forked;
return lastPlacedIndex;
}
var current = newFiber.alternate;
if (current !== null) {
var oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
上面的代码可以使用下图流程辅助理解
3.2.2 fiber标记:经过上面的流程,如果出现旧的节点已经没有/新的节点已经没有,那么只需要进行新增剩余节点/删除剩余节点
不需要再进行复杂的diff判断是否可以进行复用
当左边边界缩减后,新的节点已经遍历完成,旧的节点还有
那么就进行剩余旧节点的删除工作,直接触发deleteRemainingChildren()
删除旧的fiber
当左边边界缩减后,新的节点还没遍历完成,旧的节点已经没有
那么就进行剩余新节点的创建工作
- 使用
createChild()
创建新的fiber数据 - 使用
placeChild()
进行标记:由于newFiber.alternate为空
,因此直接newFiber.flags |= Placement
,不考虑lastPlacedIndex
function reconcileChildrenArray() {
// 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
//
}
// 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
//...
return resultingFirstChild;
}
// 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (_newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);
// 构建一个新的单链表结构,不停将当前fiber后移到下一个fiber
if (previousNewFiber === null) {
resultingFirstChild = _newFiber;
} else {
previousNewFiber.sibling = _newFiber;
}
previousNewFiber = _newFiber;
}
return resultingFirstChild;
}
// 3.2.3 新的节点和旧的节点还有,进行diff复用
var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
//...
}
// 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
if (shouldTrackSideEffects) {
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
return resultingFirstChild;
}
3.2.3 fiber标记:经过上面的流程,旧的节点/新的节点都还有,进行复杂的diff
哪些可以复用?哪些需要新增?哪些需要删除?
从下面代码可以看出
- 使用
mapRemainingChildren()
构建oldFiber的Map数据 - 遍历剩余的newFiber数据,从
mapRemainingChildren()
找到可以复用的fiber进行数据更新,否则直接创建新的Fiber数据 - 删除已经复用的fiber数据,从
existingChildren
这个Map集合中删除已经复用的oldFiber数据 - 使用
placeChild()
判断是move
还是insert
数据,如果是move
,还需要根据lastPlaceIndex
进行位置判断是否需要newFiber.flags |= Placement
- 最终构建出新的fiber的单链表结构进行返回
注:lastPlacedIndex是指目前遍历到的元素标记Placement的最大index!不是指目前遍历的index!
function reconcileChildrenArray() {
// 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
//
}
// 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
//...
return resultingFirstChild;
}
// 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
if (oldFiber === null) {
//...
}
// 3.2.3 新的节点和旧的节点还有,进行diff复用
var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
var _newFiber2 = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);
if (_newFiber2 !== null) {
if (shouldTrackSideEffects) {
if (_newFiber2.alternate !== null) {
existingChildren.delete(
_newFiber2.key === null ? newIdx : _newFiber2.key
);
}
}
lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = _newFiber2;
} else {
previousNewFiber.sibling = _newFiber2;
}
previousNewFiber = _newFiber2;
}
}
// 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
if (shouldTrackSideEffects) {
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
return resultingFirstChild;
}
3.2.4 fiber标记:经过diff后,新节点已经遍历完成,旧的节点还有
如果diff完成后,旧的节点还有剩余,也就是existingChildren
这个Map数据还残留着数据,则直接删除目前的所有旧Fiber数据
function reconcileChildrenArray() {
// 3.2.1 从左边到右边,从`index=0`不断递增,比较是否可以直接复用,减少diff的范围
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
//
}
// 3.2.2 新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
//...
return resultingFirstChild;
}
// 3.2.2 旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
if (oldFiber === null) {
//...
}
// 3.2.3 新的节点和旧的节点还有,进行diff复用
var existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {
//...
}
// 3.2.4 新的节点已经新增/移动完毕,剩下的旧节点应该删除
if (shouldTrackSideEffects) {
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}
return resultingFirstChild;
}
3.2.5 fiber标记处理:commit阶段处理Placement
- 通过
getHostParentFiber()
找到parentFiber
是HostComponent
/HostRoot
/HostPortal
的parent,否则一直parent=parent.return
- 通过
getHostSibling()
寻找具备stateNode
真实DOM + 没有Placement
标记的fiber,然后返回其node.stateNode
- 使用
insertOrAppendPlacementNode()
处理各种类型fiber的DOM的插入操作,包括: FunctionComponent、HostComponent等等的插入操作
insertOrAppendPlacementNode()
可以参考下面的分析
function commitPlacement(finishedWork) {
var parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostComponent: {
var parent = parentFiber.stateNode;
var before = getHostSibling(finishedWork);
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal: {
var _parent = parentFiber.stateNode.containerInfo;
var _before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, _before, _parent);
break;
}
}
}
注:
insertOrAppendPlacementNode()
可以参考文章首次渲染流程分析(二)
中4.3.1 insertOrAppendPlacementNodeIntoContainer()
,下面的分析摘自4.3.1
当node
是FunctionComponent
,它是不具备DOM
的!!!因此isHost
=false
,触发了第三个条件的代码
我们会直接取node.child
,也就是FunctionComponent
中顶层元素<div>
,然后触发insertOrAppendPlacementNodeIntoContainer()
,这个时候node
是HostComponent
,具备DOM
,因此可以执行插入操作,也就是#root.appendChild(<div/>)
处理完node.child
还不够,我们还得处理下node.child.sibling
,因此可能存在着FunctionComponent
的顶层元素是一个<React.Fragment>
的情况,它也是一个不具备DOM
的类型,我们需要#root.appendChild(Fragment的childDOM)
function insertOrAppendPlacementNodeIntoContainer(node, before, parent) {
var tag = node.tag;
var isHost = tag === HostComponent || tag === HostText;
if (isHost) {
var stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal);
else {
var child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
var sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
3.3 React18与Vue3的diff算法简单对比
下面Vue分析摘自以前的文章:https://segmentfault.com/a/1190000042974066
从Vue 3.2.30
的源码可以知道,源码中有对patchKeyedChildren()
方法进行了核心步骤的注释,摘出核心步骤的注册如下面代码块所示,可以分为5个步骤:
- 步骤1:从头->尾,处理相同的前置元素
- 步骤2:从尾->头,处理相同的后置元素
- 步骤3:旧vnode已经处理完毕,但是新vnode还有元素,处理新增元素,直接进行mount
- 步骤4:旧vnode还有剩余,但是新vnode已经处理完毕,处理已经废弃元素,直接进行unmount
- 步骤5:最复杂的情况,处理相同的前置元素+处理相同的后置元素后,剩下的元素有新增、废弃、乱序的情况,需要复杂处理
而React 18.3.1
的主要diff流程如下所示
- 步骤1:从左边到右边,从
index=0
不断递增,比较是否可以直接复用,减少diff的范围 - 步骤2:新的节点已经遍历完成,旧的节点还有,进行剩余旧节点的删除工作
- 步骤3:旧的节点已经遍历完成,新的节点还有,开始剩余新的节点的创建工作
- 步骤4:新的节点和旧的节点还有,进行diff复用
- 步骤5:新的节点已经新增/移动完毕,剩下的旧节点应该删除
在上面两个框架的流程概述中,我们可以发现,Vue
增加了从尾->头,处理相同的后置元素
的处理步骤,然后主要区别就在于新增、废弃、乱序
的处理
3.3.1 Vue3中新增、废弃、乱序的处理
下面步骤摘自以前的文章:https://segmentfault.com/a/1190000042974066
- 步骤5.1:为newChildren建立索引
- 步骤5.2:移除废弃的旧vnode + 更新能复用的旧vnode + newIndexToOldIndexMap和move的构建为下一步骤做准备
- 逻辑1:移除废弃的旧vnode
- 逻辑2:更新能复用的旧vnode
- 逻辑3:newIndexToOldIndexMap和move的构建为步骤5.3做准备
- 步骤5.3:移动/新增处理
- 新增:判断目前新vnode是之前没有过的新vnode
- 移动:判断目前新vnode对应的可复用的旧vnode是否需要移动位置
- 流程1: 构建最长递增子序列
- 流程2: 根据increasingNewIndexSequence进行节点的移动
increasingNewIndexSequence最长递增子序列的作用:获取旧的children在新的children的相对位置顺序仍然递增的最长子序列,减少move的次数,提升性能
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR;
j = increasingNewIndexSequence.length - 1;
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) {
patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2 /* REORDER */);
} else {
j--;
}
}
}
const move = (vnode, container, anchor, moveType, parentSuspense = null) => {
const { el, type, transition, children, shapeFlag } = vnode;
//...多种类型的数据进行不同的move处理,包括组件、Comment、Static
if (needTransition) {
//...
} else {
hostInsert(el, container, anchor);
}
};
- 如果
increasingNewIndexSequence.length=0
,即构建不出来最长递增子序列的时候,按照上面代码块的逻辑,我们从末尾开始,使用当前新vnode后面的index作为参照,不停将旧vnode移动插入到c2[nextIndex+1]
的前面 - 如果
increasingNewIndexSequence.length>0
,那么遇到i === increasingNewIndexSequence[j]
时,代表目前的nextChild
是最长递增子序列的一个元素,由于最长递增子序列代表旧vnode
的相关位置在新vnode
的相关位置仍然保持递增,因此这些位于最长递增子序列的元素可以不进行move操作,直接进行j--即可