React 状态管理库: Mobx

React 是一个专一于视图层的库。React 维护了状态到视图的映射关系,开发者只需关心状态便可,由 React 来操控视图。javascript

在小型应用中,单独使用 React 是没什么问题的。但在复杂应用中,容易碰到一些状态管理方面的问题,如:前端

  • React 只提供了在内部组件修改状态的接口 setState。致使数据、业务逻辑和视图层耦合在组件内部,不利于扩展和维护。java

  • React 应用即一颗组件树。兄弟节点,或者不在同一树杈的节点之间的状态同步是很是麻烦。node

  • 关心性能的状况下,须要手动设置 shouldComponentUpdatereact

这时就须要引入状态管理库。如今经常使用的状态管理库有 Mobx 和 Redux,本文会重点介绍 Mobx,而后会将 Mobx 和 Redux 进行对比,最后展望下将来的 React 状态管理方面趋势。git

Mobx 简介

Mobx 的理念很是简单,能够用一个 demo 就把其核心原理说清楚。Mobx/MobxReact 中有三个核心概念,observableobserveraction。为了简单起见,本文没有说起 computed 等概念。github

  • observable: 经过 observable(state) 定义组件的状态,包装后的状态是一个可观察数据(Observable Data)。数组

  • observer: 经过 observer(ReactComponent) 定义组件。性能优化

  • action: 经过 action 来修改状态。dom

简化图以下:

图片描述

只讲概念还比较模糊,下面给你们举个例子。

点击运行 https://jsfiddle.net/jhwleo/1L5jcykr/9/

// 经过 observable 定义组件的状态
const user = mobx.observable({
    name: "Jay",
     age: 22
})

// 经过 action 定义如何修改组件的状态
const changeName = mobx.action(name => user.name = name)
const changeAge = mobx.action(age => user.age = age)

// 经过 observer 定义 ReactComponent 组件。
const Hello = mobxReact.observer(class Hello extends React.Component {
        componentDidMount(){
            // 视图层经过事件触发 action
        changeName('Wang') // render Wang
    }

    render() {
                // 渲染
            console.log('render',user.name);
        return <div>Hello,{user.name}!</div>
    }
})

ReactDOM.render(<Hello />, document.getElementById('mount'));

// 非视图层事件触发,外部直接触发 action
changeName('Wang2')// render Wang2
// 重点:没有触发从新渲染
// 缘由:Hello 组件并无用到 `user.age` 这个可观察数据
changeAge('18')  // no console

例子看完了,是否是很是简单。

使用 Mobx,组件状态能够在外部定义(也能够在组件内部),所以,数据、业务逻辑能够轻易地和视图层分离,提升应用的可扩展性和可维护性。另外,因为组件状态能够在外部定义,兄弟节点之间的状态同步也很是容易。最后一点, Mobx 知道何时应该渲染页面,所以基本不须要手动设置 shouldComponentUpdate 来提升应用性能。

接下来给你们介绍下 Mobx 中 observable observer action 的用法,并会简单介绍一下其原理。

observable

Mobx 如此简单的缘由之一,就是使用了可观察数据(Observable Data)。简单说,可观察数据就是能够观察到数据的读取、写入,并进行拦截。

Mobx 提供了 observable 接口来定义可观察数据。定义的可观察数据,一般也是组件的状态。该方法接收一个参数,参数能够是原始数据类型、普通 Object、Array、或者 ES6 中的 Map 类型,返回一个 observable 类型的参数。

Array.isArray(mobx.observable([1,2,3])) === false // true
mobx.isObservable(mobx.observable([1,2,3])) === true // true

注意,数组通过 observable 包装后,就不是 Array 类型了,而是 Mobx 定义的一个特殊类型 ———— observable 类型。observable 类型,能够经过 mobx.isObservable 来检查。

虽然数据类型不同,可是使用方式基本和原来一致(原始数据类型除外)。

const observableArr =  mobx.observable([1,2,3]);
const observableObj =  mobx.observable({name: 'Jay'});
const observableMap =  mobx.observable(new Map([['name','Wang']]));

console.log(observableArr[0])  // 1
console.log(observableObj.name)  // Jay
console.log(observableMap.get('name'))  // Wang

可观察数据类型的原理是,在读取数据时,经过 getter 来拦截,在写入数据时,经过setter 来拦截。

Object.defineProperty(o, key, {
  get : function(){
        // 收集依赖的组件
    return value;
  },
  set : function(newValue){
        // 通知依赖的组件更新
        value = newValue
  },
});

在可观察数据被组件读取时,Mobx 会进行拦截,并记录该组件和可观察数据的依赖关系。在可观察数据被写入时,Mobx 也会进行拦截,并通知依赖它的组件从新渲染。

observer

observer 接收一个 React 组件做为参数,并将其转变成响应式(Reactive)组件。

// 普通组件
const Hello = mobxReact.observer(class Hello extends React.Component {
    render() {
        return <div>Hello,{user.name}!</div>
    }
})

// 函数组件
const Hello = mobxReact.observer( () => (
    <div>Hello,{user.name}!</div>
))

响应式组件,即当且仅当组件依赖的可观察数据发生改变时,组件才会自动响应,并从新渲染。

在本文最开始的例子中,响应式组件依赖了 user.name,可是没有依赖 user.age。因此当user.name 发现变化时,组件更新。而 user.age 发生变化时,组件没有更新。

这里再详细分析本文中的第一个例子:

user.name = 'Wang2'// render Wang2
// 重点:没有触发从新渲染
// 缘由:Hello 组件并无用到 `user.age` 这个可观察数据
user.age = '18'  // no console

当可观察数据变化时,Mobx 会调用 forceUpdate 直接更新组件。

源码地址

而在传统 React 应用中,当状态、属性变化后会先调用 shouldComponentUpdate,该方法会深层对比先后状态和属性是否发生改变,再肯定是否更新组件。

shouldComponentUpdate 是很消耗性能的。Mobx 经过可观察数据,精确地知道组件是否须要更新,减小了调用 shouldComponentUpdate 这一步。这是 Mobx 性能好的缘由之一。

另外须要注意的是 observer 并非 mobx 的方法,而是 mobx-react 的方法。mobxmobx-react 关系如同 reactreact-dom

action

在 Mobx 中是能够直接修改可观察数据,来进行更新组件的,但不建议这样作。若是在任何地方都修改可观察数据,将致使页面状态难以管理。

全部对可观察数据地修改,都应该在 action 中进行。

const changeName = mobx.action(name => user.name = name)

使用 Mobx 能够将组件状态定义在组件外部,这样,组件逻辑和组件视图便很容易分离,兄弟组件之间的状态也很容易同步。另外,也再也不须要手动使用 shouldComponentUpdate 进行性能优化了。

Mobx 与 Redux 对比

Mobx 的优点来源于可变数据(Mutable Data)和可观察数据 (Observable Data) 。

Redux 的优点来源于不可变数据(Immutable data)。

可观察数据的优点,在前文已经介绍过了。如今再来聊聊可变数据和不可变数据。

顾名思义,可变数据和不可变数据的区别在于,可变数据建立后能够修改,不可变数据建立后不能够修改。

可变数据,能够直接修改,因此操做起来很是简单。这使得使用 mobx 改变状态,变得十分简单。

不可变数据并不必定要用到 Immutable 库。它彻底能够是一种约定,只要建立后不修改便可。好比说,Redux 中的 state。每次修改都会从新生成一个 newState ,而不会对原来的值进行改变。因此说 Redux 中的 state 就是不可变数据。

reducer(state, action) => newState.

不可变数据的优点在于,它可预测,可回溯。示例代码以下:

function foo(bar) {
  let data = { key: 'value' };
  bar(data);
  console.log(data.key); // 猜猜会打印什么?
}

若是是可变数据,data.key 的值可能会在 bar 函数中被改变,因此不能肯定会打印什么值。可是若是是不可变数据,那么就能够确定打印值是什么。这就是不可变数据的优点 ———— 可预测。不可变数据不会随着时间的变化(程序的运行)而发生改变。在须要回溯的时候,直接获取保存的值便可。

Mobx 与 Redux 技术选型的本质,是在可变数据与不可变数据之间选择。具体业务场景的技术选型,还须要根据实际状况进行分析,脱离业务场景讨论技术选型是没有意义的。但我我的在状态管理的技术选型上,仍是倾向于 Mobx 的。缘由是前端与反作用打交道很是频繁,有 Http 请求的反作用,Dom 操做的反作用等等。使用不可变数据,还必须得使用中间件对反作用封装;在 Redux 中修改一次状态,须要通过 Action、Dispatch、Reducer 三个步骤,代码写起来太啰嗦;而前端的程序以中小型程序为主,纯函数带来的可预测性的收益,远不及其带的代码复杂度所须要付出的成本。而 Mobx 使用起来更加简单,更适合如今以业务驱动、快速迭代的开发节奏。

展望:Mobx 与不可变数据的融合

不可变数据和可变数据,都是对状态的一种描述。那么有没有一种方案,能将一种状态,同时用可变数据和不可变数据来描述呢?这样就能够同时享有两者的优点了。(注意:当咱们说可变数据时,一般它仍是可观察数据,后文统一只说可变数据。)

答案是确定的,它就是 MST(mobx-state-tree) https://github.com/mobxjs/mob...

MST 是一个状态容器:一种状态,同时包含了可变数据、不可变数据两种不一样的形式。

为了让状态能够在可变数据和不可变数据两种形式之间可以高效地相互转化,必须遵循 MST 定义状态的方法。

在 MST 中,定义状态必须先定义它的结构。状态的结构是一颗树(tree),树是由多层模型(model)组成,model 是由多个节点组成。

在下面的代码中,树只有一层 model,该 model 也只有一个节点:title。title 的类型是事先定好的,在这里是 types.string。树的结构定义好后,经过 create 方法传入数据,就生成树。

import {types} from "mobx-state-tree"

// declaring the shape of a node with the type `Todo`
const Todo = types.model({
    title: types.string
})

// creating a tree based on the "Todo" type, with initial data:
const coffeeTodo = Todo.create({
    title: "Get coffee"
})

在一些稍微复杂的例子中,树的 model 能够有多层,每层能够有多个节点,有些节点定义的是数据类型(types.xxx),有些节点直接定义的是数据。下面的示例中,就是定义了一个多层多节点的树。除此以外,注意 types.model 函数的第一个参数定义的是 model 的名字,第二参数定义的是 model 的全部属性,第三个参数定义的是 action。

import { types, onSnapshot } from "mobx-state-tree"

const Todo = types.model("Todo", {
    title: types.string,
    done: false
}, {
    toggle() {
        this.done = !this.done
    }
})

const Store = types.model("Store", {
    todos: types.array(Todo)
})

// create an instance from a snapshot
const store = Store.create({ todos: [{
    title: "Get coffee"
}]})

最关键的来了,请看下面的代码。

// listen to new snapshots
onSnapshot(store, (snapshot) => {
    console.dir(snapshot)
})

// invoke action that modifies the tree
store.todos[0].toggle()
// prints: `{ todos: [{ title: "Get coffee", done: true }]}`

在上述代码的第一部分,使用 onSnapshot 监听状态的改变。第二部分,调用 store.todos[0].toggle() ,在这个 action 中经过使用可变数据的方式,直接修改了当前的状态。同时在 onSnapshot 生成了一个状态快照。这个状态快照就是状态的不可变数据的表现形式。

MST 这么神奇,那么具体怎么用呢?MST 只是一个状态容器,同时包含了可变数据和不可变数据。你能够用 MST 直接搭配 React 使用。能够 MST + Mobx + React 配合着用,还能够 MST + Redux + React 混搭着用。

MST 比较新,业内的实践很是少,若是不是急需,如今还能够先观望一下。

相关文章
相关标签/搜索