在 react 项目中,setState 被用于更新 state,从而实现组件重新渲染更新。经过查找阅读许多资料以及源码后,本文就来个人总结一下,简要解析 react 在 setState 后是如何更新组件的。
前言:setState 的同步和异步
- 异步:setState 一般情况下是异步的,由 react 的批量更新事务(ReactDefaultBatchingStrategy)控制(即 react 控制的事件,非调用 js 原生事件时是异步的),以及生命周期函数调用 setState 也不会同步更新 state。同一个函数中执行多个 setState 时会合并,并且同一个属性以最后一次 setState 的值为准。
- 同步:setState 在 setTimeout,setInterval 和 js 原生事件中被调用,则是同步的。
原理:同步和异步是由什么控制的? 是通过 batchedUpdates 函数中设置 isBatchingUpdates 为 true 时,则 setState 为异步更新,false 时为同步更新。
react15
先介绍事务(ReactDefaultBatchingStrategyTransaction)和 batchedUpdates 函数(用来启动事务的,启动事务后 setState 就是异步更新了),后介绍setState。
ReactDefaultBatchingStrategyTransaction 事务
setState 依靠事务进行更新,事务生命周期包含 initialize、perform、close 阶段。在开启事务后,遇到 setState 后 则将 partial state 存到组件实例的_pendingStateQueue 上, 接着调用 enqueueUpdate 排队更新方法,如下 setState 后解析。
// 批处理策略
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true; //开启一次batch
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
// 启动事务, 将callback放进事务里执行
return transaction.perform(callback, null, a, b, c, d, e);
}
},
};
batchUpdates
batchedUpdates先设置 isBatchingUpdates为true(ReactDefaultBatchingStrategy.isBatchingUpdates = true)开启事务,后将 callback函数放进事务里执行(transaction.perform(callback,…)),无论你传进去的函数是什么, 无论这个函数后续会做什么, 都会在执行完 callback(setState 的第二个参数)后调用事务的 close 方法
在 React 中,调用batchedUpdates有很多地方
第一种情况:首次渲染组件时(源码:在ReactMount.js里调用了ReactUpdates.batchedUpdates)
第二种情况:元素上或者组件上绑定了react控制的事件(非调用js原生事件),事件的监听函数中调用setState。(源码:在ReactEventListener.js里,react事件系统中的dispatchEvent函数启动了事务(调用了ReactUpdates.batchedUpdates))
重点:setState后
1、setState后
调用updater的enqueueSetState方法把需要更新的state(partial state)push进去等待队列_pendingStateQueue中, 接着调用enqueueUpdate排队更新。
2、enqueueUpdate
enqueueUpdate方法里需要判断batchingStrategy.isBatchingUpdates == true,即是否开启batch事务
- 情况一:如果已经开启batch,然后标记当前组件为dirtyComponent, 存到dirtyComponents数组中,等到 ReactDefaultBatchingStrategy事务结束时(close)调用runBatchedUpdates批量更新所有组件
- 情况二:方法中如果没有开启batch(或当前batch已结束,也就是说在事务的initialize或更新阶段)就调用batchedUpdates函数开启一次batch,再重新执行enqueueUpdate方法,判断isBatchingUpdates,现在为true了,标记当前组件为dirtyComponent, 存到dirtyComponents数组中, 并没有立即更新,而是继续执行后面事情,等到 ReactDefaultBatchingStrategy事务结束时(close)调用flushBatchedUpdates函数=>runBatchedUpdates函数批量更新所有组件(第三点)
// ReactBaseClasses.js :
ReactComponent.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {// 如果有callback,就将callback放入回调函数队列
this.updater.enqueueCallback(this, callback, 'setState');
}
};
// => ReactUpdateQueue.js:
enqueueSetState: function(publicInstance, partialState) {
// 根据 this.setState 中的 this 拿到内部实例, 也就是组件实例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 取得组件实例的_pendingStateQueue
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
// 将partial state存到_pendingStateQueue
queue.push(partialState);
// 调用enqueueUpdate
enqueueUpdate(internalInstance);
}
// => ReactUpdate.js:
function enqueueUpdate(component) {
ensureInjected(); // 注入默认策略
// 如果没有开启batch(或当前batch已结束)就开启一次batch再执行, 这通常发生在异步回调中调用 setState // 的情况
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果batch已经开启就存储更新
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
3、事务结束时(close阶段)
批量更新的阶段,调用flushBatchedUpdates函数启动ReactUpdatesFlushTransaction事务负责批量更新,这个事务执行了runBatchedUpdates方法通过遍历dirtyComponents数组(在函数里ReactReconciler.performUpdateIfNecessary中调用updateComponent更新组件)进行批量更新。再结束本次batch事务(即ReactDefaultBatchingStrategy.isBatchingUpdates = false; )
var flushBatchedUpdates = function () {
// 启动批量更新事务
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
// 批量处理callback
if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
};
// ReactUpdates.js
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
// 排序保证父组件优先于子组件更新
dirtyComponents.sort(mountOrderComparator);
// 代表批量更新的次数, 保证每个组件只更新一次
updateBatchNumber++;
// 遍历 dirtyComponents
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
...
// 执行更新
ReactReconciler.performUpdateIfNecessary(
component,
transaction.reconcileTransaction,
updateBatchNumber,
);
...
// 存储 callback以便后续按顺序调用
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(
callbacks[j],
component.getPublicInstance(),
);
}
}
}
}
4、updateComponent
ReactReconciler.performUpdateIfNecessary中调用updateComponent更新组件
performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
} else {
this._updateBatchNumber = null;
}
}
5、updateComponent
react内部有3种不同组件:ReactCompositeComponent、ReactDOMComponent和ReactDOMTextComponent。
- ReactCompositeComponent:re-render, 与之前 render 的 element 比较, 如果两者key && element.type 相等, 则进入下一层进行更新; 如果不等, 直接移除重新mount
- ReactDOMComponent: render前生成新的虚拟DOM,对前后虚拟DOM进行diff算法比较(传送门:react的diff算法),找出最小更新部分,批量更新
- ReactDOMTextComponent:直接更新Text
总结
react16:加入Fiber
react16版本中加入了Fiber架构(传送门:Fiber简介)
Fiber主要分两个阶段
调度阶段(reconciliation):Fiber调度
- 将一个state更新任务拆分成多个时间小片,形成一个 Fiber 任务队列.
- 在任务队列中选出优先级高的 Fiber 执行,如果执行时间超过了deathLine,则设置为pending状态挂起状态(即执行一段时间,会跳出找Fiber任务队列中更高级的任务,如果有就放弃当前任务,即使当前任务执行了一半,可能已经经历了一些生命周期,都会被打断从来)。
渲染阶段(commit):
- 进入render函数,构建真实的virtualDomTree,React将其所有的变更一次性更新到DOM上。
重点:setState后
调用updater的enqueueSetState方法,传入state, callback
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
updater更新器中的enqueueSetState
var updater = {
...
enqueueSetState(instance, partialState, callback) {
// 获取到当前组件实例上的fiber
const fiber = ReactInstanceMap.get(instance);
callback = callback === undefined ? null : callback;
// 计算当前fiber的到期时间,即计算出优先级
const expirationTime = computeExpirationForFiber(fiber);
const update = {
expirationTime,
partialState,
callback,
isReplace: false,
isForced: false,
capturedValue: null,
next: null,
};
// 把更新信息任务插入fiber的update queue更新队列中
insertUpdateIntoFiber(fiber, update);
// 开启调度任务,进入fiber的调度阶段(reconciliation)
scheduleWork(fiber, expirationTime);
}
...
};
enqueueSetState详解:
1、get获取组件实例上的fiber
function get(key) {
return key._reactInternalFiber;
}
2、计算到期时间/优先级computeExpirationForFiber
计算当前fiber的优先级(即过期时间),expirationTime 优先级 expirationTime 不为 1 的时候,则其值越低,优先级越高。Fiber任务的优先级:文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务。如下:
//用来计算fiber的到期时间,到期时间用来表示任务的优先级。
function computeExpirationForFiber(fiber) {
var expirationTime = void 0;
if (expirationContext !== NoWork) {
//
expirationTime = expirationContext;
} else if (isWorking) {
if (isCommitting) {
//同步模式,立即处理任务,默认是1
expirationTime = Sync;
} else {
//渲染阶段的更新应该与正在渲染的工作同时过期。
expirationTime = nextRenderExpirationTime;
}
} else {
//没有到期时间的情况下,创建一个到期时间
if (fiber.mode & AsyncMode) {
if (isBatchingInteractiveUpdates) {
// 这是一个交互式更新
var currentTime = recalculateCurrentTime();
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// 这是一个异步更新
var _currentTime = recalculateCurrentTime();
expirationTime = computeAsyncExpiration(_currentTime);
}
} else {
// 这是一个同步更新
expirationTime = Sync;
}
}
if (isBatchingInteractiveUpdates) {
//这是一个交互式的更新。跟踪最低等待交互过期时间。这允许我们在需要时同步刷新所有交互更新。
if (lowestPendingInteractiveExpirationTime === NoWork || expirationTime > lowestPendingInteractiveExpirationTime) {
lowestPendingInteractiveExpirationTime = expirationTime;
}
}
return expirationTime;
}
fiber优先级定义:
module.exports = {
// heigh level
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs.
TaskPriority: 2, // Completes at the end of the current tick.
AnimationPriority: 3, // Needs to complete before the next frame.
// low level
HighPriority: 4, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 5, // Data fetching, or result from updating stores.
OffscreenPriority: 6, // Won't be visible but do the work in case it becomes visible.
};
3、insertUpdateIntoFiber(fiber, update)
把更新信息任务插入update queue更新队列中。确保更新队列存在,不存在则调用ensureUpdateQueues(fiber)创建一个fiber队列,调用insertUpdateIntoQueue(queue, update)将update的值更新到queue队列中。
function insertUpdateIntoFiber(fiber, update) {
//确保更新队列存在,不存在则创建
ensureUpdateQueues(fiber);
//上一步已经将q1和q2队列进行了处理,定义2个局部变量queue1和queue2来保存队列信息。
var queue1 = q1;
var queue2 = q2;
// 如果只有一个队列,请将更新添加到该队列并退出。
if (queue2 === null) {
insertUpdateIntoQueue(queue1, update);
return;
}
// 如果任一队列为空,我们需要添加到两个队列中。
if (queue1.last === null || queue2.last === null) {
//将update的值更新到队列1和队列2上,然后退出该函数
insertUpdateIntoQueue(queue1, update);
insertUpdateIntoQueue(queue2, update);
return;
}
// 如果两个列表都不为空,则由于结构共享,两个列表的最后更新都是相同的。所以,我们应该只追加到其中一个列表。
insertUpdateIntoQueue(queue1, update);
// 但是我们仍然需要更新queue2的`last`指针。
queue2.last = update;
}
4、scheduleWork(fiber, expirationTime)
开启调度任务,进入fiber的调度阶段
scheduleWork执行流程:scheduleWork => scheduleWorkImpl => requestWork => 同步/异步 => performSyncWork => performWork => performWorkOnRoot =>
renderRoot/completeRoot => workLoop => performUnitOfWork => beginWork/completeUnitOfWork => updateClassComponent => reconcileChildrenAtExpirationTime => reconcileChildFibers => reconcileChildrenArray
scheduleWork执行流程比较长,下面讲下主要步骤(详细步骤代码):
-
scheduleWorkImpl:
调用scheduleWorkImpl(fiber, expirationTime, false)函数更新每个node的优先级(即将一个state更新任务拆分成多个时间小片,形成一个 Fiber 任务队列) -
requestWork:
同步执行performSyncWork,异步执行scheduleCallbackWithExpiration,
scheduleCallbackWithExpiration会调浏览器的requestidlecallback,在浏览器空闲的时候进行处理。
expirationTime:同步执行任务下expirationTime为0,即nowork=0。不为0时,使用requestidlecallback/requestAnimationFrame异步执行任务 -
performSyncWork 主要的任务调度:
这里会找到高优任务先执行。
同步任务会直接调用performWorkOnRoot进行下一步,
异步任务也会调performWorkOnRoot,但处理不太一样
如果有上次遗留的任务,留到空闲时运行 -
workLoop(fiber里的判断时间片,超过deathLine,则设置为pending状态):
异步任务在处理的时候会调用shouldYield,shouldYield会判断是不是已经超时了,超时暂时先不做。 -
performUnitOfWork (reconcilation阶段)
调用beginWork处理组件,针对不同组件不同处理。此过程包括dom diff(生成新的 Virtual DOM,然后通过 Diff 算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列)。然后调用completeUnitOfWork对begin work产生的effect list进行一些处理,包括对ReactDOMComponent DOM组件的更新。
渲染阶段(commit)
进入render函数,构建真实的virtualDomTree,调用completeRoot/commitRoot,React将其所有的变更一次性更新到DOM上。
本文参考
从源码全面剖析 React 组件更新机制
React16——看看setState过程中fiber干了什么事情
react 16 渲染整理