0%

mobX 学习笔记

学习使用 Mobx

在知乎上看到了一个很好的使用 mobx 配合 hooks 使用的例子:

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
// 假设我们有一个Activity store 于activityStore.ts
import { observable, action } from 'mobx';
export default class ActivityStore {
@observable activityList: Array = [];

@action.bound fetchActivityList() {
return fetch("/activity").then(
action(data => { //用 action 在then后继续绑定当前实例
this.activityList = this.activityList.concat(data)
})
);
}

}

// 收集所有store于 stores.ts
import ActivityStore from "./activityStore"
const storeContext = React.createContext({
activityStore: new ActivityStore()
})
const useStores = () => React.useContext(storeContext); // 利用 context hook
export default useStores;

// 任何一个需要store的组件,例如 Activity.tsx
import React, { useEffect } from "react"
import useStores from './stores'
import {observer} from "mobx-react"

export default observer(function Activity() { // 套上obeserver
const { activityStore } = useStores(); // 在 React 里 use 一切

useEffect(() => {
activityStore.fetActivityList().then(() =>
console.log("fetch OK!")
})
}, [])

return <div>activityStore.activityList.map(activity => activity.name)</div>
})

我在项目里采用的也是这种使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store/index.ts
import {useContext, createContext} from "react";
import AuthStore from "./auth";
import UserStore from "./user"
import ImageStore from "./image"
import HistoryStore from './history';


const context = createContext({
AuthStore,
UserStore,
ImageStore,
HistoryStore
})

export const useStore = () => useContext(context)

接下来总结下 mobx 和 mobx-react 的主要 api:

@observable

observable 是一种让数据的变化可以被观察的方法,底层是通过和 Vue 2 相同的 Object.defineProperty() 来实现的,把该属性转化成 getter /setter。在 Mobx 5 中,使用 es6 中的 proxy 的来实现数据的变化监听,从而避免出现了跟 Vue2 中一样的问题。

computed

计算值是可以根据现有的状态或其他计算值衍生出的值。如果任何影响计算值的值发生变化了,计算值将根据状态自动更新。

autorun

直译的意思就是自动运行,使用 autorun 时,所提供的函数会立即被触发一次,以观察哪些可观察数据被引用,然后每次它的依赖关系改变时会自动运行。autorun 的作用是在可观察数据被修改之后,自动去执行依赖可观察数据的行为,这个行为一直就是传入 autorun 的函数。

传递给 autorun 的函数在调用后接受一个参数,即当前 reaction(autorun),可用于在执行期间清理 autorun

1
2
3
4
5
6
7
8
9
10
11
var numbers = observable([1,2,3]);
var sum = computed(() => numbers.reduce((a, b) => a + b, 0));

var disposer = autorun(() => console.log(sum.get()));
// 输出 '6'
numbers.push(4);
// 输出 '10'

disposer();
numbers.push(5);
// 不会再输出任何值。`sum` 不会再重新计算。

autorun 和 computed

  • 它们都是响应式调用的表达式
  • @computed 用于响应式的产生一个可以被其他 observer 使用的值,autorun 不产生新的值,而是达到一个效果(打印日志,发起网络请求等命令式的副作用)
  • @computed 中如果一个计算值不再被观察了,MobX 可以自动地将其垃圾回收,而 autorun 中的值必须要手动清理才行

when

接收两个函数参数,第一个函数必须根据可观察数据来返回一个布尔值,当该布尔值为 true 时,才会去执行第二个函数,并且只会执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { observable, when } from 'mobx'
class Leo {
@observable str = 'leo';
@observable num = 123;
@observable bool = false;
}

let leo = new Leo()
when(() => leo.bool, () => {
console.log('这是true')
})
leo.bool = true
// 这是true

注意

  1. 第一个参数,必须是根据可观察数据来返回的布尔值,而不是普通变量的布尔值。
  2. 如果第一个参数默认值为 true ,则 when 函数会默认执行一次。

reaction

高级版的 autorun ,对于如何追踪 observable 赋予了更细粒度的控制。接受两个函数参数,第一个函数引用可观察数据,并返回一个可观察数据,作为第二个函数的参数。

reaction 第一次渲染的时候,会先执行一次第一个函数,这样 MobX 就会知道哪些可观察数据被引用了。随后在这些数据被修改的时候,执行第二个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { observable, reaction } from 'mobx'
class Leo {
@observable str = 'leo';
@observable num = 123;
@observable bool = false;
}

let leo = new Leo()
reaction(() => [leo.str, leo.num], arr => {
console.log(arr)
})
leo.str = 'pingan'
leo.num = 122
// ["pingan", 122]
// ["pingan", 122]

这里我们依次修改 leo.str 和 leo.num 两个变量,会发现 reaction 方法被执行两次,在控制台输出两次结果 [“pingan”, 122] ,因为可观察数据 str 和 num 分别被修改了一次。

实际使用场景

当我们没有获取到数据的时候,没有必要去执行存缓存逻辑,当第一次获取到数据以后,就执行存缓存的逻辑。

@action

action 是修改任何状态的东西,任何修改状态的函数都应该使用 @action。Mobx 6 的默认配置中 enforceActions 设置为 true,也就是说任何修改状态的操作都需要通过 action 来完成。

runInAction

