MobX 是一个 JavaScript 状态管理库,它通过响应式编程的方式实现了高效的数据管理,让开发者可以更加简单地管理应用程序中的状态。本文将详细讲解 MobX 的实现原理,并带领读者一步步实现一个简单全面的 MobX。
MobX 的实现原理
响应式编程
MobX 的核心思想是响应式编程,也就是通过监听数据的变化来触发相关的操作。在 MobX 中,我们可以将数据(也称为状态)定义为可观察的对象,一旦数据发生变化,相关的操作就会自动执行。
import { makeObservable, observable, autorun } from 'mobx';
class Counter {
count = 0;
constructor() {
makeObservable(this, {
count: observable,
});
}
increment() {
this.count++;
}
}
const counter = new Counter();
autorun(() => {
console.log(`Count: ${counter.count}`);
});
counter.increment(); // 输出 "Count: 1"
counter.increment(); // 输出 "Count: 2"
在上面的例子中,我们创建了一个 Counter 类,并将其 count 属性定义为可观察的对象。然后,我们创建了一个 autorun 函数来监听 count 的变化,并在控制台上输出当前的计数值。最后,我们调用 increment 方法两次,使计数器的值从 0 增加到 2。每次计数器的值发生变化时,autorun 函数都会自动执行,并输出当前的计数值。
MobX 的工作流程
在 MobX 中,每一个可观察的对象都会被封装成一个 ObservableValue,当可观察的对象的属性发生变化时,ObservableValue 会自动通知所有依赖于它的函数或计算属性(ComputedValue)进行更新。
例如,在下面的代码中,我们定义了一个名为 greeting 的计算属性,它会根据 name 属性的值来生成一个问候语。
import { makeObservable, observable, computed } from 'mobx';
class Greeter {
name = '';
constructor() {
makeObservable(this, {
name: observable,
greeting: computed,
});
}
get greeting() {
return `Hello, ${this.name}!`;
}
}
const greeter = new Greeter();
console.log(greeter.greeting); // 输出 "Hello, !"
greeter.name = 'Tom';
console.log(greeter.greeting); // 输出 "Hello, Tom!"
在上面的例子中,我们创建了一个 Greeter 类,并将它的 name 属性定义为可观察的对象,将 greeting 属性定义为计算属性。当 name 属性发生变化时,greeting 属性会自动更新。
MobX 的工作流程可以分为三个步骤:
- 当可观察的对象的属性发生变化时,它会通知所有依赖于它的 ObservableValue 进行更新。
- ObservableValue 会通知所有依赖于它的计算属性进行更新。
- 计算属性会重新计算它的值,并通知所有依赖于它的 ObservableValue 和计算属性进行更新,以此类推,直到所有的依赖关系都被更新完成。
源码剖析
现在让我们来看一下 MobX 的源码,理解它是如何实现响应式编程的。
ObservableValue
首先,我们来看一下 ObservableValue 的实现。ObservableValue 是 MobX 中的核心类之一,它用于封装可观察的对象。
class ObservableValue {
value;
constructor(value) {
this.value = value;
}
get() {
// ...
}
set(newValue) {
// ...
}
// ...
}
在上面的代码中,我们定义了一个 ObservableValue 类,并给它添加了一个 value 属性,用于存储可观察对象的值。ObservableValue 还定义了 get 和 set 方法,用于获取和设置可观察对象的值。
当可观察对象的值发生变化时,我们需要通知所有依赖于它的计算属性进行更新。为了实现这个功能,我们需要在 ObservableValue 中维护一个依赖关系列表,当可观察对象的值发生变化时,我们遍历这个列表,并逐个通知所有的计算属性进行更新。
class ObservableValue {
value;
observers = new Set();
constructor(value) {
this.value = value;
}
get() {
// ...
}
set(newValue) {
if (newValue !== this.value) {
this.value = newValue;
this.notifyObservers();
}
}
addObserver(observer) {
this.observers.add(observer);
}
removeObserver(observer) {
this.observers.delete(observer);
}
notifyObservers() {
this.observers.forEach(observer => observer());
}
}
在上面的代码中,我们给 ObservableValue 添加了一个 observers 属性,用于存储所有依赖于它的计算属性。当可观察对象的值发生变化时,我们调用 notifyObservers 方法,遍历 observers 列表,并逐个通知所有的计算属性进行更新。
ComputedValue
接下来,我们来看一下 ComputedValue 的实现。ComputedValue 是一个计算属性,它用于根据其他可观察对象的值来计算它自己的值。
class ComputedValue {
get;
dependencies = new Set();
constructor(get) {
this.get = get;
}
compute() {
// ...
}
addObserver(observer) {
this.dependencies.add(observer);
}
removeObserver(observer) {
this.dependencies.delete(observer);
}
notifyObservers() {
this.dependencies.forEach(dependency => dependency());
}
}
在上面的代码中,我们定义了一个 ComputedValue 类,并给它添加了一个 get 属性,用于计算计算属性的值。ComputedValue 还定义了一个 dependencies 属性,用于存储它所依赖的可观察对象。当计算属性的值发生变化时,我们需要通知所有依赖于它的可观察对象进行更新。为了实现这个功能,我们在 ComputedValue 中也维护一个依赖关系列表,当计算属性的值发生变化时,我们遍历这个列表,并逐个通知所有的可观察对象进行更新。
class ComputedValue {
get;
dependencies = new Set();
observers = new Set();
constructor(get) {
this.get = get;
}
compute() {
const oldValue = this.value;
const newValue = this.get();
if (newValue !== oldValue) {
this.value = newValue;
this.notifyObservers();
}
return this.value;
}
addObserver(observer) {
this.dependencies.add(observer);
}
removeObserver(observer) {
this.dependencies.delete(observer);
}
addReaction(reaction) {
this.observers.add(reaction);
}
removeReaction(reaction) {
this.observers.delete(reaction);
}
notifyObservers() {
this.dependencies.forEach(dependency => dependency());
this.observers.forEach(observer => observer());
}
}
在上面的代码中,我们给 ComputedValue 添加了一个 observers 属性,用于存储所有依赖于它的计算属性和可观察对象。当计算属性的值发生变化时,我们调用 notifyObservers 方法,遍历 dependencies 和 observers 列表,并逐个通知所有的依赖关系进行更新。
autorun
最后,我们来看一下 autorun 的实现。autorun 是一个自动运行的函数,它用于创建一个计算属性,并自动运行这个计算属性。
function autorun(fn) {
const reaction = new Reaction(fn);
reaction.run();
return () => reaction.dispose();
}
class Reaction {
fn;
cleanupFunc;
dirty = true;
constructor(fn) {
this.fn = fn;
}
schedule() {
this.dirty = true;
scheduler.schedule(this.run.bind(this));
}
run() {
if (this.dirty) {
this.cleanup();
this.track();
this.dirty = false;
}
}
track() {
startBatch();
try {
this.fn();
} finally {
endBatch();
}
}
cleanup() {
if (this.cleanupFunc) {
this.cleanupFunc();
this.cleanupFunc = null;
}
}
dispose() {
if (!this.cleanupFunc) {
return;
}
this.cleanup();
this.dirty = false;
}
}
在上面的代码中,我们定义了一个 autorun 函数,它接受一个函数 fn 作为参数,并创建一个 Reaction 对象,用于自动运行 fn 函数。在 Reaction 中,我们定义了一些方法,用于跟踪 fn 函数中所有被使用的可观察对象,并在这些可观察对象的值发生变化时重新运行 fn 函数。
为了实现这个功能,我们需要在 track 方法中启动一个 batch,并在 fn 函数中使用 mobx.trace 函数来收集所有被使用的可观察对象。在 track 方法结束时,我们结束 batch,并通知所有被使用的可观察对象添加 Reaction,以便在它们的值发生变化时重新运行 fn 函数。
function startBatch() {
batchDepth++;
}
function endBatch() {
batchDepth--;
if (batchDepth === 0) {
runReactions();
}
}
function runReactions() {
const reactions = Array.from(reactionsQueue);
reactionsQueue.clear();
reactions.forEach(reaction => reaction.run());
}
在上面的代码中,我们定义了三个函数,startBatch、endBatch 和 runReactions,它们用于实现 batch 的功能。当 startBatch 函数被调用时,我们将 batchDepth 加 1;当 endBatch 函数被调用时,我们将 batchDepth 减 1,并在 batchDepth 变为 0 时运行所有 Reaction。在 runReactions 函数中,我们先将 reactionsQueue 中的所有 Reaction 取出来,并逐个调用它们的 run 方法。
至此,我们已经完成了一个简单全面的 mobx 实现。虽然这个实现还有许多可以改进的地方,但它已经可以满足基本的需求,并且可以让我们更好地理解 mobx 的实现原理。