点击进入React源码调试仓库。
概述
一旦用户的交互产生了更新,那么就会产生一个update对象去承载新的状态。多个update会连接成一个环装链表:updateQueue,挂载fiber上,
而后在该fiber的beginWork阶段会循环该updateQueue,顺次解决其中的update,这是解决更新的大抵过程,也就是计算组件新状态的实质。在React中,类组件与根组件应用一类update对象,函数组件则应用另一类update对象,然而都遵循一套相似的解决机制。暂且先以类组件的update对象为主进行解说。
相干概念
更新是如何产生的呢?在类组件中,能够通过调用setState产生一个更新:
this.setState({val: 6});
而setState实际上会调用enqueueSetState
,生成一个update对象,并调用enqueueUpdate
将它放入updateQueue。
const classComponentUpdater = { enqueueSetState(inst, payload, callback) { ... // 根据事件优先级创立update的优先级 const lane = requestUpdateLane(fiber, suspenseConfig); const update = createUpdate(eventTime, lane, suspenseConfig); update.payload = payload; enqueueUpdate(fiber, update); // 开始调度 scheduleUpdateOnFiber(fiber, lane, eventTime); ... },};
假如B节点产生了更新,那么B节点的updateQueue最终会是是如下的状态:
A / / B ----- updateQueue.shared.pending = update———— / ^ | / |_______| C -----> D
updateQueue.shared.pending中存储着一个个的update。
上面咱们解说以下update和updateQueue的构造。
update的构造
update对象作为更新的载体,必然要存储更新的信息
const update: Update<*> = { eventTime, lane, suspenseConfig, tag: UpdateState, payload: null, callback: null, next: null,};
- eventTime:update的产生工夫,若该update始终因为优先级不够而得不到执行,那么它会超时,会被立即执行
- lane:update的优先级,即更新优先级
- suspenseConfig:工作挂起相干
- tag:示意更新是哪种类型(UpdateState,ReplaceState,ForceUpdate,CaptureUpdate)
payload:更新所携带的状态。
- 类组件中:有两种可能,对象({}),和函数((prevState, nextProps):newState => {})
- 根组件中:是React.element,即ReactDOM.render的第一个参数
- callback:可了解为setState的回调
- next:指向下一个update的指针
updateQueue的构造
在组件上有可能产生多个update,所以对于fiber来说,须要一个链表来存储这些update,这就是updateQueue,它的构造如下:
const queue: UpdateQueue<State> = { baseState: fiber.memoizedState, firstBaseUpdate: null, lastBaseUpdate: null, shared: { pending: null, }, effects: null, };
咱们假如当初产生了一个更新,那么以解决这个更新的时刻为基准,来看一下这些字段的含意:
- baseState:前一次更新计算得出的状态,它是第一个被跳过的update之前的那些update计算得出的state。会以它为根底计算本次的state
- firstBaseUpdate:前一次更新时updateQueue中第一个被跳过的update对象
- lastBaseUpdate:前一次更新中,updateQueue中以第一个被跳过的update为终点始终到的最初一个update截取的队列中的最初一个update。
- shared.pending:存储着本次更新的update队列,是理论的updateQueue。shared的意思是current节点与workInProgress节点共享一条更新队列。
- effects:数组。保留update.callback !== null的Update
有几点须要解释一下:
- 对于产生多个update对象的场景,屡次调用setState即可
this.setState({val: 2});this.setState({val: 6});
产生的updateQueue构造如下:
能够看出它是个单向的环装链表
u1 ---> u2 ^ | |________|
- 对于更新队列为什么是环状。
论断是:这是因为不便定位到链表的第一个元素。updateQueue指向它的最初一个update,updateQueue.next指向它的第一个update。
试想一下,若不应用环状链表,updateQueue指向最初一个元素,须要遍历能力获取链表首部。即便将updateQueue指向第一个元素,那么新增update时依然要遍历到尾部能力将新增的接入链表。而环状链表,只需记住尾部,无需遍历操作就能够找到首部。了解概念是重中之重,上面再来看一下实现:
function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) { const updateQueue = fiber.updateQueue; if (updateQueue === null) { return; } const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; // ppending是真正的updateQueue,存储update const pending = sharedQueue.pending; if (pending === null) { // 若链表中没有元素,则创立单向环状链表,next指向它本人 update.next = update; } else { // 有元素,现有队列(pending)指向的是链表的尾部update, // pending.next就是头部update,新update会放到现有队列的最初 // 并首尾相连 // 将新队列的尾部(新插入的update)的next指向队列的首部,实现 // 首位相连 update.next = pending.next; // 现有队列的最初一个元素的next指向新来的update,实现把新update // 接到现有队列上 pending.next = update; } // 现有队列的指针总是指向最初一个update,能够通过最初一个寻找出整条 // 链表 sharedQueue.pending = update;}
- 对于firstBaseUpdate 和 lastBaseUpdate,它们两个其实组成的也是一个链表:baseUpdate,以以后这次更新为基准,这个链表存储的是上次updateQueue中第一个被跳过的低优先级的update,到队列中最初一个update之间的所有update。对于baseState,它是第一个被跳过的update之前的那些update计算的state。
这两点略微不好了解,上面用例子来阐明:比方有如下的updateQueue:
A1 -> B1 -> C2 -> D1 - E2
字母示意update携带的状态,数字示意update携带的优先级。Lanes模型中,可了解为数越小,优先级越高,所以 1 > 2
第一次以1的渲染优先级解决队列,遇到C2时,它的优先级不为1,跳过。那么直到这次解决完updateQueue时,此时的baseUpdate链表为
C2 -> D1 - E2
本次更新实现后,firstBaseUpdate 为 C2
,lastBaseUpdate 为 E2
,baseState为ABD
。
用firstBaseUpdate 和 lastBaseUpdate记录下被跳过的update到最初一个update的所有update,用baseState记录下被跳过的update之前那些update所计算出的状态。这样做的目标是保障最终updateQueue中所有优先级的update全副解决完时候的后果与预期后果保持一致。也就是说,只管A1 -> B1 -> C2 -> D1 - E2
这个链表在第一次以优先级为1去计算的后果为ABD(因为优先级为2的都被跳过了),但最终的后果肯定是ABCDE,因为这是队列中的所有update对象被全副解决的后果,下边来具体分析updateQueue的解决机制。
更新的解决机制
解决更新分为三个阶段:筹备阶段、解决阶段、实现阶段。前两个阶段次要是解决updateQueue,最初一个阶段来将新计算的state赋值到fiber上。
筹备阶段
整顿updateQueue。因为优先级的起因,会使得低优先级更新被跳过期待下次执行,这个过程中,又有可能产生新的update。所以当解决某次更新的时候,有可能会有两条update队列:上次遗留的和本次新增的。上次遗留的就是从firstBaseUpdate 到 lastBaseUpdate 之间的所有update;本次新增的就是新产生的那些的update。
筹备阶段阶段次要是将两条队列合并起来,并且合并之后的队列不再是环状的,目标不便从头到尾遍历解决。另外,因为以上的操作都是解决的workInProgress节点的updateQueue,所以还须要在current节点也操作一遍,放弃同步,目标在渲染被高优先级的工作打断后,再次以current节点为原型新建workInProgress节点时,不会失落之前尚未解决的update。
解决阶段
循环解决上一步整顿好的更新队列。这里有两个重点:
- 本次更新是否解决update取决于它的优先级(update.lane)和渲染优先级(renderLanes)。
- 本次更新的计算结果基于baseState。
优先级有余
优先级有余的update会被跳过,它除了跳过之外,还做了三件事:
- 将被跳过的update放到firstBaseUpdate 和 lastBaseUpdate组成的链表中,(就是baseUpdate),期待下次解决低优先级更新的时候再解决。
- 记录baseState,此时的baseState为该低优先级update之前所有已被解决的更新的后果,并且只在第一次跳过期记录,因为低优先级工作重做时,要从第一个被跳过的更新开始解决。
- 将被跳过的update的优先级记录下来,更新过程行将完结后放到workInProgress.lanes中,这点是调度得以再次发动,进而重做低优先级工作的要害。
对于第二点,ReactUpdateQueue.js文件头部的正文做了解释,为了便于了解,我再解释一下。
第一次更新的baseState 是空字符串,更新队列如下,字母示意state,数字示意优先级。优先级是1 > 2的 A1 - B1 - C2 - D1 - E2 第一次的渲染优先级(renderLanes)为 1,Updates是本次会被解决的队列: Base state: '' Updates: [A1, B1, D1] <- 第一个被跳过的update为C2,此时的baseUpdate队列为[C2, D1, E2], 它之前所有被解决的update的后果是AB。此时记录下baseState = 'AB' 留神!再次跳过低优先级的update(E2)时,则不会记录baseState Result state: 'ABD'-------------------------------------------------------------------------------------------------- 第二次的渲染优先级(renderLanes)为 2,Updates是本次会被解决的队列: Base state: 'AB' <- 再次发动调度时,取出上次更新遗留的baseUpdate队列,基于baseState 计算结果。 Updates: [C2, D1, E2] Result state: 'ABCDE'
优先级足够
如果某个update优先级足够,次要是两件事:
- 判断若baseUpdate队列不为空(之前有被跳过的update),则将当初这个update放入baseUpdate队列。
- 解决更新,计算新状态。
将优先级足够的update放入baseUpdate这一点能够和上边低优先级update入队baseUpdate联合起来看。这实际上意味着一旦有update被跳过,就以它为终点,将后边直到最初的update无论优先级如何都截取下来。再用上边的例子来阐明一下。
A1 - B2 - C1 - D2B2被跳过,baseUpdate队列为B2 - C1 - D2
这样做是为了保障最终全副更新实现的后果和用户行为触发的那些更新全副实现的预期后果保持一致。比方,A1和C1尽管在第一次被优先执行,展示的后果为AC,但这只是为了及时响应用户交互产生的长期后果,实际上C1的后果须要依赖B2计算结果,当第二次render时,根据B2的前序update的处理结果(baseState为A)开始解决B2 - C1 - D2队列,最终的后果是ABCD。在提供的高优先级工作插队的例子中,能够证实这一点。
变动过程为 0 -> 2 -> 3,生命周期将state设置为1(工作A2),点击事件将state + 2(工作A1),失常状况下A2失常调度,然而未render实现,此时A1插队,更新队列A2 - A1,为了优先响应高优先级的更新,跳过A2先计算A1,数字由0变为2,baseUpdate为A2 - A1,baseState为0。而后再重做低优先级工作。解决baseUpdate A2 - A1,以baseState(0)为根底进行计算,最初后果是3。
实现阶段
次要是做一些赋值和优先级标记的工作。
- 赋值updateQueue.baseState。若此次render没有更新被跳过,那么赋值为新计算的state,否则赋值为第一个被跳过的更新之前的update。
- 赋值updateQueue 的 firstBaseUpdate 和 lastBaseUpdate,也就是如果本次有更新被跳过,则将被截取的队列赋值给updateQueue的baseUpdate链表。
- 更新workInProgress节点的lanes。更新策略为如果没有优先级被跳过,则意味着本次将update都解决完了,lanes清空。否则将低优先级update的优先级放入lanes。之前说过,
此处是再发动一次调度重做低优先级工作的要害。
- 更新workInProgress节点上的memoizedState。
源码实现
下面根本把解决更新的所有过程叙述了一遍,当初让咱们看一下源码实现。这部分的代码在processUpdateQueue
函数中,它外面波及到了大量的链表操作,代码比拟多,
咱们先来看一下它的构造,我标注出了那三个阶段。
function processUpdateQueue<State>( workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void { // 筹备阶段 const queue: UpdateQueue<State> = (workInProgress.updateQueue: any); let firstBaseUpdate = queue.firstBaseUpdate; let lastBaseUpdate = queue.lastBaseUpdate; let pendingQueue = queue.shared.pending; if (pendingQueue !== null) { /* ... */ } if (firstBaseUpdate !== null) { // 解决阶段 do { ... } while (true); // 实现阶段 if (newLastBaseUpdate === null) { newBaseState = newState; } queue.baseState = ((newBaseState: any): State); queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes); workInProgress.lanes = newLanes; workInProgress.memoizedState = newState; }}
对于下面的概念与源码的主体构造理解之后,放出残缺代码,但删除了无关局部,我增加了正文,对照着那三个过程来看会更有助于了解,否则单看链表操作还是有些简单。
function processUpdateQueue<State>( workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes,): void { // 筹备阶段---------------------------------------- // 从workInProgress节点上取出updateQueue // 以下代码中的queue就是updateQueue const queue: UpdateQueue<State> = (workInProgress.updateQueue: any); // 取出queue上的baseUpdate队列(上面称遗留的队列),而后 // 筹备接入本次新产生的更新队列(上面称新队列) let firstBaseUpdate = queue.firstBaseUpdate; let lastBaseUpdate = queue.lastBaseUpdate; // 取出新队列 let pendingQueue = queue.shared.pending; // 上面的操作,实际上就是将新队列连贯到上次遗留的队列中。 if (pendingQueue !== null) { queue.shared.pending = null; // 取到新队列 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = lastPendingUpdate.next; // 将遗留的队列最初一个元素指向null,实现断开环状链表 // 而后在尾部接入新队列 lastPendingUpdate.next = null; if (lastBaseUpdate === null) { firstBaseUpdate = firstPendingUpdate; } else { // 将遗留的队列中最初一个update的next指向新队列第一个update // 实现接入 lastBaseUpdate.next = firstPendingUpdate; } // 批改遗留队列的尾部为新队列的尾部 lastBaseUpdate = lastPendingUpdate; // 用同样的形式更新current上的firstBaseUpdate 和 // lastBaseUpdate(baseUpdate队列)。 // 这样做相当于将本次合并实现的队列作为baseUpdate队列备份到current节 // 点上,因为如果本次的渲染被打断,那么下次再从新执行工作的时候,workInProgress节点复制 // 自current节点,它下面的baseUpdate队列会保有这次的update,保障update不失落。 const current = workInProgress.alternate; if (current !== null) { // This is always non-null on a ClassComponent or HostRoot const currentQueue: UpdateQueue<State> = (current.updateQueue: any); const currentLastBaseUpdate = currentQueue.lastBaseUpdate; if (currentLastBaseUpdate !== lastBaseUpdate) { if (currentLastBaseUpdate === null) { currentQueue.firstBaseUpdate = firstPendingUpdate; } else { currentLastBaseUpdate.next = firstPendingUpdate; } currentQueue.lastBaseUpdate = lastPendingUpdate; } } } // 至此,新队列曾经合并到遗留队列上,firstBaseUpdate作为 // 这个新合并的队列,会被循环解决 // 解决阶段------------------------------------- if (firstBaseUpdate !== null) { // 取到baseState let newState = queue.baseState; // 申明newLanes,它会作为本轮更新解决实现的 // 优先级,最终标记到WIP节点上 let newLanes = NoLanes; // 申明newBaseState,留神接下来它被赋值的机会,还有前置条件: // 1. 当有优先级被跳过,newBaseState赋值为newState, // 也就是queue.baseState // 2. 当都解决实现后没有优先级被跳过,newBaseState赋值为 // 本轮新计算的state,最初更新到queue.baseState上 let newBaseState = null; // 应用newFirstBaseUpdate 和 newLastBaseUpdate // 来示意本次更新产生的的baseUpdate队列,目标是截取现有队列中 // 第一个被跳过的低优先级update到最初的所有update,最初会被更新到 // updateQueue的firstBaseUpdate 和 lastBaseUpdate上 // 作为下次渲染的遗留队列(baseUpdate) let newFirstBaseUpdate = null; let newLastBaseUpdate = null; // 从头开始循环 let update = firstBaseUpdate; do { const updateLane = update.lane; const updateEventTime = update.eventTime; // isSubsetOfLanes函数的意义是,判断以后更新的优先级(updateLane) // 是否在渲染优先级(renderLanes)中如果不在,那么就阐明优先级有余 if (!isSubsetOfLanes(renderLanes, updateLane)) { const clone: Update<State> = { eventTime: updateEventTime, lane: updateLane, suspenseConfig: update.suspenseConfig, tag: update.tag, payload: update.payload, callback: update.callback, next: null, }; // 优先级有余,将update增加到本次的baseUpdate队列中 if (newLastBaseUpdate === null) { newFirstBaseUpdate = newLastBaseUpdate = clone; // newBaseState 更新为前一个 update 工作的后果,下一轮 // 持有新优先级的渲染过程解决更新队列时,将会以它为根底进行计算。 newBaseState = newState; } else { // 如果baseUpdate队列中曾经有了update,那么将以后的update // 追加到队列尾部 newLastBaseUpdate = newLastBaseUpdate.next = clone; } /* * * newLanes会在最初被赋值到workInProgress.lanes上,而它又最终 * 会被收集到root.pendingLanes。 * * 再次更新时会从root上的pendingLanes中找出渲染优先级(renderLanes), * renderLanes含有本次跳过的优先级,再次进入processUpdateQueue时, * update的优先级符合要求,被更新掉,低优先级工作因而被重做 * */ newLanes = mergeLanes(newLanes, updateLane); } else { if (newLastBaseUpdate !== null) { // 进到这个判断阐明当初解决的这个update在优先级有余的update之后, // 起因有二: // 第一,优先级足够; // 第二,newLastBaseUpdate不为null阐明曾经有优先级有余的update了 // // 而后将这个高优先级放入本次的baseUpdate,实现之前提到的从updateQueue中 // 截取低优先级update到最初一个update const clone: Update<State> = { eventTime: updateEventTime, lane: NoLane, suspenseConfig: update.suspenseConfig, tag: update.tag, payload: update.payload, callback: update.callback, next: null, }; newLastBaseUpdate = newLastBaseUpdate.next = clone; } markRenderEventTimeAndConfig(updateEventTime, update.suspenseConfig); // Process this update. // 解决更新,计算出新后果 newState = getStateFromUpdate( workInProgress, queue, update, newState, props, instance, ); const callback = update.callback; // 这里的callback是setState的第二个参数,属于副作用, // 会被放入queue的副作用队列里 if (callback !== null) { workInProgress.effectTag |= Callback; const effects = queue.effects; if (effects === null) { queue.effects = [update]; } else { effects.push(update); } } } // 挪动指针实现遍历 update = update.next; if (update === null) { // 已有的队列解决完了,检查一下有没有新进来的,有的话 // 接在已有队列后边持续解决 pendingQueue = queue.shared.pending; if (pendingQueue === null) { // 如果没有期待解决的update,那么跳出循环 break; } else { // 如果此时又有了新的update进来,那么将它接入到之前合并好的队列中 const lastPendingUpdate = pendingQueue; const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>); lastPendingUpdate.next = null; update = firstPendingUpdate; queue.lastBaseUpdate = lastPendingUpdate; queue.shared.pending = null; } } } while (true); // 如果没有低优先级的更新,那么新的newBaseState就被赋值为 // 刚刚计算出来的state if (newLastBaseUpdate === null) { newBaseState = newState; } // 实现阶段------------------------------------ queue.baseState = ((newBaseState: any): State); queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; markSkippedUpdateLanes(newLanes); workInProgress.lanes = newLanes; workInProgress.memoizedState = newState; }}
hooks中useReducer解决更新计算状态的逻辑与此处根本一样。
总结
通过下面的梳理,能够看进去整个对更新的解决都是围绕优先级。整个processUpdateQueue函数要实现的目标是解决更新,但要保障更新依照优先级被解决的同时,不乱阵脚,这是因为它遵循一套固定的规定:优先级被跳过后,记住此时的状态和此优先级之后的更新队列,并将队列备份到current节点,这对于update对象按秩序、残缺地被解决至关重要,也保障了最终出现的处理结果和用户的行为触发的交互的后果保持一致。
欢送扫码关注公众号,发现更多技术文章