Vue & Mobx 更便捷的全局状态共享

相信每个前端开发者在开发SPA应用的时候一定会碰到这样的问题:全局状态管理。


不同的前端框架都有各自推荐状态管理器。现在最流行的2个MVVM框架Vue和React,社区内推荐使用的状态管理器是Vuex和Redux。状态管理器的有2种使用者:有经验的&没经验的。对应有经验的开发者很容易知道状态管理器是什么东西、怎么去使用、还有中间的各自概念词汇,但是还是需要走一条比较长的路才可以改变最终的数据状态。对于没有经验的开发者,事情就变得更加复杂,首先要搞清楚状态管理器的用途,然后理解action、dispatch、reducer、getState这样一套流程一定会觉得无可适从。这时一个没有这么多新概念,而且将这套流程简化的工具库会成为初学者的福音,同时这个简化后的工具库也十分有利于复杂的场景开发,对于老手来说可以减少出bug的几率。而这个简化的工具库就是:Mobx。


进入Vue结合Mobx这个主题前,我们简要概括一下现在流行的两个状态管理器使用的流程。首先看Redux:

它的闭环流程是上面图片中的线路图,要显式调用到的方法有5个。而且整个数据结构相对抽象,一开始并没有一个比较直观的数据结构图,都是通过Reducer去组装的State状态树。修改数据的流程都是要走dispatch->action->subscribe这样一个回路。这样除了不直观外,还要额外写一个action对象,reducer对象等,拆开后几个互不关联的文件没有显式的方法表示他们是互通的,这样调试起来也比较难理解。


然后我们看看Vuex的流程图:


它的闭环流程是上面图片中的线路图,要显式调用到的方法有4个。这个比Redux要进步了,而且它可以显式定义全局状态对象,比较直观看到它的内部数据结构。但是它也离不开修改数据要走一个dispatch->action->commit这样的回路。同样存在项目大时候,拆开后几个互不关联的文件没有显式的方法表示他们是互通的,这样调试起来也比较难理解的问题。


既然上述的库要学习这么多新概念,做那么多操作才可以改变store,那么有没有一些更简单的方法呢?先来看看store是什么,store是存储状态的,状态state是一个对象,这个对象的属性值被项目组件引用而且实时共享。假如有一个简单的方法直接修改这个store,同时又可以通知到所有引用该store属性的组件实时更新值,那任务就完成了。


为了解决这个问题,Mobx这个状态管理库应运而生。Mobx是一个简单、可扩展的状态管理器。它通过透明的函数响应式编程(transparently applying functional reactive programming - TFRP)使得状态管理变得简单和可扩展。


看一下Mobx的操作流程:



它的闭环流程是上面图片中的线路图Mobx为开发者设计了一个很人性化的选项,严格模式和非严格模式。在Mobx的严格模式下面,修改store里面的状态要显式调用1个方法。在非严格模式下,修改store里面的状态直接像操作普通对象那样用赋值语句重新赋值就可以成功修改。调用的时候显式import引入store,让程序会更加直观。


了解了状态管理库的基本原理和简要对比后,我们来实现一个Demo,用Mobx替代Vuex用作Vue项目的状态管理器。


首先先建立一个全局store类:

                                             


这是一个标准的官方建立Mobx Store的方法,不依赖任何当前使用的前端框架,export出来是一个Mobx实例。一眼看上去可能觉得这种写法有点奇怪,但是并没有那么难,因为这里只涉及到一个新概念:ES7的属性装饰器。装饰器听起来很别扭,用一个通俗一点的术语就是一个语法糖,再通俗一点就是一个函数...装饰器后面的变量是函数的实参。至于函数怎么处理现在暂时不用管,new出来的Mobx实例是一个对象,这个对象拥有上述定义的方法和属性。在使用装饰器前,要在.babelrc文件里面加入babel-plugin-transform-decorators-legacy这个编译器,OK所有东西准备就绪。创建Class的时候像创建普通的类那样定义好方法和属性后,在属性前面加上@observable的装饰器,这个装饰器表示这个属性的被Mobx系统观察,只要被重新赋值,Mobx实例里面依赖这个属性的计算值会自动被重新计算,该属性值和依赖它的计算属性都会被同步更新。@computed装饰器表示该属性是计算属性,它不能被直接修改,只能通过修改它依赖的@observable观察属性来更新计算属性。@action装饰器比较特别,在Mobx非严格模式下,我们直接修改@observable观察属性就可以更新store里面的数据。但在Mobx严格模式下,要通过@action装饰器定义的方法才可以修改@observable观察属性的值。当然官方是推荐使用严格模式和action,这样对数据的修改会更加规范。


在Vue组件里面怎么使用呢?一起来看下面的代码:

                                          


这是一个由官方vue-cli脚手架生成的vue例子。我们尽量不加入太多自己内部实现的方法,尽量用Mobx和Vue官方提供出来的东西去实现无缝的结合。这个例子实现了一个todo list的添加和修改一个页面计数器count。只看代码比较抽象,下面我们对这个例子进行截图,看看最终是实现了什么样的效果:


Page1和Page2是一个SPA里面的2个不同组件页面,他们都共享了Mobx生成的实例Store状态。


