异步的动机——批量更新
批量更新是指:每来一个 setState
,就把它塞进一个队列里 “攒起来”,等时机成熟,再把 “攒起来” 的 state
结果合并,最后只针对最新的 state
值走一次更新流程。
this.setState({ count: this.state.count + 1 ===> 入队,[count + 1 的任务] }) this.setState({ count: this.state.count + 1 ===> 入队,[count + 1 的任务] }) this.setState({ count: this.state.count + 1 ===> 入队,[count + 1 的任务] }) // --> 合并 state,[count + 1 的任务] // --> 执行 count + 1 的任务
test = () => { for(let i = 0; i < 100; i++){ this.setState({ count: this.state.count + 1 }) } } // 该代码也只会增加任务入队的次数,并不会带来频繁的 Re-render // 100次结束后也仅仅是 state 的任务队列内容发生了变化,state 本身也不会发生改变
同步代码里的 setState
test = () => { setTimeout(() => { this.setState({ count: this.state.count + 1 }) }, 0) }
那为什么 setTimeout
可以把执行顺序从异步变成同步?
这里要注意的是:并不是 setTimeout
改变了 setState
,而是 seTimeout
帮助 setState
“逃脱” 了 React 对它的管控,只要是在 React 管控下的 setState
,一定是异步的。
setState 工作流
以下是 React 15 的 setState
,Fiber
之前的都是如此。
// setState React.prototype.setState = function(partialState, callback){ this.updater.enqueueSetState(this, partialState); if(callback){ this.updater.enqueueCallback(this, callback, 'setState'); } }
// enqueueSetState enqueueSetState: function(publicInstance, partialState){ // 根据 this 拿到对应的组件实例 var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState'); // 这个 queue 对应的就是一个组件实例的 state 数组 var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); // enqueueUpdate 用来处理当前的组件实例 enqeueUpdate(internalInstance) }
// 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; } } // 由此可以推断,batchingStrategy 就是 React 内部管理批量更新的对象
// batchingStrategy 源码 var ReactDefaultBatchingStrategy = { // 全局唯一的锁标识,false 表示当前没有进行批量更新操作 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) } } } // 每当 React 调用 batchedUpdates 去执行更新动作时,会先把 isBatchingUpdates 这个锁给 “锁上”,表明现在正处于批量更新过程中。当锁被锁上的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队,等待下一次的批量更新,而不能随意插队。 // 这里的任务锁机制,正是 React 面对大量状态,依然能够实现有序分批处理的基石。
transaction
它是 React 内部的事务机制,以下是官方的注释图:
* wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+
- anyMethod:代表要被包裹的方法
- wrapper:代表一层包裹容器,每个 wrapper 可选地包含一个 initialize(前置方法)和一个 close(后置方法),分别会在每次 anyMethod 函数执行前后者执行后执行
- perform:执行“ 包裹” 动作的 API,形式为:
transaction.perform(anyMethod)
,表示给 anyMethod 加上一层 wrapper - 可以有多个 wrapper,执行时按照 “包裹” 的顺序,依次执行对应的前置和后置函数。
再看 ReactDefaultBatchingStrategy
这个对象:
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 的机制里,可以得出:在 callback 执行完之后,RESET_BATCHED_UPDATES 将 isBatchingUpdates 置为 false,FLUSH_BATCHED_UPDATES 会执行 flushBatchedUpdates,然后里面会循环所有 dirtyComponents,调用自己的 updateComponent 来执行所有的生命周期方法,最后实现组件的更新。
与更新流程相关的 isBatchingUpdates
// 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 都是生效的。
// ReactEventListener.js dispatchEvent: function(topLevelType,NativeEvent){ // ... try{ // 处理事件 ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); } finally { TopLevelCallbackBookKeeping.release(bookKeeping); } } // 该代码是 React 的事件系统的一部分,当在组件上绑定了事件,事件也有可能触发 setState,为了确保每一次 setState 都有效,React 会在此处手动开启批量更新。
isBatchingUpdates
在 React 的生命周期函数以及合成事件执行前,已经被 React 悄悄修改为了 true
,这时开发者所做的 setState
操作,自然不会立即生效,当函数执行完毕后,事务的 close
方法会再次把 isBatchingUpdates
改为 false
。
以下是使用和不使用 setTimeout 的伪代码:
// 默认情况下的 setState test = () => { // 进来先锁上 isBatchingUpdates = true this.setState({ count: this.state.count + 1 }) // 执行完之后再解锁放开 isBatchingUpdates = false } // 使用 setTimeout 后 test = () => { // 进来先锁上 isBatchingUpdates = true setTimeout(() => { this.setState({ count: this.state.count + 1 }) }, 0) // 执行完之后再解锁放开 isBatchingUpdates = false } // 可以看出来默认情况下,setState 只能是异步的,而有 setTimeout 的话,setState 的逻辑就会异步执行,而此时 isBatchingUpdates 早已经被修改为 false 了!这也就是为什么 setTimeout 里的 setState 具备当下立刻发起同步更新的能力。
总结
setState 的表现会因为场景的不同而不同:
- 在 React 的钩子函数及合成事件中,它表现为 异步。
- 在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为 同步。