MobX 是一个用于管理应用程序状态的库,它的核心是透明的函数式响应编程,即当数据变化时,相关的视图或计算会自动更新。

MobX 使用单向数据流,其中操作改变状态,进而更新所有受影响的视图。

Alt 单向数据流

前置知识

修饰器 Decorator

类的修饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Calculator{
@logger
add(a,d){
return a+b;
}
}

function logger(target,name,descriptor){
let oldValue = descriptor.value
descriptor.value = function(){
console.log(`${name}(${Array.prototype.join.call(arguments,',')})`)
return oldValue.apply(this,arguments)
}
}

let c1 = new Calculator()
c1.add(1,2)

Proxy

基本用法

Proxy 对象接受两个参数:目标对象(target)和处理器对象(handler)。

target:这是要被代理的对象。
handler:这是一个对象,其属性是当执行操作时定义的函数(称为“陷阱”),例如 get, set, has, deleteProperty, apply, 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建一个目标对象
let target = {};

// 创建一个处理器对象,定义对目标对象的拦截操作
let handler = {
// 当读取属性时触发
get: function(obj, prop) {
return prop in obj ? obj[prop] : `Property ${prop} not found`;
},
// 当设置属性时触发
set: function(obj, prop, value) {
obj[prop] = value;
console.log(`Property ${prop} set to ${value}`);
return true;
}
};

// 创建一个代理对象
let proxy = new Proxy(target, handler);

// 测试代理对象
console.log(proxy.name); // 输出:Property name not found
proxy.name = 'Kimi'; // 设置属性,并输出:Property name set to Kimi
console.log(proxy.name); // 输出:Kimi

Observables(可观察对象)

可观察对象是 MobX 6 中的基础元素。它们可以是普通的 JavaScript 变量、对象、数组等。当这些可观察对象的值发生变化时,MobX 会自动通知依赖于这些值的 reactions。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { makeAutoObservable } from "mobx";

class Todo {
title = "";
finished = false;

constructor(title) {
this.title = title;
makeAutoObservable(this);
}

toggle() {
this.finished = !this.finished;
}
}

Reactions(反应)

反应是指那些会对可观察对象的变化做出响应的函数。常见的反应有 autorun 和 reaction。
它们很少被直接使用到,通常被封装在其他库中(比如 mobx-react)或者是特定于您的应用程序的抽象。

使用 autorun

autorun 会在其首次创建时运行一次,并在任何依赖的可观察对象发生变化时再次运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { autorun } from "mobx";

const todo = new Todo("Learn MobX");

autorun(() => {
console.log(`Todo: ${todo.title}, Finished: ${todo.finished}`);
});

// 输出: Todo: Learn MobX, Finished: false

todo.toggle();
// 输出: Todo: Learn MobX, Finished: true

mobx-react 的 Observer 函数

它是一个高阶组件(Higher-Order Component,HOC),用于将 React 组件转换为响应式组件。当与 MobX 一起使用时,Observer 可以使组件在 MobX 状态发生变化时自动重新渲染。

工作原理

实现一个简易的响应式系统

Mobx 是一个典型的观察-反应模型。这个模型可以理解为观察者模式,但是这个观察者模式的重点在于订阅,而这个模型的重点在于依赖收集。

实现 observable

熟悉 Vue 或者 ES6 的同学都知道如何检测到一个普通对象的读取,无非是 Object.defineProperty 或者 Proxy。借助其中一个 api,便可以实现简单的 observable 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function observable(target) {
const _target = {...target}
let proxy = {};

Object.keys(target).forEach(key => {
Object.defineProperty(proxy, key, {
get() {
const value = _target[key]
console.log(`read ${key} = ${value}`)
return value
},
set(value) {
console.log(`set ${key} = ${value}`)
_target[key] = value;
return value;
}
})
})
return proxy;
}

依赖收集与反应:从实现 autorun 开始

autorun 接收一个函数做参数,并且有个特性:它会立即执行参数 fn。

当 fn 函数执行的时候,如果依赖到了某个 observable,则必定会触发它的某个 key 的 getter,而此时也就可以确定,fn 依赖了 observable 的这个 key。

1
2
3
4
5
let watcher = null;
function autorun(fn) {
watcher = fn;
fn()
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 定义一个全局变量 watcher,用于保存当前的观察函数
let watcher = null

// autorun 函数用于注册一个观察函数 fn
function autorun(fn) {
// 将全局 watcher 设置为传入的函数
watcher = fn
// 立即执行传入的函数
fn()
}

// observable 函数用于将一个普通对象转换为可观察对象
function observable(target) {
// 创建对象的浅拷贝
const _target = { ...target }
// 创建一个空对象 proxy,将作为返回的代理对象
let proxy = {}
// 创建一个 Map,用于存储各个属性的观察者集合
let subs = new Map()

// 遍历 target 对象的所有属性
Object.keys(target).forEach((key) => {
// 为每个属性定义 getter 和 setter
Object.defineProperty(proxy, key, {
get() {
// 如果存在全局 watcher,则将其添加到当前属性的观察者集合中
if (watcher) {
let watchers = subs.get(key)
if (!watchers) {
// 如果当前属性还没有观察者集合,则创建一个新的集合
subs.set(key, new Set([watcher]))
} else {
// 否则将当前 watcher 添加到现有集合中
watchers.add(watcher)
}
}
// 获取属性值
const value = _target[key]
// 输出读取日志
// console.log(`read ${key} = ${value}`)
return value
},
set(value) {
// 输出设置日志
// console.log(`set ${key} = ${value}`)
// 更新属性值
_target[key] = value
// 获取当前属性的观察者集合
let watchers = subs.get(key)
// 如果存在观察者,则逐一执行观察者
if (watchers) {
watchers.forEach((watcher) => {
watcher()
})
}
// 返回设置的值
return value
},
})
})
// 返回代理对象
return proxy
}

// 创建一个可观察对象 data,初始值为 { age: 10, name: "Zchary" }
const data = observable({ age: 10, name: "Zchary" })

// 注册一个 autorun 函数,用于自动运行并输出 data 对象的状态
autorun(() => {
console.log(`autorun: ${data.name} is ${data.age} years old`)
})

// 通过定时器每隔 1 秒递增 data.age 属性的值,共执行 5 次
for (let i = 0; i < 5; i++) {
setTimeout(() => {
data.age++
}, 1000 * (i + 1))
}