这里要先引入2个Mobx库里面提供的方法:toJS和reaction。由于todoListStore.todos是Mobx实例里面的一个方法,Vue系统并不能对其实例方法进行Object.defineProperty进行转换,所以要用toJS转换为一个标准的JavaScript对象属性。由于Vue系统对其自身注册的data属性进行绑定,那么我们第一步就是先将Mobx的Store里面和Vue实例的data进行挂钩和绑定。在修改状态时候,我们并不对挂载的data属性进行直接修改,而且用Mobx实例定义的action方法“addCount”、“addTodo”进行修改(这里模拟Mobx的严格模式)。修改后Mobx实例里面的@observable观察属性会自动被更新,但是这个时候页面还不知道要更新属性,所以要用到一个reaction的方法进行通知。Reaction方法接收2个参数,参数类型是function。第一个参数返回的是被@observable观察的属性的变动量,第二个参数是接收第一个函数返回的值作为实参,然后传递给当前方法。


这里有一个比较重要的概念是Mobx对什么进行观察的问题。官方文档说,它是对定义Mobx实例时候定义的@observable属性进行观察,是属性!属性!属性!重要的事情说三次,并不是指属性的值、值、值,可以对属性的直接或者间接引用进行观察。再回来我们Demo上面,todoList原来是一个空数组,现在给数组里面插入元素,但因为数组是引用类型,它实际内存地址是没有变,所以值是没有改变,Mobx是观察不到数组的变化的。但是数组里面的length属性是会根据数组里面的元素多少而变化,而这个属性是todo的数组的间接引用属性,这个属性会被Mobx观察到。所以我们reaction里面第一个方法返回被@observable观察的属性的间接引用,也就是todoList的length属性,然后再将Vue实例里面的data属性重新赋值,这时候将store里面更新后的数组拷贝一次赋值给data属性list,那么Vue实例就会更新UI。对count属性赋值时候,实际上这个属性的内存地址发生了改变,Mobx认为这个属性的引用是变化了,就会直接出发reaction方法。


在Page2上面:

                                          

这里我们也是显式引入Mobx生产的Store实例,这个实例是SPA页面里面的单例,同时Mobx已经帮我们处理了实例里面的状态共享和跟踪问题。在组件上面直接将要显式的Store值赋值给Vue实例data属性,那么Page2组件也可以读取到已经修改后最新的Store的count属性和todoList列表。这里我们尝试用Mobx的非严格模式是修改count属性值,我们直接用赋值语句对其进行减一操作,Mobx系统就会触发reaction函数,这时候对Vue实例data重新赋值就可以更新Page2的组件UI。


最后简要介绍一下Mobx系统的原理。

                                                 

这是Mobx实例Store的@observable被观察属性和对应@computed计算属性(观察者)或者@observable自有属性(观察者)的依赖关系图。


1)一个被观察属性发生变化,它向其所有观察者发送一个通知,以表明它已经过时;任何受它影响的计算属性将递归地将通知传递给观察者;最后,依赖关系树的一部分将被标记为已经过时。在图中⑤观察者,⑤通过③②间接观察①,在①被改变时,①发送一个通知表明它已经过时,通知传播路径②→③→⑤,观察属性和观察者外面圆圈被标记为橙色虚线。


2)被观察属性①在发送已过时通知并存储新值之后,将发送ready通知。这个通知消息能表示该被观察属性是否确实发生了变化。


3)当所有观察者收到一个ready通知后,它们知道它们观察的值确实已经被改变,这样它们将开始重新计算。这样减少被重复计算的次数。例如图中观察者④的计算值只有在被观察者③计算值改变之后才会重新计算,因为③依赖②①。


4)如果被观察属性没有再发ready通知表明自己已经改变的话,被观察属性就会标记自己为完成计算的状态,否则,被观察属性将重新计算,并发送一个准备就绪的消息给自己的观察者。这个被观察和观察的依赖关系如图⑤→③→②→①。特别注意的是,假如④的值没有变化,图中”-”观察者是不会被执行。


总结一下,通过Mobx可以为组件提供统一全局的状态管理器,这个状态管理器是独立的库,可以被任何需要状态管理的前端框架使用。这个库提供了更加简便的方式去更新Store里面的数据,并且提供了严格模式和非严格模式选择。严格模式下通过action去更新state,非严格模式下直接对state赋值就可以实现更新。结合Vue库使用,可以将其Store映射到Vue实例的data属性里面,通过修改Store触发reaction方法从而对Vue实例的data属性重新赋值并触发Vue组件重新渲染。


但是现在还有一个没有完成点,最理性的状态是Vue的computed计算属性去获取Store里面state属性,这样可以保证开发者不会错误修改到Vue实例的data属性。这个改造需要结合Vuex的原理去改造,暂时还处于研究和实验的阶段。


参考资料:

https://mobx.js.org/getting-started.htmlhttp://cn.mobx.js.org/

https://medium.com/@mweststrate/becoming-fully-reactive-an-in-depth-explanation-of-mobservable-55995262a254

https://tech.meituan.com/vuex-code-analysis.html

https://github.com/vuejs/vuex/tree/dev/src


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者陈偲伟授权发布。