runInAction 是个简单的工具函数,它接收代码块并在(异步的)动作中执行。这对于即时创建和执行动作非常有用,例如在异步函数过程中。runInAction(f) 是 action(f)() 的语法糖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { observable, computed, reaction, action} from 'mobx'
class Store {
@observable string = 'leo';
@observable number = 123;
@action.bound bar(){
this.string = 'pingan'
this.number = 100
}
}
let store = new Store()
reaction(() => [store.string, store.number], arr => {
console.log(arr)
})
runInAction(() => {
store.string = 'pingan'
store.number = 100
})//["pingan", 100]

observer

observer 函数/装饰器 是由 mobx-react 提供的,可以把React 组件转换成响应式组件。它使用 autoRun 包装了组件的 render 函数以确保任何组件渲染中的使用的数据变化都可以强制刷新组件。而当数据没有变化时,它同样确保了组件不会重新渲染,类似于 React PureComponent。

何时使用 observer

所有使用 observable 数据的组件都应该使用 observer 来确保数据变化时组件能够响应。

踩坑

Mobx 使用第三方组件

在上一篇文章里,我提到了使用 antd 组件没有重新渲染的问题。原因文档中也有提到

解决方法上篇文章已经提过,就不再赘述了。

Mobx 追踪属性访问,而不是值

MobX 追踪的是属性访问而不是值,值本身是不可观察的。

1
2
3
4
5
6
7
8
9
let message = observable({
title: "Foo",
author: {
name: "Michel"
},
likes: [
"John", "Sara"
]
})

observed-refs

官方文档给出了一个陷阱的例子。我稍微进行了改造,代码如下:

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
import { observer } from "mobx-react-lite";
import { makeAutoObservable } from "mobx";

class Timer {
secondsPassed = 0;
constructor() {
makeAutoObservable(this);
}
increaseTimer() {
this.secondsPassed += 1;
}
}
const myTimer = new Timer();
const TimerView = observer(({ secondsPassed }) => <span>{secondsPassed}</span>);
// 加上 observer 试试看
const App = () => {
console.log("App 执行了");
return (
<div className="App">
<TimerView secondsPassed={myTimer.secondsPassed} />
<button onClick={() => myTimer.increaseTimer()}>+1</button>
</div>
);
};
export default App;

可以点击这个链接在线运行这个例子,点击 +1 按钮什么都不会发生。原因是 App 组件在点击 +1 按钮时不会重新渲染,而对于 TimerView 组件来说 props.secondsPassed 属性指向 0 从未改变过,而 0 是一个不可变值,所以 TimerView 组件并不会重新渲染。

如果把 App 变成 observer 组件就可以看到不同,原因是 myTimer.secondsPassed 的变化会触发App 组件的重新渲染。而 TimeView 组件接收到的参数也会变化从而触发 TimeView 组件的重新渲染。不过这里我想说的是另一种改造方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Timer {
secondsPassed = {
value: 0
};
constructor() {
makeAutoObservable(this);
}
increaseTimer() {
this.secondsPassed.value += 1;
}
}
const myTimer = new Timer();
const TimerView = observer(({ secondsPassed }) => <span>{secondsPassed.value}</span>);
const App = () => {
console.log("App 执行了");
return (
<div className="App">
<TimerView secondsPassed={myTimer.secondsPassed} />
<button onClick={() => myTimer.increaseTimer()}>+1</button>
</div>
);
};

在上面的情况下,App 组件在点击 +1 按钮时同样不会重新渲染,但对于 TimerView 组件来说则有所不同。props.secondsPassed 属性指向 myTimer.secondsPassed 所指向的 Object {value : 0},而 myTimer.secondsPassed.value 的指向发生了变化,由 0 变成了 1。所以 TimerView 会重新渲染。

越晚引用越好

在 MobX 中英文版本的文档中都有类似的例子:

When using mobx-react it is recommended to dereference values as late as possible. This is because MobX will re-render components that dereference observable values automatically. If this happens deeper in your component tree, less components have to re-render.

Slower

1
<DisplayName name={person.name} />

Faster

1
<DisplayName person={person} />

起初我其实不太理解为什么下面的写法会更好,上面的写法看起来似乎更简单易懂。主要是也没仔细看文档,最近才意识到这个问题。如果按照上面的写法 person.name 的变化不仅会触发 DisplayName 组件的重新渲染,也会触发当前组件的重新渲染。因为当前组件也使用了 person.name。而第二种写法 person.name 的变化只会触发 DisplayName 组件的重新渲染。

在 文档的 React 优化部分提到的尽可能使用多的小组件而不是把数据集中在一个组件也是同样的原因。

文档

还有一些重要的内容我可能没有提及,文档中的这些部分也很有必要阅读

总结

在使用了 mobx 之后,总有种在使用 Vue 的感觉。mobx 跟 Vue 的原理是很相似的,它们的数据响应式实现原理是一致的。Vue 2 跟 mobX 4 使用了 Object.defineProperty ,Vue 3 和 Mobx 5 使用了 Proxy。mobX 跟 Vue 的 api 也有点像,computed 跟 autorun,感觉有点类似 Vue 里的 computed 跟 watch。不过以上更多是个人的一点感想和总结,如果有不对的地方还请谅解,并给予指正。非常感谢!


参考: