前言
抽奖这东西,欢喜就好,祝各位端午节安康。今日早读文章由网易考拉海购@Gloria投稿分享。
@Gloria,前端工程师,就职于网易考拉海购
正文从这开始~
现在诸多状态管理方案涌现,每种方案的背后都有支撑其实现的思想,而这些思想并不是“空穴来风”,都是为了解决开发中出现的各种问题而诞生。
接下来的会深入探讨时下比较流行的两种状态管理方案,Redux,Mobx。
为了深入了解Redux,不可避免地就要谈到它的前身Flux。
概念 在正文开始之前,我们需要理解在平时使用诸如react.js
、vue.js
这类MV*框架时接触到Model和View的概念。
MVVM两个概念示意图.PNG-3.2kB 一个完整的交互流程就如上图所示。
View View,意为“视图”,即最终在浏览器上看到的页面元素。
Model Model,翻译过来就是“模型”,那…什么是“模型”呢?且看下面这些代码。
上面这一段代码,其中a.b
,a.c
,a.d
,每当这些属性值发生改变之后,框架会帮助我们生成View。
如果我们再稍微宏观地看待这一问题,其实可以将a
这个对象看作是data(数据),而上面的html代码就是template(模板),于是就有了这种理解:框架通过将data
应用到template
上,最后生成View,即b
过程。
在这里data
+template
就是Model
,即所谓的“模型”,而通常意义上template
是固定不变的,不会动态发生变化(这种动态变化已经被涵盖在模板本身的语法中了),所以大多数时候我们实现的各种交互就是改变data
上属性的过程,示意图中的a
过程。
目前开发中存在的问题 ok,介绍完Model和View这两个概念后,在这两个抽象层面上谈一谈平时开发过程中遇到的问题。
碎片化修改 我们实现交互基础就是操作Model,就拿上面那个代码片段来说,操作Model就是修改a.b
,a.c
,a.d
,于是操作这个Model就会像下图所展示的情况一样,修改操作会“碎片化”地存在于整个组件文件的各个角落。
碎片化.PNG-24.6kB 对于没有严格开发模式限制的工程,一旦页面复杂度上去了,如果多人维护这样的代码,添加feature的时候可以说会比较刺激了。
大多数情况的表情应该是这样的
黑人问号.jpg-5.4kB 数据流捉摸不定 1. 复杂的数据流
先来谈一谈vue.js
之类基于检测数据变动实现局部更新的MVVM框架,这些框架提供了多种多样影响Model的方式。
看一看这张图
复杂数据流.PNG-13.7kB 最明显的,跟上面那张图相比,增加了从View到Model这一个方向,这种改变自然是框架“双向数据绑定”所带来的。毫无疑问,这种feature给我们带来了一定的便利,但与此同时,它会使得最终生成View的逻辑更加扑朔迷离,为什么这么说呢?
从另外一个角度看待这个问题,最终到View的不同路径数越多,就代表生成View的方式越多,生成View的方式越多,代码的可预测性就越弱。
很显然,在这张图当中,以View做为终点的路径还是不少的,以碎片化修改为起点的路径有2条,以View作为起点的路径有3条。
从路径数量这个角度,很直观地就可以得出这类框架设计对于代码可维护性是不友好的。
2. 简单的数据流
但是,也有一些框架数据流是比较简单的(比如React),改变Model的方式仅限于手动调用setState,或者View触发setState,在代码的predicatable(可预测性)方面有比较大的优势。
React数据流.PNG-12.3kB OK,以上这些与这次的主题有什么关系呢?
Flux 上面已经谈到了现在MV*框架中存在的问题,比如vue,react等都仅仅是视图层框架,也就是说,它们只负责渲染View,而对于Model的变化没有统一的管理方案。
Flux的出现其实就是为了管理Model的变化,使得应用的可伸缩性,和代码的可预测性更强。
单向数据流基础 Flux其实就是在React单向数据流的基础上做了一层对Model的管理,那就看一看它是如何借鉴的。
单向数据流基础.PNG-17.9kB 相比其它框架设计,最大的不同之处就是:React没有View-->Model
这个方向。就拿上面复杂数据流方案来说,以View为起点的数据流路径就可以减少两条,保证了最终生成View的逻辑是相对清晰的。
如何看待Flux架构 Flux其实提供了一整套Model修改模式。这种模式的初衷,在我看来,就是为了提高代码的可预测性,再通俗一点就是,当你看到了一段代码时,让你更清晰地知道它会做什么。
为什么这么说呢?我们在维护工程时无外乎就是扮演两个角色:使用者和定义者。而往往我们在代码中确很少体现这两种角色抽象,最多也只是在文档和代码规范层面,任你玩出花来,也很难做到比较高的通用性。
再具体一点,Flux将使用者和定义者的抽象引入了Model的修改过程。类似Clent-Service架构,如果使用者(客户端)想要修改数据库,必须通过调用定义者(服务端)提供的接口实现。
1. 请求
在Flux中,request(请求)等价于action,触发一个action相当调用一次接口,action的type字段相当于接口地址,其它字段相当于payLoad(请求参数)。
action应该是一个对象:
{ type: 'delete-todo' , //接口地址 todoID: '1234' //payLoad };
既然将action当作了request,那么我们应该如何实现server(服务器)呢?
2. 路由
就像Clent-Service中一样,server接收请求并将不同的请求映射为相应的数据库修改操作。将server中接收请求的部分称为router(路由)。
一个router应该长这样:
let router = ( function router (){ let dataBase = { todos : []}; //模拟数据库的对象 return function ( request ){ switch ( request . type ){ case 'ADD_TODO' : deleteToDo ( request , dataBase ); break ; ... } }; })();
发送一个请求:
router ({ type : 'delete-todo' , todoID : '1234' });
deleteToDo()
其实就是相应修改数据库的操作,里面的具体逻辑需要我们自己写,显然,删除一个”待办事项”,deleteToDo()
应该长下面这样:
function deleteToDo ( request , dataBase ){ let todos = dataBase . todos ; for ( let i = 0 ; i < todos ; i ++){ if ( todos [ i ]. id === request . todoID ){ todos. splice ( i , 1 ); return ; } } }
ok,到目前为止,整个流程已经跑通了。定义一个request,使用router发送这个request,router根据request地址分配相应的数据库处理逻辑,于是就得到了下面这种抽象:
单dataBase架构.PNG-7kB 用上面这种架构已经可以勉强驾驭一些比较简单的应用场景,而面对稍微复杂一点的应用场景就捉襟见肘了,为什么这么说呢?
这种架构最基本的应用单元就是组件,每个组件的Model其实就是对应的dataBase,如果我们想在某个组件内修改其它组件的dataBase,就需要拿到这个组件的router,而”拿router”这件事可并没有那么简单。。大体上根据组件之间的关系,分为3种情况:父子关系、爷孙关系和兄弟关系,于是就会出现下面这种情况。
多dataBase架构.PNG-23.1kB 为了解决这一问题,Flux的另一个概念就来了,dispatcher。
3. 请求分发器
Flux的dispatcher(请求分发器),其实解决了上述问题。
dispatcher相对各个组件而言是全局性的,它可以将请求发送到所有的router,用户无需知道他需要请求的router,让每个router自行处理进来的request,这种抽象其实是将request视为全局性请求,一个request可以同时操作多个dataBase。
引入dispatcher.PNG-16.7kB 当然,dispatcher不会自己寻找它需要分发到的router,我们需要调用register()
方法手动注册router
dispatcher . register ( router );
在注册好router后,直接调用dispatcher的dispatch()
方法即可,可以像下面这样发送一个request:
dispatcher . dispatch ({ type : 'delete-todo' , todoID : '1234' });
默认情况下,Flux会按照注册顺序依次将request放进router。如果我们希望自定义发送request后,部分router的执行顺序怎么办?Flux提供了waitFor()
方法。
举个例子:routerA接收到请求之后,希望依次经过routerB,和routerC,可以像下面伪代码这样实现:
let tokenB = dispatcher . register ( routerB ); let tokenC = dispatcher . register ( routerC ); let routerA = function ( request ){ switch ( request . type ){ case 'ADD_TODO' : dispatcher . waitFor ( tokenB , tokenC ); break ; ... } };
OK,你必须提前拿到routerB和routerC的token
,然后按照顺序传入waitFor()
方法(个人认为这种”拿token”,无异于上面提到的”拿router”,是一个设计缺陷)。
4. 数据库
dataBase(数据库)其实就代表了组件的state(状态)。
而Flux将router和dataBase视为一体,将请求的解析和数据库的修改统一交给store来处理。
store.reduce()
相当于router,而store._state
则相当于dataBase,于是就有了下面这种架构
store架构.PNG-19kB 最后,Flux采用了向外抛事件的方式,将_state
映射到Model的工作交给用户去解决。
你可以调用store.addListener()
方法,传入回调函数即可监听到_state
的变化。
store . addListener (() => { let state = store . getState (); ...映射到 Model 的操作... });
结语 Flux的一整套抽象(action,dispatcher,store),在单向数据流的基础上可以提高应用的可维护性和代码的可预测性。然而,全局action+多store的架构面对复杂的应用依然不能很好地解决复杂数据流的问题,waitFor()
虽然可以满足自定义多store接收action的顺序,但是它会让数据流变得复杂,难以维护。
Redux作为Flux的继承者,单store的架构其实就很好地避免了上述问题,之后的文章会深入分析Redux是如何在Flux的基础上改进自身架构的。
参考:
Flux官方介绍:In Depth Overview
Flux官方仓库:github.com/facebook/flux
关于本文
作者:@Gloria
原文:
https://zhuanlan.zhihu.com/p/38050036
最后,为你推荐
【第1265期】那些前端MVVM框架是如何诞生的