事实上,这正是 setState 异步的一个重要的动机——避免频繁的 re-render
。
在实际的 React 运行时中,setState 异步的实现方式有点类似于 Vue 的 $nextTick
和浏览器里的 Event-Loop
:每来一个 setState,就把它塞进一个队列里“攒起来”
。等时机成熟,再把“攒起来”的 state
结果做合并,最后只针对最新的 state 值走一次更新流程
。这个过程,叫作“批量更新
”,批量更新的过程正如下面代码中的箭头流程图所示:
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务,count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队, [count+1的任务,count+1的任务, count+1的任务]
});
↓
合并 state,[count+1的任务]
↓
执行 count+1的任务
值得注意的是,只要我们的同步代码还在执行,“攒起来”这个动作就不会停止。(注:这里之所以多次
+1
最终只有一次生效,是因为在同一个方法中多次 setState 的合并动作不是单纯地将更新累加。比如这里对于相同属性的设置,React 只会为其保留最后一次的更新)。因此就算我们在 React 中写了这样一个 100 次的 setState 循环:
test = () => {
console.log(‘循环100次 setState前的count’, this.state.count)
for(let i=0;i<100;i++) {
this.setState({
count: this.state.count + 1
})
}
console.log(‘循环100次 setState后的count’, this.state.count)
}
也只是会增加 state 任务入队的次数,并不会带来频繁的 re-render。当 100 次调用结束后,仅仅是
state
的任务队列内容发生了变化,state
本身并不会立刻改变:
“同步现象”背后的故事:从源码角度看 setState 工作流
读到这里,相信你对异步这回事多少有些眉目了。接下来我们就要重点理解刚刚代码里最诡异的一部分——setState 的同步现象
:
reduce = () => {
setTimeout(() => {
console.log(‘reduce setState前的count’, this.state.count)
this.setState({
count: this.state.count - 1
});
console.log(‘reduce setState后的count’, this.state.count)
},0);
}
从题目上看,setState
似乎是在 setTimeout
函数的“保护”之下,才有了同步这一“特异功能”。事实也的确如此,假如我们把 setTimeout
摘掉,setState
前后的 console
表现将会与 increment
方法中无异:
reduce = () => {
// setTimeout(() => {
console.log(‘reduce setState前的count’, this.state.count)
this.setState({
count: this.state.count - 1
});
console.log(‘reduce setState后的count’, this.state.count)
// },0);
}
点击后的输出结果如下图所示:
现在问题就变得清晰多了:为什么 setTimeout 可以将 setState 的执行顺序从异步变为同步
?
这里我先给出一个结论:
并不是 setTimeout 改变了 setState,而是 setTimeout 帮助 setState “逃脱”了 React 对它的管控
。只要是在 React 管控下的 setState,一定是异步的
。
接下来我们就从 React 源码里,去寻求佐证这个结论的线索。
时下虽然市场里的 React 16、React 17 十分火热,但就 setState 这块知识来说,React 15 仍然是最佳的学习素材。因此下文所有涉及源码的分析,都会围绕 React 15 展开。关于 React 16 之后 Fiber 机制给 setState 带来的改变,不在本讲的讨论范围内
解读 setState 工作流
我们阅读任何框架的源码,都应该带着问题、带着目的去读。React 中对于功能的拆分是比较细致的,setState 这部分涉及了多个方法。为了方便你理解,我这里先把主流程提取为一张大图:
接下来我们就沿着这个流程,逐个在源码中对号入座。首先是 setState
入口函数:
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, ‘setState’);
}
};
入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里我们以对象形式的入参为例,可以看到它直接调用了 this.updater.enqueueSetState
这个方法:
enqueueSetState: function (publicInstance, partialState) {
// 根据 this 拿到对应的组件实例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, ‘setState’);
// 这个 queue 对应的就是一个组件实例的 state 数组
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// enqueueUpdate 用来处理当前的组件实例
enqueueUpdate(internalInstance);
}
这里我总结一下,enqueueSetState
做了两件事:
-
将新的
state
放进组件的状态队列里; -
用
enqueueUpdate
来处理将要更新的实例对象
继续往下走,看看 enqueueUpdate
做了什么:
function enqueueUpdate(component) {
ensureInjected();
// 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
if (!batchingStrategy.isBatchingUpdates) {
// 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
这个 enqueueUpdate
非常有嚼头,它引出了一个关键的对象——batchingStrategy
,该对象所具备的isBatchingUpdates
属性直接决定了当下是要走更新流程,还是应该排队等待;其中的batchedUpdates
方法更是能够直接发起更新流程。由此我们可以大胆推测,batchingStrategy
或许正是 React
内部专门用于管控批量更新的对象。
接下来,我们就一起来研究研究这个 batchingStrategy
。
/**
* batchingStrategy源码
**/
var ReactDefaultBatchingStrategy = {
// 全局唯一的锁标识
isBatchingUpdates: false,
// 发起更新动作的方法
batchedUpdates: function(callback, a, b, c, d, e) {
// 缓存锁变量
var alreadyBatchingStrategy = ReactDefaultBatchingStrategy.isBatchingUpdates
// 把锁“锁上”
ReactDefaultBatchingStrategy.isBatchingUpdates = true
if (alreadyBatchingStrategy) {
callback(a, b, c, d, e)
} else {
// 启动事务,将 callback 放进事务里执行
transaction.perform(callback, null, a, b, c, d, e)
}
}
}
batchingStrategy
对象并不复杂,你可以理解为它是一个“锁管理器”。
这里的“锁”,是指 React 全局唯一的 isBatchingUpdates
变量,isBatchingUpdates
的初始值是 false
,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate
去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents
里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。
理解了批量更新整体的管理机制,还需要注意 batchedUpdates
中,有一个引人注目的调用:
transaction.perform(callback, null, a, b, c, d, e)
这行代码为我们引出了一个更为硬核的概念——React 中的 Transaction
(事务)机制。
理解 React 中的 Transaction(事务) 机制
Transaction
在 React 源码中的分布可以说非常广泛。如果你在 Debug React 项目的过程中,发现函数调用栈中出现了 initialize
、perform
、close
、closeAll
或者 notifyAll
这样的方法名,那么很可能你当前就处于一个 Trasaction
中
Transaction
在 React 源码中表现为一个核心类,React 官方曾经这样描述它:Transaction 是创建一个黑盒,该黑盒能够封装任何的方法
。因此,那些需要在函数运行前、后运行的方法可以通过此方法封装(即使函数运行中有异常抛出,这些固定的方法仍可运行),实例化 Transaction 时只需提供相关的方法即可。
这段话初读有点拗口,这里我推荐你结合 React 源码中的一段针对 Transaction
的注释来理解它:
*
* wrappers (injected at creation time)
* + +
* | |
* ±----------------|--------|--------------+
* | v | |
* | ±--------------+ | |
* | ±-| wrapper1 |—|----+ |
* | | ±--------------+ v | |
* | | ±------------+ | |
* | | ±—| wrapper2 |--------+ |
* | | | ±------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | ±–+ ±–+ ±--------+ ±–+ ±–+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* ±---------------->|-|—|-|—|–>|anyMethod|—|—|-|—|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | ±–+ ±–+ ±--------+ ±–+ ±–+ |
* | initialize close |
* ±----------------------------------------+
*
说白了,Transaction
就像是一个“壳子”,它首先会将目标函数用 wrapper
(一组 initialize
及 close
方法称为一个 wrappe
r) 封装起来,同时需要使用 Transaction
类暴露的 perform
方法去执行它。如上面的注释所示,在 anyMethod 执行之前,perform 会先执行所有 wrapper 的 initialize 方法,执行完后,再执行所有 wrapper 的 close 方法。这就是 React 中的事务机制。
“同步现象”的本质
下面结合对事务机制的理解,我们继续来看在 ReactDefaultBatchingStrategy
这个对象。ReactDefaultBatchingStrategy
其实就是一个批量更新策略事务,它的 wrapper
有两个:FLUSH_BATCHED_UPDATES
和 RESET_BATCHED_UPDATES
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
我们把这两个 wrapper
套进 Transaction
的执行机制里,不难得出一个这样的流程:
到这里,相信你对 isBatchingUpdates
管控下的批量更新机制已经了然于胸。但是 setState
为何会表现同步这个问题,似乎还是没有从当前展示出来的源码里得到根本上的回答。这是因为 batchingUpdates
这个方法,不仅仅会在 setState
之后才被调用。若我们在 React 源码中全局搜索 batchingUpdates
,会发现调用它的地方很多,但与更新流有关的只有这两个地方:
// ReactMount.js
_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
// 实例化组件
var componentInstance = instantiateReactComponent(nextElement);
// 初始渲染直接调用 batchedUpdates 进行同步渲染
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context
);
…
}
这段代码是在首次渲染组件时会执行的一个方法,我们看到它内部调用了一次 batchedUpdates
,这是因为在组件的渲染过程中,会按照顺序调用各个生命周期函数。开发者很有可能在声明周期函数中调用 setState
。因此,我们需要通过开启 batch 来确保所有的更新都能够进入 dirtyComponents
里去,进而确保初始渲染流程中所有的 setState
都是生效的。
下面代码是 React 事件系统的一部分。当我们在组件上绑定了事件之后,事件中也有可能会触发 setState。为了确保每一次 setState 都有效,React 同样会在此处手动开启批量更新。
// ReactEventListener.js
dispatchEvent: function (topLevelType, nativeEvent) {
…
try {
// 处理事件
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
话说到这里,一切都变得明朗了起来:isBatchingUpdates
这个变量,在 React 的生命周期函数以及合成事件执行前,已经被 React 悄悄修改为了 true,这时我们所做的 setState
操作自然不会立即生效。当函数执行完毕后,事务的 close 方法会再把 isBatchingUpdates
改为 false。
以开头示例中的 increment 方法为例,整个过程像是这样:
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
等大厂,18年进入阿里一直到现在。**
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-oHpQFLyb-1715591126930)]
[外链图片转存中…(img-K36e65xR-1715591126931)]
[外链图片转存中…(img-v3tkOcI0-1715591126932)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!