75

【第1304期】聊一聊Redux的前身Flux

 6 years ago
source link: http://www.10tiao.com/html/293/201806/2651228959/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言

抽奖这东西,欢喜就好,祝各位端午节安康。今日早读文章由网易考拉海购@Gloria投稿分享。

@Gloria,前端工程师,就职于网易考拉海购

正文从这开始~

现在诸多状态管理方案涌现,每种方案的背后都有支撑其实现的思想,而这些思想并不是“空穴来风”,都是为了解决开发中出现的各种问题而诞生。

接下来的会深入探讨时下比较流行的两种状态管理方案,Redux,Mobx。

为了深入了解Redux,不可避免地就要谈到它的前身Flux。

概念

在正文开始之前,我们需要理解在平时使用诸如react.jsvue.js这类MV*框架时接触到Model和View的概念。

MVVM两个概念示意图.PNG-3.2kB

一个完整的交互流程就如上图所示。

View

View,意为“视图”,即最终在浏览器上看到的页面元素。

Model

Model,翻译过来就是“模型”,那…什么是“模型”呢?且看下面这些代码。

<div>
   
<p>{{a.b}}</p>
   
<p>{{a.c}}</p>
   
<p>{{a.d}}</p>
</div>

上面这一段代码,其中a.ba.ca.d,每当这些属性值发生改变之后,框架会帮助我们生成View。

如果我们再稍微宏观地看待这一问题,其实可以将a这个对象看作是data(数据),而上面的html代码就是template(模板),于是就有了这种理解:框架通过将data应用到template上,最后生成View,即b过程。

在这里data+template就是Model,即所谓的“模型”,而通常意义上template是固定不变的,不会动态发生变化(这种动态变化已经被涵盖在模板本身的语法中了),所以大多数时候我们实现的各种交互就是改变data上属性的过程,示意图中的a过程。

目前开发中存在的问题

ok,介绍完Model和View这两个概念后,在这两个抽象层面上谈一谈平时开发过程中遇到的问题。

碎片化修改

我们实现交互基础就是操作Model,就拿上面那个代码片段来说,操作Model就是修改a.ba.ca.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:[]};//模拟数据库的对象
   
returnfunction(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框架是如何诞生的


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK