前言
在前面的文章中,我们分析初次渲染的具体流程
接下来我们将着重于分析各种触发渲染更新的操作、更新时的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相关属性,包括
memoizedStatebaseStatequeue:包括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.nextworkInProgressHook=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对应的hookcurrentlyRenderingFiber.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--即可