什么是MobX
在一些react项目中,我们能看到Mobx的身影,被用来做任务状态管理工具,它通过运用透明的函数式响应编程(Transparent Functional Reactive Programming,TFRP)使状态管理变得简单和可扩展;
它有下面几个有特点
- 简单直接:编写无模板的极简代码来精准描述出你的意图。要更新一个记录字段?使用熟悉的 JavaScript 赋值就行。要在异步进程中更新数据?不需要特殊的工具,响应性系统会侦测到你所有的变更并把它们传送到其用武之地。
- 轻松实现最优渲染:所有对数据的变更和使用都会在运行时被追踪到,并构成一个截取所有状态和输出之间关系的依赖树。这样保证了那些依赖于状态的计算只有在真正需要时才会运行,就像 React 组件一样。无需使用记忆化或选择器之类容易出错的次优技巧来对组件进行手动优化。
- 架构自由:MobX 不会用它自己的规则来限制你,它可以让你在任意 UI 框架之外管理你的应用状态。这样会使你的代码低耦合、可移植和最重要的——容易测试。
版本说明
- Mobx4可以运行在任何支持ES5语法的浏览器;
- Mobx5版本运行在任何支持ES6语法的浏览器;
- Mobx4和Mobx5具有相同的api,都需要使用装饰器语法;
- Mobx6是目前最新的版本,为了兼顾与标准JavaScript的最大兼容性,默认情况下放弃了装饰器语法。
基本实现与使用
MobX的几个核心:
observable:定义一个存储state的可追踪字段(Proxy)
action:将一个方法标记为可修改state的action
computed:标记一个可以由state派生出新值并且缓存其输出的计算属性
工作流程如下:
observable
刚开始接触的同学可能会有点懵,什么是observable?
observable是一种让数据的变化可以被观察的方法,在mobx中提供了observable方法,通过observable的使用,在项目中数据一旦发生变化就会被观察到,从而我们就可以利用此来进行视图的更新等;
如何让数据用上observable呢?四种方式
1、使用@observable
对数据进行装饰 (mobx4、mobx5的写法)
import {observable, toJS} from 'mobx';
class Demo {
@observable myData1 = {a:1, b: 'test', c: [1,2,3]};
@observable myData2 = [1,2,3];
@computed get myData2Length () {
return this.myData2.length;
}
}
const demo = new Demo();
console.log(demo.myData2Length); // 3
console.log(demo.myData2.length); // 错误写法
console.log(toJS(demo.myData2).length); // 正确写法 3
上面demo.myData2.length
会报错,因为我们对一个数组或对象通过@obervable
进行装饰,其已经不再是一个Object或Array对象,而是一个obervable对象,也就不会有Array特有的一些属性或方法了,如果你想把它当作Array来用就要通过mobx提供的toJS()
方法递归地将一个(observable)对象转换为 javascript 结果;
所以请注意像数组,Maps 和 Sets 这样的集合都将被自动转化为可观察对象,在使用其时要注意把它转成你想要的对象,不然容易踩坑!!!
2、使用observable()
方法 (基本很少会这么写)
import {observable} from 'mobx';
let person = observable({name: 'emma', weight: 50});
3、使用decorate()
方法
decorate也是mobx提供来对类和对象进行装饰的工具,有了它我们就可以集中一个地方对类中的属性进行声明
import {observable, toJS, decorate} from 'mobx';
class Demo {
myData1 = {a:1, b: 'test', c: [1,2,3]};
myData2 = [1,2,3];
get myData2Length () {
return toJS(this.myData2);
}
}
decorate(Demo, {
myData1: @observable,
myData2: @observable,
myData2Length: @computed
})
4、使用makeObservable 或 makeAutoObservable (mobx6写法)
makeObservable(target, annotations?, options?)
这个函数可以捕获已经存在的对象属性并且使得它们可观察;任何 JavaScript 对象(包括类的实例)都可以作为 target 被传递给这个函数。 一般情况下,makeObservable 是在类的构造函数中调用的,并且它的第一个参数是 this 。 annotations 参数将会为每一个成员映射 注解。
import { makeObservable, observable, computed, action, flow } from "mobx"
class Doubler {
value
constructor(value) {
makeObservable(this, {
value: observable,
double: computed,
increment: action,
fetch: flow
})
// makeAutoObservable(this); 也可以用这个方法他会自动识别给我们进行声明
this.value = value
}
get double() {
return this.value * 2
}
increment() {
this.value++
}
*fetch() {
const response = yield fetch("/api/value")
this.value = response.json()
}
}
observable对象有哪些属性和方法呢?
上面说到对象和数组被使用@observable
装饰后就变成了obervable对象,作为obervable对象其实也通过了一些和object或array差不多的属性和方法,下面会列一些常用的
obervable数组常用方法
- intercept(interceptor) - 可以用来在任何变化作用于数组前将其拦截。参见 observe & intercept
- observe(listener, fireImmediately? = false) - 监听数组的变化。回调函数将接收表示数组拼接或数组更改的参数,它符合 ES7 提议。它返回一个清理函数以用来停止监听器。
- clear() - 从数组中删除所有项。
- replace(newItems) - 用新项替换数组中所有已存在的项。
- find(predicate: (item, index, array) => boolean, thisArg?) - 基本上等同于 ES7 的 Array.find 提议。
- findIndex(predicate: (item, index, array) => boolean, thisArg?) - 基本上等同于 ES7 的 Array.findIndex 提议。
- remove(value) - 通过值从数组中移除一个单个的项。如果项被找到并移除的话,返回 true 。
- [MobX 4 及以下版本] peek() - 和 slice() 类似, 返回一个有所有值的数组并且数组可以放心的传递给其它库。
obervable与ES6 Map 规范
其实observable 映射所暴露的方法是依据 ES6 Map 规范,也就是说ES6中map的方法,在obervsble对象上同样也有:
- has(key) - 返回映射是否有提供键对应的项。注意键的存在本身就是可观察的。
- set(key, value) - 把给定键的值设置为 value 。提供的键如果在映射中不存在的话,那么它会被添加到映射之中。
- delete(key) - 把给定键和它的值从映射中删除。
- get(key) - 返回给定键的值(或 undefined)。
- keys() - 返回映射中存在的所有键的迭代器。插入顺序会被保留。
- values() - 返回映射中存在的所有值的迭代器。插入顺序会被保留。
- entries() - 返回一个(保留插入顺序)的数组的迭代器,映射中的每个键值对都会对应数组中的一项 [key, value]。
- forEach(callback:(value, key, map) => void, thisArg?) - 为映射中每个键值对调用给定的回调函数。
- clear() - 移除映射中的所有项。
- size - 返回映射中项的数量。
当然除了ES6 Map的一些方法以外,还有mobx提供的一些方法:
- toJS() - 将 observable 映射转换成普通映射。
- toJSON(). 返回此映射的浅式普通对象表示。(想要深拷贝,请使用 mobx.toJS(map))。
- intercept(interceptor) - 可以用来在任何变化作用于映射前将其拦截。参见 observe & intercept。
- observe(listener, fireImmediately?) - 注册侦听器,在映射中的每个更改时触发,类似于为 Object.observe 发出的事件。想了解更多详情,请参见 observe & intercept。
- merge(values) - 把提供对象的所有项拷贝到映射中。values 可以是普通对象、entries 数组或者 ES6 字符串键的映射。
- replace(values) - 用提供值替换映射全部内容。是 .clear().merge(values) 的简写形式。
对observable装饰的数据做出响应
computed 、 autorun 和 reaction
computed 和 autorun它们都是响应式调用的表达式,但是,如果你想响应式的产生一个可以被其它 observer 使用的值,请使用 @computed,如果你不想产生一个新值,而想要达到一个效果,请使用 autorun。 举例来说,效果是像打印日志、发起网络请求等这样命令式的副作用。
对于 computed 如果前一个计算中使用的数据没有更改,计算属性将不会重新运行,当然如果你声明了一个计算属性,但是没有去使用,同样也不会重新运行,也就是使用到了某计算属性且发生数据前后变更才会重新运行,这种方式对性能方面来说实在不要太爽;
而 autorun 则是每次它的依赖关系改变时会再次被触发,传递给 autorun 的函数在调用后将接收一个参数,即当前 reaction(autorun),可用于在执行期间清理 autorun。
reaction 是 autorun的变种,用法:reaction(() => data, (data, reaction) => { sideEffect }, options?)
,对于如何追踪 observable 赋予了更细粒度的控制。 它接收两个函数参数,第一个(数据 函数)是用来追踪并返回数据作为第二个函数(效果 函数)的输入。 不同于 autorun 的是当创建时效果函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。 在执行 效果 函数时访问的任何 observable 都不会被追踪
computed的使用
方式1:@computed
class Foo {
@observable length = 2;
@computed get squared() {
return this.length * this.length;
}
set squared(value) { // 这是一个自动的动作,不需要注解
this.length = Math.sqrt(value);
}
}
方式2:通过decorate方法
import {decorate, observable, computed} from "mobx";
class OrderLine {
price = 0;
amount = 1;
constructor(price) {
this.price = price;
}
get total() {
return this.price * this.amount;
}
}
decorate(OrderLine, {
price: observable,
amount: observable,
total: computed
})
方式3:使用makeObservable 或 makeAutoObservable
autorun的使用
autorun方法给给其传两个参数,第一个参数是一个函数,函数中相关原来发生变更时就会导致这个函数执行
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` 不会再重新计算。
第二个参数是一个对象,可传可不传,有如下可选的参数:
- delay: 可用于对效果函数进行去抖动的数字(以毫秒为单位)。如果是 0(默认值) 的话,那么不会进行去抖。
- name: 字符串,用于在例如像 spy 这样事件中用作此 reaction 的名称。
- onError: 用来处理 reaction 的错误,而不是传播它们。
- scheduler: 设置自定义调度器以决定如何调度 autorun 函数的重新运行
when
when(predicate: () => boolean, effect?: () => void, options?)
when 观察并运行给定的 predicate,直到返回true。 一旦返回 true,给定的 effect 就会被执行,然后 autorunner(自动运行程序) 会被清理。 该函数返回一个清理器以提前取消自动运行程序。
class MyResource {
constructor() {
when(
// 一旦...
() => !this.isVisible,
// ... 然后
() => this.dispose()
);
}
@computed get isVisible() {
// 标识此项是否可见
}
dispose() {
// 清理
}
}
when-promise
如果没提供 effect 函数,when 会返回一个 Promise 。它与 async / await 可以完美结合。
async function() {
await when(() => that.isVisible)
// 等等..
}
改变obervable装饰的数据
action 与 action.bound
当我们需要改变我们obervable装饰的数据时,我们就需要在修改数据的对应函数前面加上@action
或 @action.bound
,action 装饰器/函数遵循 javascript 中标准的绑定规则。 但是,action.bound 可以用来自动地将动作绑定到目标对象。 注意,与 action 不同的是,(@)action.bound 不需要一个name参数,名称将始终基于动作绑定的属性。
class Ticker {
@observable tick = 0
@action.bound
increment() {
this.tick++ // 'this' 永远都是正确的
}
}
const ticker = new Ticker()
setInterval(ticker.increment, 1000)
注意: action.bound 不要和箭头函数一起使用;箭头函数已经是绑定过的并且不能重新绑定。
runInAction
runInAction 是个简单的工具函数,它接收代码块并在(异步的)动作中执行。这对于即时创建和执行动作非常有用,例如在异步过程中。runInAction(f) 是 action(f)() 的语法糖。
mobx.configure({ enforceActions: true })
class Store {
@observable githubProjects = []
@observable state = "pending" // "pending" / "done" / "error"
@action
fetchProjects() {
this.githubProjects = []
this.state = "pending"
fetchGithubProjectsSomehow().then(
projects => {
const filteredProjects = somePreprocessing(projects)
// 将‘“最终的”修改放入一个异步动作中
runInAction(() => {
this.githubProjects = filteredProjects
this.state = "done"
})
},
error => {
// 过程的另一个结局:...
runInAction(() => {
this.state = "error"
})
}
)
}
}
集成React
上面的小节中,我们知道了怎么去创建一个MobX的store:如何去定义state、使用action更新state、使用computed等对state的变化进行响应。MobX 可以独立于 React 运行, 但是他们通常是结合在一起使用,那么如何将MobX集成到React中去呢?
这里就需要用到mobx-react
或mobx-react-lite(更加轻量)
包,提供了用于包裹React Component的 observer
HOC方法
observer
observer HOC 将自动订阅 React components 中任何 在渲染期间 被使用的 可被观察的对象 。 因此, 当任何可被观察的对象 变化 发生时候 组件会自动进行重新渲染(re-render)。 它还会确保组件在 没有变化 发生的时候不会进行重新渲染(re-render)。 但是, 更改组件的可观察对象的不可读属性, 也不会触发重新渲染(re-render)。
在实际项目中,这一特性使得MobX应用程序能够很好的进行开箱即用的优化,并且通常不需要任何额外的代码来防止过度渲染。
mobx-react中Provider
和inject
通过context将store注入并使得任何层级的子组件可以访问到store。
MoX 和 React 结合具体实例
1、先创建两个MobX Store
// HomeStore.js
import { observable, computed, action } from 'mobx';
class HomeStore {
@observable homeName = "mhkHome";
@observable homeMaster = "mhk";
@computed get sayHello () {
return "hello, I'am " + this.homeMaster
}
@action.bound setNewMaster (newMaster) {
this.homeName = newMaster + 'Home';
this.homeMaster = newMaster;
}
}
export default new HomeStore();
// MasterInfoStore.js
import { observable, computed, action, runInAction } from 'mobx';
import { getNewHobby } from './request'
class MasterInfoStore {
@observable name = "mhk";
@observable age = 18;
@observable hobby = ['sleep', 'play game'];
@computed get sayHello () {
return "hello, I like " + this.hobby.toJS().join(',');
}
@action.bound async setNewHobby () {
const newHobby = await getNewHobby();
runInAction(() => {
this.hobby = newHobby;
})
}
}
export default new MasterInfoStore();
2、将两个store包起来
// store.js
import HomeStore from "./HomeStore";
import MasterInfoStore from "./MasterInfoStore";
export default {
HomeStore,
MasterInfoStore,
};
3、在根组件通过Provider组件注入全局
// App.jsx
import React from "react";
import { render } from "react-dom";
import Home from "./Home";
import MasterInfo from "./MasterInfo";
import { Provider } from "mobx-react";
import stores from "../stores/index";
const App = (props) => {
React.useEffect(() => {
console.log(stores.HomeStore.sayHello);
});
return (
<div>
<Home />
<MasterInfo />
</div>
);
};
render(
<Provider {...stores}>
<App />
</Provider>,
document.getElementById("app")
);
4、子组件通过inject、observer用上MobX对应的store
// 类组件 Home.jsx
import React from 'react';
import {inject, observer} from 'mobx-react';
@inject('HomeStore')
@observer
export default class Home extends React.Component {
componentWillMount () {
this.props.setNewMaster('mark')
}
render() {
const { homeName, homeMaster } = this.props;
return (
<div>
<p>这个房子叫:{homeName}</p>
<p>房屋主人是:{homeMaster}</p>
</div>
)
}
}
// 函数组件 MasterInfo.jsx
import {useEffect} from 'react';
import {toJS, observer, inject} from 'mobx-react';
const MasterInfo = (props) => {
const { name, age, hobby, sayHello, setNewHobby } = props;
useEffect(() => {
console.log('i like ', toJS(hobby).join(' and '));
}, [hobby]);
const async fetchHobby = () => {
await setNewHobby();
console.log('更新成功')
}
return (
<div>
<p>我叫{name},今年{age}岁,喜欢{toJS(hobby).join(',')}</p>
<button onClick={fetchHobby}>更新爱好</button>
</div>
)
}
export default inject('MasterInfoStore')(observer(MasterInfo));
参考文档