MobX

一.目标定位

Simple, scalable state management

一个简单,够用的状态管理库。还是想要解决应用状态(数据)管理的问题

二.设计理念

Anything that can be derived from the application state, should be derived. Automatically.

源于应用状态的所有东西,都应该自动得到。比如UI,数据序列化,服务通信

也就是说,只要知道哪些东西是状态相关的(源于应用状态),在状态发生变化时,就应该自动完成状态相关的所有事情,自动更新UI,自动缓存数据,自动通知server

这种理念看似新奇,其实就是数据驱动,细想一下,React体系(react + react-redux + redux + redux-saga)也满足这种理念,状态变化(dispatch action引发stateChange)后,UI自动更新(Container update),自动触发缓存数据,通知server等副作用(saga

三.核心实现

MobX is inspired by reactive programming principles as found in spreadsheets. It is inspired by MVVM frameworks like in MeteorJS tracker, knockout and Vue.js. But MobX brings Transparent Functional Reactive Programming to the next level and provides a stand alone implementation. It implements TFRP in a glitch-free, synchronous, predictable and efficient manner.

参考了MeteorJStrackerknockout以及Vue,这几个东西的共同特点是都内置了数据绑定,属于所谓的MVVM架构,分别借鉴了:

  • MeteorJS的设计理念:自动追踪依赖(tracker, autorun等等),不用再声明依赖,让很多事情变得更简单

  • knockout的数据绑定:ko.observable

  • Vue的运行时依赖收集和computed:基于getter&setter数据绑定实现

所以,MobX的核心实现与Vue非常相似,可以看做把Vue的数据绑定机制单拎出来,再做增强和扩展:

  • 增强:observable不仅支持Array, Object,还支持Map及不可变的Value(对应boxed value

  • 扩展:提供observer(把数据变化暴露出来),spy(把内部状态暴露出来),action(规范约束,或是为了迎合Flux)

P.S.从功能上来看,有observable和observer就能保证可用了。action算是对灵活性的约束,spy用于DevTools接入,都不重要

另外,MobX还利用ES Decorator语法让监听变化与OOP结合起来,看起来相当优雅,例如:

import { observable, computed } from "mobx";

class OrderLine {
    @observable price = 0;
    @observable amount = 1;

    @computed get total() {
        return this.price * this.amount;
    }
}

如果没有这种类注解语法,那就一点也不漂亮了:

var OrderLine = function() {
    extendObservable(this, {
        price: observable.ref(0),
        amount: observable.ref(1),
        total: computed(function() {
            return this.price * this.amount;
        })
    });
}

这样用起来感觉麻烦了很多,远不及注解形式优雅。利用Decorator把observable和OOP体系结合起来,算是MobX的一大亮点

P.S.Decorator特性目前还处于new proposal阶段,属于很不稳定的特性,因此大多只用一般形式:

function myDecorator(target, property, descriptor){}

babel转换结果上看,算是对Object.defineProperty的拦截(所以Decorator方法签名与Object.defineProperty完全一致)

P.S.其实Vue生态也有类似的与OOP结合的东西,例如vuejs/vue-class-component

四.结构

       modify        update           trigger
action ------> state ------> computed -------> reaction

对比Flux

保留了Flux的action,新增了一层computed,提出了reaction的概念

这里的action比Flux的action概念要厚得多,相当于action + dispatcher + store里负责响应action修改state的部分,简言之,MobX的action是动词,Flux的action是名词。MobX的action是一个动作,直接修改状态,Flux的action只是个事件消息,由事件接收方(store里负责响应action修改state的部分)修改状态

computed与Vue的computed含义相同,都是指依赖state的衍生数据(能根据state算出来的数据),state变化后,自动重新计算computed。另外,computed在概念上被称为derivation,也就是“衍生”,因为computed依赖state,是从state衍生出来的数据

reaction指的是对state变化做出的响应,比如更新视图,或者通知server(利用autorun)。与computed最大的区别是computed产生新数据不含副作用(而reaction含副作用但不产生新数据)

与Flux的(state, action) => state思路基本一致,computed可以看作上层state,而reaction里的一个重要部分就是更新视图,那么就简化成了:

       modify         trigger
action ------> state  -------> views

对比Flux的结构:

action             传递action         update state
------> dispatcher ---------> stores ------------> views

如上面提到的,action + dispatcher + store里负责响应action修改state的部分才等价于MobX的action

对比Redux

                  call             new state
action --> store ------> reducers -----------> view

(引自Redux

Redux里的reducer在MobX里都给塞进action了,不用再拿reducer来描述state结构,也不用再关注reducer纯不纯(MobX只要求computed是纯函数)

computed在Redux里是片空白,所以由reactjs/reselect来填补,同样为了复用数据衍生逻辑,同样自带缓存。所以MobX至少相当于Redux + reselect

对比Vuex

       commit           mutate        render
action ------> mutation ------> state ------> view

Vuex的特点是从设计上区分了同步/异步action,分别对应mutation和action

比起MobX,恰好是两个极端。Vuex嫌Flux的action不够细化,没有考虑异步场景,才提出了mutation之上的action,而MobX嫌区分同步异步,纯与不纯太麻烦,才提出了动词action,囊括异步和副作用

computed在Vuex里叫做getter,二者没什么太大区别。Vuex也是一开始就考虑了state衍生数据,不像Redux需要reselect来填补空白

五.优势

从实现上看,只有MobX内置了数据变化监听,也就是把数据绑定的核心工作提到了数据层,这样做的最大好处是修改state变得很自然,不需要dispatch,也不用造action,想改就直接按直觉去改

状态修改方式符合直觉

React示例:

@observer
class TodoListView extends Component {
    render() {
        return <div>
            <ul>
                {this.props.todoList.todos.map(todo =>
                    <TodoView todo={todo} key={todo.id} />
                )}
            </ul>
            Tasks left: {this.props.todoList.unfinishedTodoCount}
        </div>
    }
}

const TodoView = observer(({todo}) =>
    <li>
        <input
            type="checkbox"
            checked={todo.finished}
            {/* 想改就直接改 */}
            onClick={() => todo.finished = !todo.finished}
        />{todo.title}
    </li>
)

(完整示例见React components

不用为了改状态去定义action(甚至为了定义状态去添reducer),要改直接改,不用通过类库API。这一点与Vue数据绑定的优势相同,类库自己能监听到数据变化,不需要用户手动通知变化,业务写起来方便了

更强大的DevTools

Flux中action层的核心作用是让状态变化可追溯,action作为状态变化的原因可以被记录下来(DevTools或logger),而MobX把函数名作为action携带的原因信息,通过spy实现状态变化可追溯,可以实现更强大的DevTools,比如让组件的数据依赖可视化

mobx-react-devtools

组件级的精确数据绑定

相比react-redux,mobx-react能做到更精确的视图更新,组件粒度的精确重渲染,不像react-redux需要从外部(Container)向下diff找到需要重新渲染的View,MobX明确知道数据依赖关系,不用找。那么从性能上看,至少节省了找dirty View的成本

另一个性能点是mobx-react去掉了Container的概念,实际上是通过劫持组件生命周期的方式来实现的(具体见下面源码简析部分),这样就减少了React组件树深度,理论上性能会稍好一些

另外,因为依赖收集是由MobX完成的,带来的好处是能分析出实际需要的数据依赖,避免了人为产生的不必要的Container带来的性能损耗

P.S.关于运行时依赖收集机制的更多信息,请查看运行时依赖收集机制

不限制state的结构

Flux要求state是个纯对象,这样不仅强迫用户花精力去设计state的结构,还强制把数据和相应操作分开了,用MobX的话来讲:

But this introduces new problems; data needs to be normalized, referential integrity can no longer be guaranteed and it becomes next to impossible to use powerful concepts like prototypes.

限制state不能被随意修改,这样建立在数据模型上的一些原有优势就没了,比如原型

而MobX对state的结构及类型都没有什么限制,MobX里state的定义是:

Graphs of objects, arrays, primitives, references that forms the model of your application.

不要求单一状态树,也不要求纯对象,例如:

class ObservableTodoStore {
    @observable todos = [];
    @observable pendingRequests = 0;
​
    constructor() {
        mobx.autorun(() => console.log(this.report));
    }
​
    @computed get completedTodosCount() {
        return this.todos.filter(
            todo => todo.completed === true
        ).length;
    }
​
    @computed get report() {
        if (this.todos.length === 0)
            return "<none>";
        return `Next todo: "${this.todos[0].task}". ` +
            `Progress: ${this.completedTodosCount}/${this.todos.length}`;
    }
​
    addTodo(task) {
        this.todos.push({
            task: task,
            completed: false,
            assignee: null
        });
    }
}
​
const observableTodoStore = new ObservableTodoStore();

这样的state定义是MobX的基本玩法,不用从业务中剥离出共享数据,也不用担心当前的state结构能否满足将来的场景(以后有多条数据怎么办,数据量太大了怎么办,state结构要怎么调整)……数据和相应操作可以关联在一起,爱怎么组织都行(用class,或者保持Bean + Controller)

在迁移现有项目时,更能突显出不限制state结构的优势,不改变原有的model定义,侵入性很小,只需要添一些注解,就能获得状态管理层带来的好处,何乐不为?想象一下给一个复杂的老项目上Redux,至少需要:

  1. 把共享状态都提出来,作为state

  2. 把对应的操作也都提出来,作为reducer和saga,并保证reducer结构与state一致

  3. 定义action,把数据和操作关联起来

  4. 在合适的地方插入Container

  5. 把所有修改state的部分都换成dispatch

……算了,成本极高,不建议重构

六.源码简析

mobx

核心部分是Observable,也就是负责完成@observable装饰动作的部分:

export class IObservableFactories {
    box<T>(value?: T, name?: string): IObservableValue<T> {
        if (arguments.length > 2) incorrectlyUsedAsDecorator("box")
        return new ObservableValue(value, deepEnhancer, name)
    }

    shallowBox<T>(value?: T, name?: string): IObservableValue<T> {
        if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowBox")
        return new ObservableValue(value, referenceEnhancer, name)
    }

    array<T>(initialValues?: T[], name?: string): IObservableArray<T> {
        if (arguments.length > 2) incorrectlyUsedAsDecorator("array")
        return new ObservableArray(initialValues, deepEnhancer, name) as any
    }

    shallowArray<T>(initialValues?: T[], name?: string): IObservableArray<T> {
        if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowArray")
        return new ObservableArray(initialValues, referenceEnhancer, name) as any
    }

    map<T>(initialValues?: IObservableMapInitialValues<T>, name?: string): ObservableMap<T> {
        if (arguments.length > 2) incorrectlyUsedAsDecorator("map")
        return new ObservableMap(initialValues, deepEnhancer, name)
    }

    //...还有很多
}

(摘自mobx/src/api/observable.ts

递归向下给数据身上都挂上getter&setter,例如Class Decorator的实现:

const newDescriptor = {
    enumerable,
    configurable: true,
    get: function() {
        if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true)
            typescriptInitializeProperty(
                this,
                key,
                undefined,
                onInitialize,
                customArgs,
                descriptor
            )
        return get.call(this, key)
    },
    set: function(v) {
        if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true) {
            typescriptInitializeProperty(
                this,
                key,
                v,
                onInitialize,
                customArgs,
                descriptor
            )
        } else {
            set.call(this, key, v)
        }
    }
}
// 定义getter&setter
if (arguments.length < 3 || (arguments.length === 5 && argLen < 3)) {
    Object.defineProperty(target, key, newDescriptor)
}

(摘自mobx/src/utils/decorators.ts

数组的变化监听见mobx/src/types/observablearray.ts,与Vue的实现没太大区别

mobx-react

“Container”的实现如下:

// 注入的生命周期逻辑
const reactiveMixin = {
    componentWillMount: function() {},
    componentWillUnmount: function() {},
    componentDidMount: function() {},
    componentDidUpdate: function() {},
    shouldComponentUpdate: function(nextProps, nextState) {}
}
// 劫持组件的生命周期
function mixinLifecycleEvents(target) {
    patch(target, "componentWillMount", true)
    ;["componentDidMount", "componentWillUnmount", "componentDidUpdate"].forEach(function(
        funcName
    ) {
        patch(target, funcName)
    })
    if (!target.shouldComponentUpdate) {
        target.shouldComponentUpdate = reactiveMixin.shouldComponentUpdate
    }
}

(摘自mobx-react/src/observer.js

劫持组件声明周期主要有3个作用:

  • 把数据更新与UI更新关联起来

  • 把组件状态暴露出去,接入DevTools

  • 内置shouldComponentUpdate优化

react-redux通过setState({})来触发Container更新,而mobx-react通过forceUpdate来触发被劫持的View更新:

const initialRender = () => {
    if (this.__$mobxIsUnmounted !== true) {
        let hasError = true
        try {
            isForcingUpdate = true
            if (!skipRender) Component.prototype.forceUpdate.call(this)
            hasError = false
        } finally {
            isForcingUpdate = false
            if (hasError) reaction.dispose()
        }
    }
}

(摘自mobx-react/src/observer.js

接入DevTools的部分:

componentDidMount: function() {
    if (isDevtoolsEnabled) {
        reportRendering(this)
    }
},
componentDidUpdate: function() {
    if (isDevtoolsEnabled) {
        reportRendering(this)
    }
}

内置的shouldComponentUpdate:

shouldComponentUpdate: function(nextProps, nextState) {
    if (this.state !== nextState) {
        return true
    }
    return isObjectShallowModified(this.props, nextProps)
}

(摘自mobx-react/src/observer.js

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code