Mobx
前言
-
在介绍Mobx前,看一下Mobx的中文文档简介,看到这样写着:“MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程使得状态管理变得简单和可扩展。”
-
战火洗礼的库” 怎么看都感觉很奇怪,读起来很拗口??,而且网上很多介绍 MobX 的文章都是这么写的,在 github 翻阅其 README 发现写的是:"MobX is a battle tested library that makes state management simple and scalable by transparently applying functional reactive programming (TFRP)"
可以看到作者原本要表达的意思是 MobX 是经过了许多的测试,拥有比较强的健壮性。下面是通过谷歌翻译的结果,看起来也比中文网的表达要准确一些。
-
简介
-
是什么
- Mobx是一个简单的,可扩展的Javascript状态管理库
- MobX 是由 Mendix、Coinbase、Facebook 开源和众多所赞助的。
- 一般回和React结合使用(大多数是和react,自己也可以试试vue或者angular)
-
浏览器支持
Mobx version activity supported supported browsers 5.X Y NOT : IE 11 and lower,Node5 and lower 4.X Y any es5 complaint browser 1-3.X N any es5 complaint browser -
变动
- 最新的mobx版本应该是6.x了,主要是去掉了之前版本的装饰器风格写法,另外引入了makeAutoObservable 这个api,后续也会提到
-
-
Mobx核心思量
- 任何源自应用状态的东西都应该自动的获得
- mobx的核心原理是通过action触发state的变化,进而触发state的衍生对象(computed value & Reactions);开发者只需要定义Observable的数据和由此衍生的数据(computed value)或者操作(Reactions),剩下的更新自然就交给Mobx去做就可以了
-
observable用法
import {observable} from 'mobx' observable(value) @observable classProperty = valueobservable值可以是JS基本数据类型,引用数据类型,类实例,对象,数组或者映射
-
observable.map
-
observable.map(values) - 创建一个动态键的 observable 映射,如果你不只关注某个特定的entry的更改,而且对添加或删除其他entry时也做出反应的话,那么Observable maps会非常有用
-
observable.map(values) 中的valuse可以是对象,数组或者字符串键的ES6 map
-
通过es6 map构造函数,你可以使用observable(new Map())或者使用装饰器 @observable map = new Map()的类属性来初始化observable映射
-
下列observable映射所暴露的方法是根据ES6 Map规范:
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规范,而是由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.array
-
observable.array(values)可将数组转变为可观察的,也是递归的,数组中的所有值都是可观察的
-
除了所有内置函数(map(),filter(),reduce(),some()等),observable.map还可以使用
intercept(interceptor)- 可以用来在任何变化作用于数组前将其拦截。参见 observe & interceptobserve(listener, fireImmediately? = false)- 监听数组的变化。clear()- 从数组中删除所有项。replace(newItems)- 用新项替换数组中所有已存在的项。find(predicate: (item, index, array) => boolean, thisArg?)- 基本上等同于 ES7 的Array.find提议。remove(value)- 通过值从数组中移除一个单个的项。如果项被找到并移除的话,返回true。
-
-
observable.object
-
observable.object(values)可将一个普通js对象的所有属性拷贝至一个克隆对象并将克隆对象可观察化,(普通对象是值不是用构造函数创建出来的对象,而是以Object作为其原型,或者根本没有原型),默认情况下,observable是递归以用的
-
注意:
- 只有普通的对象可以转变成 observable 。对于非普通对象,构造函数负责初始化 observable 属性。 要么使用
@observable注解,要么使用extendObservable函数。 - 属性的 getter 会自动转变成衍生属性,就像
@computed所做的。 observable是自动递归到整个对象的。在实例化过程中和将来分配给 observable 属性的任何新值的时候。Observable 不会递归到非普通对象中。- 这些默认行为能应对95%的场景,但想要更细粒度的控制,比如哪些属性应该转变成可观察的和如何变成可观察的,请参见装饰器。
- 只有普通的对象可以转变成 observable 。对于非普通对象,构造函数负责初始化 observable 属性。 要么使用
-
-
boxed values
-
observable.box(value)接收任何值并把值存储到箱子中。 使用.get()可以获取当前值,使用.set(newValue)可以更新值。 -
此外,还可以使用它的
.observe方法注册回调,以监听对存储值的更改。 但因为 MobX 自动追踪了箱子的变化,在绝大多数情况下最好还是使用像mobx.autorun这样的 reaction 来替代 -
observable.box(scalar)返回的对象签名是:.get()- 返回当前值。.set(value)- 替换当前存储的值并通知所有观察者。intercept(interceptor)- 可以用来在任何变化应用前将其拦截。参见 observe & intercept。.observe(callback: (change) => void, fireImmediately = false): disposerFunction- 注册一个观察者函数,每次存储值被替换时触发。返回一个函数以取消观察者。参见 observe & intercept。change参数是一个对象,其中包含 observable 的newValue和oldValue。
-
-
observable小结:
import {observable,isArrayLike,extendObservable} from 'mobx' //通过observable将变量转换为可观察的对象 //mobx对任意变量对处理方式有两种: // 1 :数组、对象以及es6中的map //直接把observable作为函数来将变量转换为可观察对对象,之后对数组、以及map中的数据进行合理的操作。 //1.1 数组(array) const arr = observable(['a','b','c']) console.log(arr[0],arr.push(‘d’)) //如果直接用observable函数赶回的结果不是数组,而引入isArrayLike模块后,返回的就相当于数组 //可以用一些数组的方法,但注意不能通过数组越界的方式访问或改变数据数据,mobx不回监视越界的访问 //1.2 对象 const obj = observable({a:1,b:1}) console.log(obj.a,obj.b) //mobx只能对已有的数据进行监视,如果要监视新增的属性,需要使用mobx的extendObservable()方法,因此最好在程序初始化时就说明所有程序会用的属性 //1.3 map const map = obervable(new Map()) map.set('a',1) console.log(map.has('a')) map.delete('a') console.log(map.has('a')) //和数组类型相似,返回的结果不是真正的map。但表现的足够接近map,可以调用一些map的方法 // 2 :其他类型 // 一些原始类型数据,如String、Number、Boolean等,需要调用observable.box来将变量包装为可观察的对象,之后对该变量的直接赋值将会被监视。 const num = observable.box(20) const str = observable.box('hello') const bool = observable.box(true) console.log(num.get(),str.get(),bool.get()) num.set(1) str.set('hi') bool.set(false) //使用get方法得到原始类型的值,使用set方法修改原始类型的值 //3: 使用deacorators修饰器声明可观察的对象 //decorator只能修饰类或类的成员,因为此我们要创建一个类 class Store { @observable num = 20 @observable str = 'hello' @observable bool = true } const store = new Store() //这个时候不需要用.box来包装可观察的对象,访问直接使用 store.num等,内部方法可以使用this.num获取 -
对可观察的数据做出反应
-
computed
- computed可以将其它可观察数据以用户想要的方式组合起来,变成一个新的可观察数据 - 也可以结合observable使用,来修饰类的成员 - ``` import {observable,computed,autorun} from 'mobx' class Store { @observable count = 10; @observable price = 21 } const store = new Store() const desc = comouted(()=>{ return '个数:'+ store.count +'个; 单价:'+store.price +'元/个' }) console.log(desc.get()) // 个数:10个;单价:21元/个 desc.observable((obj)=>{ console.log(obj.newValue) }) store.count = 12 // count发生变化: 个数:12个; 单价:21元/个 store.price = 15 // price发生变化: 个数:12个; 单价:15元/个 ``` -
autorun
- 可观察数据发生改变后,自动执行依赖可观察数据的行为,这个行为一般指的是传入autorun的函数
- 当项目中可观察数据比较多,数据每发生一次就触发一次autorun,会很浪费计算资源
- computed值可以作为一种新的可观察数据对待
-
//接上 autorun(()=>{ console.log(store.count + '/' + store.price) }) stroe.count = 10 // 10/15 store.price = 20 // 10/20
-
when(提供执行逻辑条件,算是一种改进后的autorun),接收两个参数:
- 第一个函数:根据可观察数据返回一个布尔值,当布尔值为true时,执行第二个参数,且只执行一次
- 第二个函数:如果可观察数据返回的布尔值一开始就是true,那么立即同步执行第二个函数
-
import { observable,isArrayLike,computed,autorun,when } from 'mobx'; class Store { @observable array = []; @observable obj = {}; @observable map = new Map(); @observable string = 'Hello'; @observable number = 20; @observable bool = false; @computed get mixed() { return store.string + '/' + store.number; } } var store = new Store(); when(() => store.bool,() => {console.log("it's true");}); store.bool = true // it's true
-
reaction:autorun的变种,对于如何追踪observable赋予了更细粒度的控制,接收两个参数:
- 第一个函数:引用可观察数据,并返回一个值,这个值会作为第二个函数的参数。
- 第二个函数:副作用函数,当调用时会接收两个参数,第一个参数是由data函数返回的值,第二个参数是当前的reaction,可以用来在执行期间清理reaction
-
import {observable,computed,when,reaction} from 'mobx' class Store { @observable list = [ { title:'redux', isFunPro:true },{ title'mobx', isFunPro:false } ]; @observable num = 0 } const store = new Store() // 对title变化做出反应 const reaction1 = reaction( () => store.list.map(item => item.title), titles => console.log('reaction1:',titles.join(',')) ) const autorun1 = autorun(() => { console.log('autorun1:',store.map(item => item.title).join(',')) }) store.list.push({title:'flux',isFunPro:true}) //输出 reaction1:redux,mobx,flux autorun1: redux,mobx,flux const reaction2 = reaction( () => store.count, (count,reaction) => { console.log('store.count:',count) reaction.dispose() } ) store.count =1 // store.count:1 store.count = 2 //无输出,第二个参数作为清理函数使用 console.log(store.count) //2
-
修改可观察数据
-
可以对多次状态数据的赋值合并为一次,从而减少触发autorun或reaction的次数
-
action既可以作为普通函数也可以作为decorator来使用,常用的是decorator的方式
-
import {observable,computed,action,reaction,autorun} from 'mobx' class Store { @observable list = [ { title:'orange', price:11 },{ title:'apple', price:10 } ]; @action addFruit(){ this.list.push({ title:'banana', price:20 }) this.list.push({ title:'cheeries', price:68 }) } } var store = new Store() reaction (()=>store.list.map(item => item.title),titles =>{ console.log(titles.join(',')) }) store.addFruit() // orange,apple,banana,cheeries 只执行了一次reaction -
action还有一种特殊使用方法:action.bound,action.bound 可以用来自动地将动作绑定到目标对象。 注意,与
action不同的是,(@)action.bound不需要一个name参数,名称将始终基于动作绑定的属性。import {observable,computed,reaction,action} from 'mobx' class Store { @observable name = 'Jacky'; @observable age = 61 @computed get mixed(){ return this.name + '/' + store.age } @action.bound bar() { this.name = 'Alan' this.age = 71 } } var store = new Store() reaction(() => [store.name, store.age], arr => { console.log(arr) }) store.bar() // ["Alan", 71] 注意:action.bound不要和箭头函数一起使用,箭头函数已经是绑定过的并且不能重新绑定
-
-
runInAction(name?,thunk)
- runInAction是个简单的工具函数,它接收代码块并在(异步的)动作中执行,这对于及时创建和执行动作非常有用,允许随时定义匿名的action,并运行它
-
import {observable,computed,reaction,runInAction,action} from 'mobx' class Store { @observable array = []; @observable obj = {}; @observable map = new Map(); @observable string = 'hello'; @observable number = 20; @observable bool = false; @computed get mixed(){ return store.string + '/' + store.number } @action.bound bar(){ this.string = 'world'; this.number = 30 } } var store = new Store() reaction( ()=> [store.string,store.number], arr => console.log(arr.join(',')) ) runInAction(()=>{ store.string = 'wordl'; store.number = 30 }) // 和调用store.bar() 效果一样
-
结合react使用
-
安装依赖
npm i mobx mobx-react -S -
入口文件配置
import React from 'react' import {render} from 'react-dom' import {Provider} from 'mobx-react' import store from './store' import App from './app' render( <Provider {...store}> <App /> </Provider>, doucment.querySelector('#root') ) -
store文件
// store/index.js import {observable,action} from 'mobx' class Store { @observable name = 'test' @observable list = [] @action.bound async fetch(){ const res = await ..... //异步请求 this.list = res.data } setName(name){ this.name = name } } const store = new Store() export default store -
App页面
import React, {Compunent} from 'react' import {inject,observer} from 'mobx-react' @inject('store') @observer class App extends Component { componentDidMount(){ this.props.store.fetch() //获取数据 } edit = () => this.props.store.setName('mobx-test') render(){ const {list,name} = this.props.store return ( <div> <div>name:{name}</div> <button onClick={this,edit}>修改name</button> <ul className='list-box'> { list.map((item,index)=> { return ( <li key={index}>{item.id}</li> <li key={index}>{item.name}</li> <li key={index}>{item.list}</li> ) }) } </ul> </div> ) } } export default App
-
-
mobx 6
-
- mobx现在的最新版应该是6,这个版本的api相比于之前有了极大的简化,装饰器声明已经无效,要实现Observable,Action等声明,必须makeObservable或者makeAutoObservable声明
-
//before import {observable,cpmputed,action,makeObservable} form 'mobx' class Store { @observable string ='hello' @observable num = 10 @observable list = [] @computed get unfinishListCount(){ return this.list.filter(item => !item.done).length } @action.bound addItem(item) { this.list.push(item) } } //after class Store { list = [] constructor(){ makeObservable(this, { todos:observable, unfinishListCount:get, addItem:action }) } get unfinishListCount(){ return this.list.filter(item => !item.done).length } addItem(item){ this.list.push(todo) } } - 看起来貌似比之前的写法并没有简化很多,6.0推出了另外一个api makeAutoObservable
-
import {runInAction,makeAutoObservable,autorun} from 'mobx' const store = makeAutoObservable({ list:[], get unfinishListCount(){ return this.list.filter(item => !item.done).length }, addItem(item){ this.list.push(item) }, setList(list){ this.list = list } async getList(){ //异步 const res = await ******* //请求数据 if(res.code===0){ const {list} = res.data runInAction(()=>{ this.list = list }) //或者可以 使用已经存在的方法 this.setList(list) } } }) autorun((store.list)=> { console.log('list长度为:',store.list.length) }) //注:makeAutoObservable中所有的方法都被处理成action get后面的方法都被处理成计算属性
-
总结:
- mobx基本介绍到这里就结束了,只是大致的说明了一下mobx中的api和使用方法,个人感觉中文文档写的比较乱,但是个人英文水平也不太高,读起来官方文档也是比较吃力,更不用说按照源文档进行一些演示了,后续还是会深入研究mobx的实现,也希望大家也有所收获,之前也是准备结婚的事情,耽搁了一些时间,现准备差不多了,等结完婚后,再和大家见面啦。
-
代码地址
- <git@github.com:Alex-hot/mobx-test.git>
-
参考