setState执行之后会发生什么
setState
执行之后,会执行一个叫 enqueueSetState
的方法,这个主要作用是创建 Update
对象和发起调度,可以看下这个函数的逻辑
enqueueSetState: function (inst, payload, callback) {
// 1. inst是组件实例,从组件实例中拿到当前组件的Fiber节点
var fiber = get(inst);
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
// 2.1 根据更新发起时间、优先级、更新的payload创建一个update对象
var update = createUpdate(eventTime, lane);
update.payload = payload;
// 2.2 如果 setState 有回调,顺便把回调赋值给 update 对象的 callback 属性
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 3. 将 update 对象关联到 Fiber 节点的 updateQueue 属性中
enqueueUpdate(fiber, update);
// 4. 发起调度
var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
}
从上面源码可以清晰知道,setState
调用之后做的4件事情
- 根据组件实例获取其 Fiber 节点
- 创建
Update
对象 - 将
Update
对象关联到 Fiber 节点的updateQueue
属性中 - 发起调度
根据组件实例获取其 Fiber 节点
其实就是拿组件实例中的 _reactInternals
属性,这个就是当前组件所对应的 Fiber 节点
function get(key) {
return key._reactInternals;
}
题外话:react利用双缓存机制来完成 Fiber 树的构建和替换,也就是 current
和 workInProgress
两棵树,那 enqueueSetState
里面拿的是那棵树下的 Fiber 节点呢?
答案是:current树下的Fiber节点。具体的原理在下面update对象丢失问题再说明
创建update对象
function createUpdate(eventTime, lane) {
var update = {
eventTime: eventTime,
lane: lane,
tag: UpdateState,
payload: null,
callback: null,
next: null
};
return update;
}
属性的含义如下:
- eventTime:update对象创建的时间,用于
ensureRootIsScheduled
计算过期时间用 - lane:此次更新的优先级
- payload:setState的第一个参数
- callback:setState的第二个参数
- next:连接的下一个 update 对象
将Update
对象关联到Fiber节点的updateQueue
属性
这里执行的是 enqueueUpdate
函数,下面是我简化过后的逻辑
function enqueueUpdate(fiber, update) {
var updateQueue = fiber.updateQueue;
var sharedQueue = updateQueue.shared;
var pending = sharedQueue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
可以看到这里的逻辑主要是将 update 对象放到 fiber 对象的 updateQueue.shared.pending
属性中, updateQueue.shared.pending
是一个环状链表。
那为什么需要把它设计为一个环状链表?我是这样理解的
shared.pending
存放的是链表的最后一个节点,那么在环状链表中,链表的最后一个节点的next指针,是指向环状链表的头部节点,这样我们就能快速知道链表的首尾节点- 当知道首尾节点后,就能很轻松的合并两个链表。比如有两条链表a、b,我们想要把 b append到 a 的后面,可以这样做
const lastBPoint = bTail
const firstBPoint = bTail.next
lastBPoint.next = null
aTail.next = firstBPoint
aTail = lastBPoint
后面即使有c、d链表,同样也可以用相同的办法合并到a。react 在构建 updateQueue
链表上也用了类似的手法,新产生的 update
对象通过类似上面的操作合并到 updateQueue
链表,相关参考视频讲解:进入学习
发起调度
在 enqueueUpdate
末尾,执行了 scheduleUpdateOnFiber
函数,该方法最终会调用 ensureRootIsScheduled
函数来调度react的应用根节点。
当进入 performConcurrentWorkOnRoot
函数时,就代表进入了 reconcile
阶段,也就是我们说的 render
阶段。render
阶段是一个自顶向下再自底向上的过程,从react的应用根节点开始一直向下遍历,再从底部节点往上回归,这就是render
阶段的节点遍历过程。
这里我们需要知道的是,在render
阶段自顶向下遍历的过程中,如果遇到组件类型的Fiber节点,我们会执行 processUpdateQueue
函数,这个函数主要负责的是组件更新时 state 的计算
processUpdateQueue做了什么
processUpdateQueue
函数主要做了三件事情
- 构造本轮更新的
updateQueue
,并缓存到 currentFiber 节点中 - 循环遍历
updateQueue
,计算得到newState
,构造下轮更新的updateQueue
- 更新 workInProgress 节点中的
updateQueue
、memoizedState
属性
这里的 updateQueue
并不指代源码中 Fiber 节点的 updateQueue
,可以理解为从 firstBaseUpdate
到 lastBaseUpdate
的整条更新队列。这里为了方便描述和理解,直接用 updateQueue
替代说明。
变量解释
因为涉及的变量比较多,processUpdateQueue
函数的逻辑看起来并不怎么清晰,所以我先列出一些变量的解释方便理解
- shared.pending:
enqueueSetState
产生的 update对象 环形链表
- first/lastBaseUpdate:-- 下面我会用 baseUpdate 代替
- 当前 Fiber 节点中
updateQueue
对象中的属性,代表当前组件整个更新队列链表的首尾节点
- 当前 Fiber 节点中
- first/lastPendingUpdate:下面我会用 pendingUpdate 代替
shared.pending
剪开后的产物,分别代表新产生的 update对象 链表的首尾节点,最终会合并到 currentFiber 和 workInProgress 两棵树的更新队列尾部
- newFirst/LastBaseUpdate:下面我会用 newBaseUpdate 代替
- newState计算过程会得到,只要存在低优先级的 update 对象,这两个变量就会有值。这两个变量会赋值给 workInProgress 的
baseUpdate
,作为下一轮更新 update对象 链表的首尾节点
- newState计算过程会得到,只要存在低优先级的 update 对象,这两个变量就会有值。这两个变量会赋值给 workInProgress 的
- baseState:newState 计算过程依赖的初始 state
- memoizedState:当前组件实例的 state,
processUpdateQueue
末尾会将 newState 赋值给这个变量
构造本轮更新的 updateQueue
上面我们说到 shared.pending
是enqueueSetState
产生的 update对象 环形链表,在这里我们需要剪断这个环形列表取得其中的首尾节点,去组建我们的更新队列。那如何剪断呢?
shared.pending
是环形链表的尾部节点,它的下一个节点就是环形链表的头部节点,参考上一小节我们提到的链表合并操作。
var lastPendingUpdate = shared.pending;
var firstPendingUpdate = lastPendingUpdate.next;
lastPendingUpdate.next = null;
这样就能剪断环形链表,拿到我们想要的新的 update 对象 —— pendingUpdate
。接着我们要拿着这个 pendingUpdate
做两件事情:
- 将
pendingUpdate
合并到当前Fiber节点的更新队列 - 将
pendingUpdate
合并到 currentFiber树 中对应 Fiber节点 的更新队列
为什么要做这两件事情?
- 第一个是解决状态连续性问题,当出现多个 setState 更新时,我们要确保当前 update对象 的更新是以前一个 update对象 计算出来的 state 为前提。所以我们需要构造一个更新队列,新的 update对象 要合并到更新队列的尾部,从而维护state计算的连续性
- 第二个是解决 update 对象丢失问题。在
shared.pending
被剪开之后,shared.pending
会被赋值为null,当有高优先级任务进来时,低优先级任务就会被打断,也就意味着 workInProgress 树会被还原,shared.pending
剪开之后得到的pendingUpdate
就会丢失。这时就需要将pendingUpdate
合并到 currentFiber树 的更新队列中
接下来可以大致看一下这一部分的源码
var queue = workInProgress.updateQueue;
var firstBaseUpdate = queue.firstBaseUpdate;
var lastBaseUpdate = queue.lastBaseUpdate;
// 1. 先拿到本次更新的 update对象 环形链表
var pendingQueue = queue.shared.pending;
if (pendingQueue !== null) {
// 2. 清空pending
queue.shared.pending = null;
var lastPendingUpdate = pendingQueue;
var firstPendingUpdate = lastPendingUpdate.next;
// 3. 剪开环形链表
lastPendingUpdate.next = null;
// 4. 将 pendingupdate 合并到 baseUpdate
if (lastBaseUpdate === null) {
firstBaseUpdate <