一、前言
批量更新机制应该是前端框架中一个老生常谈的问题了,这些知识也是面试官比较喜欢问的,那么在不同的技术框架背景下,处理批量更新的手段也是各不相同,今天我们主要来探讨一下 Mobx 的批量更新机制,在学习本章之前需要对 Mobx 的响应式机制有所了解,具体可阅读我这篇文章 最通俗易懂的Mobx响应式机制
二、原理介绍
下面通过这个案例进行介绍:
class Store {
count = 1;
name = 'xl';
age = 12;
batchCount = true;
change() {
this.count = 2
this.count = 3
this.count = 4
this.name = 'xl1'
this.age = 13
}
batchCount = false;
constructor() {
makeObservable(this, {
count: observable,
name: observable,
age: observable,
change: action
})
autorun(() => {
console.log(this.count)
console.log(this.name)
console.log(this.age)
})
}
}
const store = new Store();
store.change();
在这个例子中,我们创建可以一个副作用(reaction),他会监听 count、name 以及 age 的变化,一旦这些值发生变化该富足用就会自动执行。同时我们也定义了一个 change 方法,在这个方法中我们多次更新了 count 的值,同时也更新了 name 和 age,当我们执行 change 方法时最终副作用当然只会执行一次,下面我会一步步进行讲解。
1、startBatch和endBatch
在介绍批量更新的原理之前我们先来了解两个比较重要的函数 startBatch 和 endBatch,顾名思义 startBatch 表示当前开始进行批量更新了,endBatch 表示批量更新已经结束了,是否处于批量更新其实就是通过一个全局变量 inBatch 来控制,当 inBatch>0 时表示当前处于批量更新。
这里要注意了 inBatch 是 number 类型的,学过 react 的同学可能了解过 React16 是怎么实现批量更新的,他是通过一个 boolare 类型的变量来控制的,当变量为 true 时表示当前正在进行批量更新,那为什么 mobx 要用一个 number 类型的变量来控制呢?下面会具体介绍的。
我们先看一下 startBatch 的实现:
function startBatch() {
// 将inBatch执行++操作
globalState.inBatch++;
}
这个方法很简单就是执行 inBatch++,下面看下 endBatch 函数。
function endBatch() {
// 执行inBatch--操作
if (--globalState.inBatch === 0) {
// 此时批量更新已经结束了,需要执行批量更新过程中收集到的所有的副作用了,因为在批零更新的过程中副作用是不会被执行的而是会被收集起来,在这里被统一执行。
runReactions();
// ...
}
}
// runReactions
function runReactions() {
// 如果当前处于批量更新则不执行当前的副作用
if (globalState.inBatch > 0) {
return;
}
// 在批量更新或者普通更新的过程中所有的副作用都会被存放到pendingReactions上,注意一个副作用只会入队一次
var allReactions = globalState.pendingReactions;
var remainingReactions = allReactions.splice(0);
// 遍历所有的副作用,调用他们的runReaction_方法
for (var i = 0, l = remainingReactions.length; i < l; i++) {
remainingReactions[i].runReaction_();
}
}
看到这里大家应该对批量更新因该有一定的了解了,下面来解答几个问题:
1、mobx 为什么要用一个 number 类型的变量来处理批量更新? 我们来看一下这个例子并简单来模拟一下流程:
const pendingReactions = []
let inBatch = 0;
change() {
//进入action方法需要进行批量更新
// inBatch++;
// 触发count的setter方法需要进行批量更新并将依赖count的所有副作用添加到对列中
// inBatch++
//pendingReactions.push(reactions1)
this.count = 2
// count更新完需要结束批量更新,并执行队列中的副作用但是此时inBatch>0表示还在批量更新的流程中,所以暂时还不能执行
//inBatch--
// if(inBatch === 0){runReactions()}
// 后面的流程都一样
this.count = 3
this.count = 4
this.name = 'xl1'
this.age = 13
// action方法中的批量更新已经结束需要通知所有的副作用
// inBatch-- 此时inBatch=0
// if(inBatch === 0){runReactions()}
}
这样就比较明了了。
2、为什么把所有要执行的副作用都要托管到 pendingReactions
数组中?
这也是为了方便管理,比如我们直接执行一次 this.count = 2,那么队列中将会保存依赖 count 的所有副作用,当我们执行 change 方法时,pendingReactions
就会保存 依赖 count、name 和 age 的所有的副作用。
2、mobx是怎么处理注册的action方法?
在上面这个例子中我们将 change 方法标志为 action,我们来看一下 mobx 处理的结果:
我们看到 change 方法居然变成了一个叫 res 的方法,别急我们看到 res 方法中最终是调用了 executeAction 方法的,我们断点调试一下看看这个函数接收的各个参数是什么:
我们看到参数 fn 其实就是我们定义的 change 方法,下面就来看下在 executeAction 中是怎么处理 fn 方法的。
// 通知mobx开始执行action方法了,在这个方法中会执行startBatch方法
var runInfo = _startAction(actionName, canRunAsDerivation, scope, args);
try {
return fn.apply(scope, args); // 执行 change 方法
} catch (err) {
runInfo.error_ = err;
throw err;
} finally {
// 这个方法中会执行endBatch方法
_endAction(runInfo);
}
}
我们看到在 executeAction 方法中只做了三件事,一是调用 startBatch 方法通知开始进入批量更新了,二是调用 fn 函数,在这个函数会触发属性的 setter 方法将属性的副作用保存到 pendingReactions
中,三是调用 endBatch 方法,执行 pendingReactions
中所有的副作用。
2、副作用的收集
在最通俗易懂的Mobx响应式机制这篇文章中我们介绍过当我们触发了属性的 setter 方法时就会去通知依赖这个属性的所有的副作用重新执行,具体代码如下:
class ObservableValue{
// ....
// 更新value的方法
setNewValue_(newValue: T) {
const oldValue = this.value_
this.value_ = newValue
// 通知所有的副作用重新执行
this.reportChanged()
// ...
}
}
其实最关键的就是 reportChanged 方法,这是批量更新的入口,其实实现也很简单:
reportChanged() {
startBatch()
// 这个函数中会遍历当前属性的所有的副作用,最终会调用副作用的schedule_方法并将这个副作用push到pendingReactions中
propagateChanged(this)
endBatch()
}
//schedule_方法
_proto.schedule_ = function schedule_() {
if (!this.isScheduled_) {
this.isScheduled_ = true;
// this表示当前的副作用
globalState.pendingReactions.push(this);
runReactions();
}
};
讲到这里其实批量更新的原理就介绍完了,其实思路挺简单的,首先是通过 inBatch 来表示当前是否是在批量更新的流程中,其次就是将所有的副作用全部保存在一个队列中,当批量更新结束就执行这个队列中的副作用。
三、思考
通过上面的介绍其实大家也会发现一个问题,mobx 的批量更新机制也是同步的,如果我改成下面这样的写法其实就不会再进行批量更新了:
class Store {
count = 1;
name = 'xl';
age = 12;
change() {
setTimeout(() => {
this.count = 2
this.count = 3
this.count = 4
this.name = 'xl1'
this.age = 13
}, 2000)
}
constructor() {
makeObservable(this, {
count: observable,
name: observable,
age: observable,
change: action
})
autorun(() => {
console.log(this.count)
console.log(this.name)
console.log(this.age)
})
}
}
const store = new Store();
store.change();
其实这和 React16 中的批量更新机制的问题是一样的,如果我们是异步更新属性就会跳出批量更新机制,但是在 mobx 中也很好解决,只要我们使用 action 将我们的函数包裹住就行了,如下所示:
change() {
setTimeout(action(() => {
this.count = 2
this.count = 3
this.count = 4
this.name = 'xl1'
this.age = 13
}), 2000)
}
而 React16 也为我们提供了一个 unstable_batchedupdates
API,让我们手动去设置批量更新。