实际上在之前React v15相关原理的文章中就setState的几个特点有了较为清晰的梳理,虽然在React v16中引入了Fiber架构,但是setState整体的处理逻辑以及使用并没有什么区别,只是底层处理不同而已,为什么还有梳理v16 setState相关的逻辑呢?侧重点不同,本文不会主要去看setState的一些特点的形成原因(在前面的文章中就已经说明了再说就没有意义),主要想通过setState去看视图更新机制。
以简单的实例来梳理整个源码运行流程,实例如下:
class Hello extends React.Component {
constructor(props) {
super(props);
this.state = { date: Date.now() };
}
render() {
return React.createElement(
'button',
{
onClick: () => {
debugger
this.setState({ date: Date.now() })
}
},
this.state.date,
);
}
}
当点击按钮后就会调用setState,其主要逻辑调用对应updater的enqueueSetState方法,和v15版本没有什么区别,区别在于enqueueSetState中的执行逻辑,该函数的主要逻辑如下:
var classComponentUpdater = {
enqueueSetState: function (inst, payload, callback) {
...
var update = createUpdate(expirationTime, suspenseConfig);
update.payload = payload;
...
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
}
}
就是创建update对象,调用scheduleWork开始调度作业,是不是跟render挂载过程相似的处理逻辑。在v16版本中update队列和scheduleWork是核心,所有相关的处理都是通过这两个函数来处理的。
scheduleWork会调用scheduleUpdateOnFiber函数做相关处理,其中主要逻辑是:
function scheduleUpdateOnFiber(fiber, expirationTime) {
...
var priorityLevel = getCurrentPriorityLevel();
if (expirationTime === Sync) {
if ( // Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext) {
...
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root);
...
if (executionContext === NoContext) {
...
flushSyncCallbackQueue();
}
}
} else {
ensureRootIsScheduled(root);
...
}
...
}
setState触发更新是满足expirationTime === Sync,执行ensureRootIsScheduled。该函数的主要执行逻辑代码如下:
function ensureRootIsScheduled() {
...
if (expirationTime === Sync) {
// Sync React callbacks are scheduled on a special internal queue
callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
callbackNode = scheduleCallback(priorityLevel, performConcurrentWorkOnRoot.bind(null, root), // Compute a task timeout based on the expiration time. This also affects
// ordering because tasks are processed in timeout order.
{
timeout: expirationTimeToMs(expirationTime) - now()
});
}
...
}
通过Debug知道会执行scheduleSyncCallback函数,该函数的具体逻辑如下:
function scheduleSyncCallback() {
...
if (syncQueue == null) {
syncQueue = [callback];
immediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl);
}
...
}
从上面逻辑可知会调用Scheduler_scheduleCallback函数,该函数作用是创建task并推入taskQueue中,其中Task的回调函数就是flushSyncCallbackQueueImpl。
当Scheduler_scheduleCallback函数执行完后,代码逻辑会退回到scheduleUpdateOnFiber执行后续逻辑,实际上setState的更新处理逻辑到这里就结束了,那么视图是如何被更新的呢?
通过源码以及调用栈分析,之后会执行flushSyncCallbackQueueImpl函数,该函数就是视图更新的触发,该函数的主要逻辑如下:
function flushSyncCallbackQueueImpl() {
if (!isFlushingSyncQueue && syncQueue !== null) {
// Prevent re-entrancy.
isFlushingSyncQueue = true;
var i = 0;
try {
var _isSync = true;
var queue = syncQueue;
// 执行后面的传入函数
runWithPriority$1(ImmediatePriority, function () {
for (; i < queue.length; i++) {
var callback = queue[i];
do {
callback = callback(_isSync);
} while (callback !== null);
}
});
syncQueue = null;
}
}
}
从上面的逻辑可知该函数实际上是执行syncQueue中的内容,而syncQueue内容就是之前的performSyncWorkOnRoot,追本溯源还是回到了performSyncWorkOnRoot的具体处理逻辑,这个逻辑实际上在之前的Render执行过程中有相关的梳理(workLoopSync -> performUnitOfWork),实际上核心就是两点:
至此完成一次setState的主要的更新流程。实际上上面整个逻辑中有个非常重要的点就是flushSyncCallbackQueueImpl函数的执行(这个函数总是会被执行的,不清楚来源不影响主要脉络的理解),从上面整个逻辑梳理知道这个函数会处理syncQueue,而整个视图的渲染的入口performSyncWorkOnRoot就被保存在SyncQueue中,但是setState整个过程中并没有触发该函数的执行,那么问题来了flushSyncCallbackQueueImpl函数是在哪里被谁触发调用的呢?
通过源码查找可知flushSyncCallbackQueueImpl直接和间接调用有两处:
通过一系列的排查,发现flushSyncCallbackQueue的调用涉及到事件机制。setState的触发一般是在事件处理程序中,当然也有其他情况的。这里就以事件处理中触发setState来讲。React中事件是合成事件,所有的事件都是注册在document或挂载点上的,对应的event也是合成对象SyntheticEvent。v16版本中事件处理过程不同了,这里就不展开,主要记住不论怎么变化都是需要原生DOM来触发的。
在React中不同的事件也存在优先级,主要分成三个优先级:
不同的事件对对应的处理程序是不同的,离散事件对应dispatchDiscreteEvent处理函数,用户阻塞事件对应dispatchUserBlockingUpdate处理函数,连续事件对应dispatchEvent处理函数。click事件就是调用dispatchDiscreteEvent来处理,而dispatchDiscreteEvent函数的处理逻辑如下:
function dispatchDiscreteEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
...
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, container, nativeEvent);
}
discreteUpdates会调用discreteUpdatesImpl函数(即discreteUpdates$1函数),该函数会触发flushSyncCallbackQueue函数的执行,继而触发flushSyncCallbackQueueImpl从而触发视图渲染。
setState触发后的整个逻辑主要脉络如下:
在上面的脉络中,这里补充下一点,就是workLoopSync中处理所有组件更新,在这个过程中对于class组件其render函数会被调用来获得最新的UI,所以最好保持render函数是一个纯函数。