Mobx基本使用

5,779 阅读10分钟

最近在学习使用React & Mobx,学习过程中把一些经常使用到的点收集起来,汇总一下,方便后续查阅。

Mobx

Mobx中文文档的介绍

MobX 是一个经过战火洗礼的库,它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。MobX背后的哲学很简单:

  • 任何源自应用状态的东西都应该自动地获得
  • 其中包括UI、数据序列化、服务器通讯,等等。

浏览器支持情况

  • Mobx >= 5 版本运行在任何支持 ES6 Proxy的浏览器
  • Mobx 4 可以运行在任何支持ES5的浏览器上,而且也将持续地维护

Mobx 5和Mobx 4的API是相同的,并且语义上也能达到相同的效果,只是Mobx4存在一些局限性

React & Mobx 的关系

React和Mobx是相辅相成、相互合作的

React提供机制,把应用状态转换为可渲染组件树,并对其进行渲染。而Mobx提供机制来存储和更新应用状态,供React使用。

Mobx工作流程

image.png

  • action, 事件调用,Action是唯一可修改state的函数,并且可能会有其它的副作用
  • state,是可观察和最低限度定义的,不应该包含冗余或推导数据。可以是图形,包含类、数组、引用等等。
  • computed values,是可以使用pure function(纯函数)从state中推导出来的值,Mobx会自动更新它,并在它不再使用时将其优化掉。
  • Reactions,很像Computed values,会对state的变化做出反应,但它们不产生一个值,而是会产生一些副作用,如更新UI.

Mobx基本使用

本文使用的是Mobx 4.4版本,在项目中使用React+Mobx,主要介绍如下内容:

  • 开发环境搭建
  • Mobx常用API介绍
  • 与React搭配使用时的常见问题

开发环境搭建

通过create-react-app创建一个typescript应用,具体的细节,这里不再述了,大家可以按照create-react-app的指引来操作.

大体步骤如下:

> npm install -g create-react-app
## 创建一个typescript应用
> create-react-app my-app --template typescript
> cd my-app
> npm install mobx@4.4.0 react-mobx
## 启动本地开发服务器, 默认会启用3000端口,通过http://localhost:3000来访问。
> npm run start     

Mobx常用API介绍

1. Observable 设置可观察数据

Observable是一个让数据的变化可以被观察的方法,底层是通过将该属性转化成getter/setter来实现的。其值可以是JavaScript原始数据类型、引用类型、普通对象、类实例、数组和映射。

observable使用(装饰器模式)

import React from 'react'
import {observable, ObservableMap} from 'mobx'

export default class Demo extends React.Component{
    // 基本数据类型
    @observable num:number = 99
    @observable str:string = 'hello james'
    @observable flag:boolean = true
    // 数组 对象
    @observable list:Array<number> = [1,2,3]
    @observable obj:object = {hello: 'james'}
   
    // 映射(Map)
    map:ObservableMap = observable.map({hi: 'map'})
    
    componentDidMount(){
        // 获取值
        console.log(this.num, this.str, this.flag)
        console.log(this.list, this.obj)
        console.log(this.map.get('hi')
        
        // 修改值
        this.num = 100
        this.str = 'hello world'
        this.flag = false
        
        this.list.push(4)
        this.obj.hello = 'world'
        this.map.set('hi', 'james')
    }
}
注意

1.在使用数组时,应避免下标越界去访问数组中的值,这不会被Mobx所监视,实际开发中,应注意数组长度的判断。 例如:

@observable list:Array<number> = [1,2,3]
// some code...
this.list[9]  // undefined

2.在使用对象时,如果需要动态添加新的属性,需要使用到extendObservable, 否则新增的属性不会被Mobx所监视。

import {observable, extendObservable} from 'mobx'
// some code ...
@observable obj:object = {hello: 'james'}
// some code ...
extendObservable(this.obj, {
    name: 'demo'
})

2. computed 响应可观察数据的变化

computed values,计算值是可以根据现有的状态或其它计算值进行组合计算的值,可以使实际可修改的状态尽可能小。computed values是高度优化过的,应尽可能的多使用。

import React from 'react'
import {observable, computed} from 'mobx'
import { observer } from "mobx-react";

@observer
export default class Price extends React.Component{
    @observable value = 2
    @observable amount = 3
    
    @computed get total(){
        return this.value * this.amount
    }
    render(){
        return <div>Total: {this.total}</div>
    }
}
computed的setter

computed的setter不能用来改变computed values,而是用来修改它里面的成员的值,从而使用得computed values发生变化。

import React from 'react'
import {observable, computed} from 'mobx'
import { observer } from "mobx-react";

@observer
export default class Price extends React.Component{
    @observable value = 2
    @observable amount = 3
    
    @computed get total(){
        return this.value * this.amount
    }
    // setter一定要定义在getter后,一些typescript版本会认为声明了两个名称相同的属性而报错
    set total(val){
        this.value = val
    }
    componentDidMount(){
        console.log(this.total) // 6
        this.total = 5
        console.log(this.total) // 15
    }
    render(){
        return <div>Total: {this.total}</div>
    }
}
注意
  1. 如果任何影响computed values的值发生变化了,computed values将根据状态自动进行变化。 如果值未发生变化,它也不会变化
  2. computed values在计算期间抛出异常,异常会被捕获,并在读取值的时候抛出异常。抛出的异常不会中断跟踪,所有计算值可以从异常中恢复。
import React from 'react'
import {observable, computed} from 'mobx'
import { observer } from "mobx-react";

@observer
export default class Price extends React.Component{
    @observable value = 2
    @observable amount = 3
    
    @computed get total(){
        if(this.value === 0 ){
            throw new Error('value is 0')
        }
        return this.value * this.amount
    }
    set total(val){
        this.value = val
    }
    componentDidMount(){
        console.log(this.total) // 6
        this.total = 0
        console.log(this.total) // 报错, value is 0
        this.total = 5          
        console.log(this.total) // 10
    }
    render(){
        return <div>Total: {this.total}</div>
    }
}

3. autorun

autorun, 自动运行?是的,自动运行。

当观测到的数据发生变化的时候,如果变化的值处在autorun中,那么autorun就会自动执行。

import React from 'react'
import {observable, autorun} import 'mobx'

export default class Demo extends React.Component{
    @observable hello = 'world'
    @observable flag = false
    
    componentDidMount(){
        this.handler = autorun(() => {
            console.log('current flag value is: ', this.flag)
            if(flag){
                console.log('ouput: => ', this.hello)
            }
        })
    }
    componentWillUnmount(){
        // 为避免细微的内存问题,需要调用清理函数
        this.handler()
    }
    render(){
        return <div>
            <a onClick={() => this.flag = !this.flag }>click me to</a>
        </div>
    }
}

运行后,可以看到输出了“current flag value is false”, 当我们点击a标签时,输出了:

current flag value is false
output: => world

即,修改autorun中任意一个可观察的数据,即可触发自动运行。

3. when

when(提供了执行逻辑的条件,算是一种改进后的autorun), 接收两个函数参数:

  • 第一个函数:根据可观察数据返回一个布尔值。当该布尔值为true时,执行第二个函数,且只执行一次。
  • 第二个函数:如果可观察数据返回的布尔值一开始就是true,那么立即同步执行第二个函数。
import {when} from 'mobx'
// some code ...
@observable store: object = {
   name: "james",
   nick: "zhang",
   count: 0,
 };
 
// some code ...
componentDidMount(){
    this.handler = when(
     () => this.store.count > 10,
     () => {
       console.log("this.store.count value > 10", this.store.count);
     }
   );
}
componentWillUnmount(){
   // 为避免细微的内存问题,需要调用清理函数
   this.handler()
}

4. reaction

reaction(通过分离可观察数据声明,以副作用的方式,对autorun做出了改进) 按需确定哪些是我们需要我们观察的数据,在这些数据发生变化时,主动触发副作用函数,接收两个函数参数:

  • 第一个函数:引用可观察数据,并返回一个值,这个值会作为第二个函数的参数。
  • 第二个函数:也叫副作用函数。
    reaction(
      () => [this.store.name, this.store.count],
      (arr) => console.log("reaction => ", arr)
    );

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

5. action 修改可观察数据

autorun或reaction,在修改可观察数据时,都会触发并运行一次副作用函数,但是,通常情况下,这样的高频触发可能是没有必要的。如:用户多次点击了一个按钮,但实际可能只需触发一次回调即可。

此类场景,就比较适合使用action

action,是修改任何状态的行为,使用action的好处是能将多次修改可观察状态合并为一次,从而减少触发 autorun 或 reaction的次数。

import React from 'react'
import { observable, computed, reaction, action } from 'mobx'

export default class Demo extends React.Component{
    @observable name = 'james'
    @observable age = 30
    @action 
    change(){
        this.name = 'hello react'
        this.age = 8
    }
    componentDidMount(){
        reaction(() => [this.name, this.age], arr => {
            console.log(arr)
        })
    }
    render(){
        return <div>
            <button type="button" onClick={() => this.change()}>Change</button>
        </div>
    }
}

当我们点击按钮“Change”时,会触发回调函数连续修改2个变量的值,此时会发现,控制台只输出了一次,即reaction只被执行了一次。

6. action.bound

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 不要和箭头函数一起使用。箭头函数已经是绑定过的并且不能重新绑定

7. runInAction

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

import React from 'react'
import { observable, computed, reaction, action, runInAction } from 'mobx'

export default class Demo extends React.Component{
    @observable name = 'james'
    @observable age = 30
    @action.bound
    change(){
        this.name = 'hello react'
        this.age = 8
    }
    componentDidMount(){
        reaction(() => [this.name, this.age], arr => {
            console.log(arr)
        })
        
        setTimeout(() => {
            runInAction(() => {
                this.name = 'hello python'
                this.age = 31
            })
        }, 1000)
    }
    render(){
        return <div>
            <button type="button" onClick={this.change}>Change</button>
        </div>
    }
}

使用小技巧

1. 动态添加Mobx 对象属性

在使用对象时,如果需要动态添加新的属性,需要使用到extendObservable, 否则新增的属性不会被Mobx所监视。

import {observable, extendObservable} from 'mobx'
// some code ...
@observable obj:object = {hello: 'james'}
// some code ...
extendObservable(this.obj, {
    name: 'demo'
})

2. 尽早绑定函数

此技巧适用于普通的 React 和特别是使用了 PureRenderMixin 的库,尽量避免在 render 方法中创建新的闭包。 // bad

render() {
    return <MyWidget onClick={() => { alert('hi') }} />
}

在这个示例中, MyWidget 里使用的 PureRenderMixin 中的 shouldComponent 的返回值永远是 false,因为每当父组件重新渲染时你传递的都是一个新函数。

// good

render() {
    return <MyWidget onClick={this.handleClick} />
}

handleClick = () => {
    alert('hi')
}

3. 优化React组件渲染

@observer 组件会追踪它们使用的所有值,并且当它们中的任何一个改变时重新渲染。 所以你的组件越小,它们需要重新渲染产生的变化则越小;这意味着用户界面的更多部分具备彼此独立渲染的可能性。

在专用组件中渲染列表

这点在渲染大型数据集合时尤为重要。 React 在渲染大型数据集合时表现非常糟糕,因为observer必须评估每个集合变化的集合所产生的组件。 因此,建议使用专门的组件来映射集合并渲染这个组件,且不再渲染其他组件: // bad

@observer class MyComponent extends Component {
    render() {
        const {todos, user} = this.props;
        return (<div>
            {user.name}
            <ul>
                {todos.map(todo => <TodoView todo={todo} key={todo.id} />)}
            </ul>
        </div>)
    }
}

在这个示例中,当user.name值发生改变时,React会不协调所有的ToDoView组件,尽管ToDoView组件不会重新渲染,但是协助的过程本身也是非常昂贵的。

// good

@observer class MyComponent extends Component {
    render() {
        const {todos, user} = this.props;
        return (<div>
            {user.name}
            <TodosView todos={todos} />
        </div>)
    }
}

@observer class TodosView extends Component {
    render() {
        const {todos} = this.props;
        return <ul>
            {todos.map(todo => <TodoView todo={todo} key={todo.id} />)}
        </ul>)
    }
}

4. 永远要清理reaction

所有形式的 autorun、 observe 和 intercept, 只有所有它们观察的对象都垃圾回收了,它们才会被垃圾回收。 所以当不再需要使用它们的时候,推荐使用清理函数(这些方法返回的函数)来停止它们继续运行。 对于 observe 和 intercept 来说,当目标是 this 时通常不是必须要清理它们。 对于像 autorun 这样的 reaction 要棘手得多,因为它们可能观察到许多不同的 observable,并且只要其中一个仍在作用域内,reaction 将保持在作用域内,这意味着其使用的所有其他 observable 也保持活跃以支持将来的重新计算。 所以当你不再需要 reaction 的时候,千万要清理掉它们!

import React from 'react'
import {observable, autorun} import 'mobx'

export default class Demo extends React.Component{
    @observable hello = 'world'
    @observable flag = false
    
    componentDidMount(){
        this.handler = autorun(() => {
            console.log('current flag value is: ', this.flag)
            if(flag){
                console.log('ouput: => ', this.hello)
            }
        })
    }
    componentWillUnmount(){
        // 为避免细微的内存问题,需要调用清理函数
        this.handler()
    }
    render(){
        return <div>
            <a onClick={() => this.flag = !this.flag }>click me to</a>
        </div>
    }
}

相关链接