使用Mobx管理 React 应用状态
1、MobX 介绍
MobX 是一个简单、可伸缩的响应式状态管理库。通过 MobX 你可以用最直观的方式修改状态,其他的一切 MobX 都会为你处理好(如自动更新UI),并且具有非常高的性能。当状态改变时,所有应用到状态的地方都会自动更新。
1.1 React和Mobx关系
React 通过提供机制把应用状态转换为可渲染组件树并对其进行渲染。而MobX提供机制来存储和更新应用状态供 React 使用。
1.2 核心概念
- State:驱动应用的数据
- Computed values:计算值。如果你想创建一个基于当前状态的值时,请使用 computed
- Reactions:反应,当状态改变时自动发生
- Actions:动作,用于改变 State
- 依赖收集(autoRun):MobX 中的数据以来基于观察者模式,通过 autoRun 方法添加观察者
1.3 工作流程
state 的更新过程是同步执行的,也就是说,action 更改 state 后,新的 state 可以立即被获取。而 computed value 采用的是延迟更新,只有当它被使用的时候才会重新计算值,当使用 computed value 的组件被卸载时,它也会被自动回收。
⚠️注意:
MobX 中提到的 state 指的是可以观测的 state,因为对于不可观测的 state ,他们的修改并不会自动产生影响,对 MobX 的数据流来说是没有意义的。
2.MobX 核心 API 介绍
2.1. 设置可观察数据 observable
observable 是一种让数据的变化可以被观察的方法,底层是通过把该属性转化成 getter / setter 来实现的。
observable 值可以是 JS原始数据类型、引用类型、普通对象、类实例、数组和映射。
observable使用
对于JS原始类型(Number/String/Boolean), 使用observable.box()方法设置:
// 设置原始值 const num = observable.box(1) const string = observable.box('YY') const bool = observable.box(true) const symble = observable.box(Symbol(1)) const notnull = observable.box(null) const undefin = observable.box(undefined) // 获取原始值 get() console.log(num.get(),string.get(),bool.get(),symble.get(),notnull.get(),undefin.get())
对于数组、对象类型,使用 observable() 方法设置:
const list = observable([1, 2, 3]); list[2] = 3; list.push(5) // 可以调用数组方法 console.log(list[0], list[1], list[2], list[3]) // 1 2 3 5 const obj = observable({a: '11', b: '22'}) obj.a = "33"; console.log(obj.a, obj.b)
@observable 使用
装饰器可以在 ES7 或者 TypeScript 类属性中属性使用,将其转换成可观察的。 @observable 可以在实例字段和属性 getter 上使用。 对于对象的哪部分需要成为可观察的,@observable 提供了细粒度的控制。
MobX 也提供使用装饰器 @observable 来将其转换成可观察的,可以使用在实例的字段和属性上。
class store { @observable arr = []; @observable obj = {}; @observable map = new Map(); @observable str = ''; @observable num = 1; @observable bool = false; @observable symble = Symble(); @observable set = new Set(); } let leo = new store() console.log(leo.num)
其实就是装饰器 @observable 更进一步封装了 observable.box()。
2.2 响应可观察数据的变化
2.2.1 (@)computed
计算值(computed values)是可以根据现有的状态或其它计算值进行组合计算的值。可以使实际可修改的状态尽可能的小。
使用方式 1
class Goods { @observable price = 1; @observable amount = 2; constructor(price = 1) { this.price = price; } @computed get total() { return this.price * this.amount; } } let m = new Goods() console.log(m.total) // 2 m.price = 10; console.log(m.total) // 20
使用方式2:使用 decorate
class Goods { price = 0; amount = 2; constructor(price = 1) { this.price = price; } get total() { return this.price * this.amount; } } decorate(Goods, { price: observable, amount: observable, total: computed })
使用方式3:使用 observable.object 创建
observable.object 和 extendObservable 都会自动将 getter 属性推导成计算属性
import {observable} from "mobx"; const Goods = observable.object({ price: 0, amount: 1, get total() { return this.price * this.amount } })
2.2.2 autorun
autorun 直译就是自动运行的意思,第一次autorun的函数会直接执行,以后只要autorun里面依赖的observable数据改变,autorun都会再次执行
import { observable, autorun } from 'mobx' class Store { @observable str = 'YY'; @observable num = 123; } let store = new Store() autorun(() => { console.log(`${store.str}--${store.num}`) }) // YY--123 store.str = 'D' // D--123
autorun 默认会执行一次,以获取哪些可观察数据被引用。
autorun 的作用是在可观察数据被修改之后,自动去执行依赖可观察数据的行为,这个行为一直就是传入 autorun 的函数。
2.2.3 when
接收两个函数参数,第一个函数必须根据可观察数据来返回一个布尔值,当该布尔值为 true 时,才会去执行第二个函数,并且只会执行一次。
import { observable, when } from 'mobx' class Store { @observable str = 'YY'; @observable num = 123; @observable bool = false; } let sto = new Store() when(() => sto.bool, () => { console.log('这是true') }) sto.bool = true // 这是true
当 sto.bool 设置成 true 以后,when 的第二个方法便执行了。
2.2.4 reaction
接收两个函数参数,第一个函数引用可观察数据,并返回一个可观察数据,作为第二个函数的参数。
class Store { @observable str = 'YY'; @observable num = 123; @observable bool = false; } let sto = new Store() reaction(() => [sto.str, sto.num], arr => { console.log(arr) }) sto.str = 'DT' sto.num = 122 // [DT, 123] // [DT, 122]
2.2.5 小结
- computed 可以将多个可观察数据组合成一个可观察数据;
- autorun 可以自动追踪所引用的可观察数据,并在数据发生变化时自动触发;
- when 可以设置自动触发变化的时机,是 autorun 的一个变种情况;
- reaction 可以通过分离可观察数据声明,以副作用的方式对 autorun 做出改进;
3. 修改可观察数据
action 是修改任何状态的行为,使用 action 的好处是能将多次修改可观察状态合并成一次,从而减少触发 autorun 或者 reaction 的次数。
import { observable, computed, reaction, action} from 'mobx' class Store { @observable string = 'DD'; @observable number = 123; @action bar(){ this.string = 'YY' this.number = 100 } } let store = new Store() reaction(() => [store.string, store.number], arr => { console.log(arr) }) store.bar() // ["YY", 100]
4.Mobx-React实现ToDoList案例
4.1 初始化 React 项目
用create-react-app脚手架创建一个react项目
然后安装依赖 ,配置一下.babelrc或babel.config.js
yarn add mobx mobx-react
4.2 项目的目录层次结构
入口index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root'));
App.js
Provider组件,在react中,mobx-react提供了 Provider 组件用来包裹最外层组件节点,并且传入 store(通过)context 传递给后代组件
import React from 'react' import { Provider } from 'mobx-react' import store from './store' import Home from './pages/home' function App() { return ( <div> <Provider store={store}> <Home/> </Provider> </div> ); } export default App;
创建Store 类用于存储数据。
store/index.js实现代码如下
import { observable, action, computed } from 'mobx' import moment from 'moment' class AppStore { @observable time = '2020' @observable num = 1 @observable todos = [] @computed get desc() { return `${this.time} 还有 ${this.todos.length} 条任务待完成` } @action addTodo(todo) { this.todos.push(todo) } @action deleteTodo() { this.todos.pop() } @action resetTodo() { this.todos = [] } @action getNow() { this.time = moment().format('YYYY-MM-DD HH:mm:ss') } @action async result () { let num = await this.fetch(); } fetch(){ return new Promise(resolve => { setTimeout(() => { resolve(199) },1000) }) } } const store = new AppStore() setInterval(()=>{ store.getNow() }, 1000) export default store
创建<Home> 组件
/pages/home实现代码如下
@inject 是为了向当前被装饰的组件 注入 store 这个props。当然 store 这个 prop 其实是由 Provider 提供的
import React from 'react' import { inject, observer } from 'mobx-react'; @inject ('store') @observer export default class Home extends React.Component { constructor(props) { super(props) this.state = {} } addTodoData = () => { let { store } = this.props; store.addTodo('一条新任务') } deleteTodoData =() => { let { store } = this.props; store.deleteTodo() } resetTodoData = () => { let { store } = this.props; store.resetTodo() } render() { let { store } = this.props return( <div className='home'> <h1>在React中使用mobx</h1> <div>{store.desc}</div> <button onClick={this.addTodoData}>添加任务</button> <button onClick={this.deleteTodoData}>删除任务</button> <button onClick={this.resetTodoData}>任务重置</button> { store.todos.map((item,index,arr) => { return( <div key={index}>{item}</div> ) }) } </div> ) } }
效果图
5.小结
MobX 的设计哲学是“可从应用状态中派生的任何内容都应当自动的被派生”,后半句有两个关键字:自动和派生。心中秉持这一设计哲学,再来看 MobX 的派生状态模型就比较清晰了,Computed values 和 Reactions 都可以视作是从 State 中派生出的,State 变化时触发 Computed values 的重新计算和 Reations 的重新运行。为了让派生能自动的进行,MobX 通过Object.definePropery
或Proxy
方式拦截对象的读写操作,从而允许用户以自然的方式来修改状态,MobX 负责更新派生的内容。
MobX 提供了几个核心 API 来帮助定义状态(observable
)、响应状态(autorun
, computed
)和修改状态(action
),通过这些 API 可以让程序立即具备响应式能力。MobX 可以单独使用,也可以与任何流行的 UI 框架一起使用,不过 MobX 最常见的是与 React 一起使用,mobx-react 是流行的 MobX 和 React 的绑定实现库。
参考