MobX分享
文档地址: https://cn.mobx.js.org/
MobX是一种简单的、可扩展的状态管理,它通过透明的函数响应式编程使得状态管理变得简单和可扩展。
React通过提供机制把应用状态转换成为可渲染组件树并对其进行渲染,而MobX提供机制来存储和更新应用状态供React使用。
mobx的工作流程:
MobX要点
- 定义状态并使其可观察
- 创建视图以相应状态的变化
- 更改状态
在MobX中,数据是通过加@observable作为可检测的被观察者,通过添加@observer将view作为观察者,对数据进行监测,如果要改变数据则使用@action(可以直接在view层改,不建议),@computed可以用来计算数据
严格模式
mobx在严格模式下,不允许在 action 外更改任何状态。但是不同版本严格模式的用法不同,3.x、4.x、5.x三个版本下的严格模式用法。
1、mobx@3.x:useStrict(boolean)
2、mobx@4.x:configure({ enforceActions: boolean })
3、mobx@5.x:configure({ enforceActions: value })
可接收的值为:
"never" (默认): 可以在任意地方修改状态
"observed": 在某处观察到的所有状态都需要通过动作进行更改。在正式应用中推荐此严格模式。
"always": 状态始终需要通过动作来更新(实际上还包括创建)。
文档地址:https://cn.mobx.js.org/refguide/api.html#enforceactions
概念与原则
state(状态)
状态是驱动应用的数据
Derivations(衍生)
- 任何源自状态并且不会摘由任何进一步的相互作用的东西就是衍生
a. 用户界面
b. 衍生数据,如剩下的待办事项的数量
c. 后端集成,如把变化发送到服务器
- 类型
a. Computed values(计算值)
永远可以使用纯函数从当前可观察状态中衍生出的值
b. Reactions(反应)
当前状态改变时需要自动发生的副作用,需要有一个桥梁来连接命令式编程和响应式编程
Actions(动作)
动作是任一段可以改变状态的代码
核心API
observable相关
observable的值可以是JS的基本类型、引用类型、普通对象、类实例、数组和映射
用法:
observable(value) @observable example = value;
针对不同的value类型,有以下的匹配规则:
数组
如果value是数组,会返回一个Observable Array。
let list = observable([1, 2, 3, 4, 5]); // 还有其他两种写法 @observable list = ([1, 2, 3, 4, 5]); let list = observable.array([1, 2, 3, 4, 5]);
以上三种写法其实功能一样,可以自己选择想要用的方法
observable会将数组转变为可观察的,也是递归的,所以数组中的所有值(包括之后添加的)都是可观察的,具体演示见装饰器。
变成可观察的数组还可以使用以下方法:
//在任何变化作用于数组时拦截 intercept(target, propertyName?, interceptor) //删除所有项 clear() //查找 find(), findIndex // 通过值从数组中移除一个单个的项。如果项被找到并移除的话,返回 true remove(value)
observable.array
会创建一个人造数组(类数组对象)来代替真正的数组。 实际上,这些数组能像原生数组一样很好的工作,并且支持所有的原生方法,包括从索引的分配到包含数组长度。
所以Array.isArray(observable([]))
都会返回false,所以无论何时当你需要传递 observable 数组到外部库时,通过使用 array.slice()
在 observable 数组传递给外部库或者内置方法前创建一份浅拷贝,也就是说Array.isArray(observable([]).slice())
会返回true
Map
如果value是ES6的Map,会返回一个新的Observable Map
// 三种定义方式 可自选 let map = observable(new Map()); @observable map = new Map(); let map = observable.map(new Map()); // observable.map(value) 中的value可以是对象、数组或者字符串键的ES6 map
ES6 的Map对应的方法都可以直接用,比如 has(key), set(key, value)等等,Mobx也提供了一些方法,可以看下文档。
object
如果把一个普通的 JavaScript 对象传递给 observable
方法,对象的所有属性都将被拷贝至一个克隆对象并将克隆对象转变成可观察的。 (普通对象是指不是使用构造函数创建出来的对象,而是以 Object
作为其原型,或者根本没有原型。)
class ObjectTest { @observable obj = { name: 'll' } person = observable.object({ name: 'xiaoming', age: 10 }); cat = observable({ voice: '喵喵' }); }
只有普通的对象可以转变成 observable 。对于非普通对象,构造函数负责初始化 observable 属性。 要么使用 `@observable` 注解,要么使用 `extendObservable` 函数。
属性的 getter 会自动转变成衍生属性,就像 `@computed` 所做的。
const objectT = new ObjectTest(); extendObservable(objectT, { firstName: 'li', lastName: 'xiaoming', get fullName() { return this.firstName + " " + this.lastName }, });
原始类型
如果value是原始类型值,比如Number、String、Boolean
除了使用observable(value)之外,还可以使用observable.box(value)
// 返回当前值。 get() // 替换当前存储的值并通知所有观察者。 set(value) // 注册一个观察者函数,每次存储值被替换时触发。返回一个函数以取消观察者。 // change 参数是一个对象,其中包含 observable 的 newValue 和 oldValue .observe(callback: (change) => void)
装饰器
observable.deep、observable.ref、observable.shallow区别
官方解释:
observable: observable.deep 的别名
observable.deep: 任何 observable 都使用的默认的调节器。它将任何(尚未成为 observable )数组,映射或纯对象克隆并转换为 observable 对象,并将其赋值给给定属性
observable.ref: 禁用自动的 observable 转换,只是创建一个 observable 引用
observable.shallow: 只能与集合组合使用。 将任何分配的集合转换为 observable,但该集合的值将按原样处理
// @observable.shallow value = { @observable.deep value = { name: 'liming', age: 12, details: { tel: 111, addr: '路' } } @action changeTel = () => { this.value.details.tel = 222; } <div> <p>{this.props.data.value.details.tel}</p> <button onClick={this.props.data.changeTel}></button> </div>
如果是observable.shallow,那么name、age都会自动被转成observable对象,如果在界面中使用一旦它们的值被修改,界面上马上变化。而此时details.tel和details.addr并不是observable对象,如果修改它们的值,界面中是不会有任何变化的,依旧是原来的值。如果我们把value比作第一层,details比作第二层,第一层所有变量会被转化成observable对象,而第二层的变量不会被转成observable对象
而observable.deep,和observable.shallow的区别在于,observable.deep不仅会把第一层所有变量会被转化成observable对象,也会把二层的变量转成observable对象,所以修改details.tel,页面会跟着改变,类似所说的深拷贝
shallow和deep可以按照我们讲的深浅拷贝来理解。
// observable.ref @observable.ref test = { name: 'liming', age: 12 } @action changeName() => { this.test.name = 'xiaoming'; }
上面这种写法中的test不是observable对象,在界面中使用后,再修改值,界面也不会变化。
改变observables action
用法:
@observable num = 0; @action addNum = => { this.num++ }
action有一个自己的装饰器 @action.bound,可以用来自动地将动作绑定到目标对象
@observable test = "test"; @observable boundStr = "bound"; @action changeTest = () => { this.test = "action"; } @action.bound changeBound() { this.boundStr = "action-bound"; }
注意:action.bound 不要和箭头函数一起使用
当开启严格模式时,在action外修改state会报错
async actions & flows
在严格模式中,action方法中不支持异步修改state
异步修改state方式:
1. 异步方法和修改state方法分为两个方法
@action changeTest = () => { setTimeout(() => { this.changeT(); }, 100); } @action changeT = () => { this.test = "异步"; }
2. 直接调用action方法
@action changeTest = () => { setTimeout(() => { action('changeT', () => { this.test = "异步"; })(); // 注意需要调用 }, 100); }
3. 使用runInAction
@action changeTest = () => { setTimeout(() => { runInAction(() => { this.test = "异步"; }); }, 100); }
async await
@action.bound async asyncChange() { const data = await this.timeout(); runInAction(() => { this.test = data; }); }
flows
flow包住一个generator函数,里面修改state时不需要再用action来包装
@action.bound asyncChange = flow(function * () { const data = yield this.timeout(); this.test = data; });
直接操控Object
values(thing)
将集合中的所有值作为数组返回keys(thing)
将集合中的所有键作为数组返回entries(thing)
返回集合中的所有项的键值对数组set(thing, key, value)
或set(thing, { key: value })
使用提供的键值对来更新给定的集合remove(thing, key)
从集合中移除指定的项。用于数组拼接has(thing, key)
如果集合中存在指定的 observable 属性就返回 trueget(thing, key)
返回指定键下的子项
<div> <h4>object改变</h4> <p>{values(actions.obj).join(", ")}</p> <button onClick={this.addProps}>添加obj属性</button> </div> addProps = () => { const { actions } = this.props; set(actions.obj, { age: 18 }); }
响应observables computed
计算值,可以根据现有的状态或者其他计算值衍生出来的值,类似excel表格的公式。
computed和reactions的区别:
computed应用这种情况:你想要响应式产生一个新的呗其他observer使用的值,使用computed
reactions并不想产生一个新值,而是在某些值发生变化时作出一些反应,产生一些副作用
computed会缓存计算值,当它使用的state没有发生变化时,会直接使用缓存值,而不会重新计算。
@observable firstName = 'li'; @observable lastName = 'ming'; @computed get fullName() { return this.firstName + this.lasName; } <div> <p>fullName: {computeds.fullName}</p> <p>fullName: {computeds.fullName}</p> <button onClick={computeds.changeFirstName}>修改firstName</button> </div>
autorun
autorun(() => void, options?)
自动运行 -- 修改autorun引用的可观察数据,导致autorun 自动运行(首次加载会自动运行,自动追踪可观察数据)
when
when(predicate: () => boolean, effect?: () => void, options?)
第一个函数根据引入的可观察数据返回一个布尔值,当布尔值为 true 的时候,则执行第二个函数(when 提供了条件执行逻辑)
reaction
reaction(() => data, (data, reaction) => { sideEffect }, options?)
相比autorun在函数体内部写入需要监测的数据, reaction会更直观的看到需要检测的数据,当代码量比较大或者迭代时,reaction返回的data会让我们很直观的看到被检测的数据有哪些。
autorun:所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发,整体返回一个disposer作清理函数
reaction:创建时效果函数不会直接运行,若第三个参数加了 { fireImmediately: true },会在数据函数第一次运行后立即触发,整体返回一个disposer作清理函数
when: 只执行一次,第二个参数返回清理函数
区别:
第一个函数根据可观察函数,数据变化后,返回一个新的值,该值作为第二个函数的参数
autorun(() => { console.log('autorun', productList.length); }); reaction(() => productList.length, data => { console.log('reaction'); if(data > 5) { console.log('reaction', '数量超过5'); } }); when(() => productList.length > 5, () => { console.log('when', '数量超过5'); });
对何作出反应
MobX 会对在追踪函数执行过程中读取现存的可观察属性做出反应。
- “读取” 是对象属性的间接引用,可以用过
.
(例如user.name
) 或者[]
(例如user['name']
) 的形式完成。 - “追踪函数” 是
computed
表达式、observer 组件的render()
方法和when
、reaction
和autorun
的第一个入参函数。 - “过程(during)” 意味着只追踪那些在函数执行时被读取的 observable 。这些值是否由追踪函数直接或间接使用并不重要。
MobX追踪属性访问,而不是值
let message = observable({ title: "Foo", author: { name: "Michel" }, likes: [ "John", "Sara" ] })
现在 MobX 基本上所做的是记录你在函数中使用的是哪个箭头。之后,只要这些箭头中的其中一个改变了(它们开始引用别的东西了),它就会重新运行。
见演示。
修改根数据将不会做出反应,解决方法可以使用ref或者box包住。
实用工具 toJS
递归地将一个(observable)对象转换为 javascript 结构。 支持 observable 数组、对象、映射和原始类型。
var obj = mobx.observable({ x: 1 }); var clone = mobx.toJS(obj); console.log(mobx.isObservableObject(obj)); // true console.log(mobx.isObservableObject(clone)); // falseIntercept & observe
observe
和 intercept
可以用来监测单个 observable(它们不追踪嵌套的 observable) 的变化。 intercept
可以在变化作用于 observable 之前监测和修改变化。 observe
允许你在 observable 变化之后拦截改变。
Intercept
用法:intercept(target, propertyName?, interceptor)
target
:检测的observable
propertyName
: 可选参数,用来指定某个属性进行拦截。
interceptor
:回调函数,接受一个对象
intercept相当于告诉MobX对于检测的数据,变化前应该做些什么,可以分为以下几种情况
- 把从函数中接收到的change对象原样返回
- 修改change对象并将其返回
- 返回null,表示此次变化会被忽略,不会被应用
- 抛出异常
此函数会返回一个disposer函数,当调用时可以取消拦截器。
可以为同一个 observable 注册多个拦截器。 它们会按照注册的顺序串联起来。 如果一个拦截器返回 null
或抛出异常,其它的拦截器不会再执行。
可以注册一个拦截器同时作用于父对象和某个属性。 在这种情况下,父对象的拦截器在属性拦截器之前运行。
const disposer = intercept(person, 'firstName', change => { if(!change.newValue) { return null; } if(change.newValue.length < 5) { change.newValue += '1'; return change; } if(change.newValue.length === 5) { return change; } if(change.newValue.length > 5) { disposer(); return; } throw new Error(change.newValue + '错误'); })
observe
用法: observe(target, propertyName?, listener, invokeImmediately?)
target
: 观察的 observable
propertyName
: 可选参数,用来指定某个属性进行观察。
listener
: 在每次变化作用于 observable 后调用的回调函数。接收一个用来描述变化的对象,除了装箱的 observable,它调用 listener
有两个参数: newValue、oldValue
。
invokeImmediately
: 默认是 false。如果你想 observe
直接使用 observable 的状态(而不是等待第一次变化)调用 listener
的话,把它设置为 true。不是所有类型的 observable 都支持。
该函数返回一个 disposer
函数,当调用时可以取消观察者。
可以使用autorun或者reaction代替
const disposerOb = observe(cat, change => { console.log('observe', change); if(change.newValue === '白色') { changeFirstName({ target: {value: '李'}}); } if(change.newValue === '黑白') { disposerOb(); } });
intercept
和 observe
的回调函数接收一个事件对象,它至少有如下属性:
object
: 触发事件的 observabletype
: 当前事件类型(字符串)
具体类型的返回的附加字段可以看下文档。
Provider (mobx-react
包)
ReactDOM.render( <Provider {...stores}> <Example /> </Provider>, document.getElementById('root') );
inject (mobx-react
包)
相当于Provider
的高阶组件。可以用来从 React 的context
中挑选 store 作为 prop 传递给目标组件。用法:
inject("store1", "store2")(observer(MyComponent))
@inject("store1", "store2") @observer MyComponent
@inject((stores, props, context) => props) @observer MyComponent
@observer(["store1", "store2"]) MyComponent
is a shorthand for the the@inject() @observer
combo.