Redux从设计到源码

莹莹 ·2017-07-14 16:23

本文主要讲述三方面内容:html

  1. Redux 背后的设计思想
  2. 源码分析以及自定义中间件
  3. 开发中的最佳实践

Redux背后的设计思想

在讲设计思想前,先简单讲下Redux是什么?咱们为何要用Redux?前端

1. Redux是什么?

Redux是JavaScript状态容器,能提供可预测化的状态管理。数据库

它认为:redux

  • Web应用是一个状态机,视图与状态是一一对应的。
  • 全部的状态,保存在一个对象里面。

咱们先来看看“状态容器”、“视图与状态一一对应”以及“一个对象”这三个概念的具体体现。后端

Store是Redux中的状态容器,它里面存储着全部的状态数据,每一个状态都跟一个视图一一对应。数组

Redux也规定,一个State对应一个View。只要State相同,View就相同,知道了State,就知道View是什么样,反之亦然。promise

好比,当前页面分三种状态:loading(加载中)、success(加载成功)或者error(加载失败),那么这三个就分别惟一对应着一种视图。前端工程师

如今咱们对“状态容器”以及“视图与状态一一对应”有所了解了,那么Redux是怎么实现可预测化的呢?咱们再来看下Redux的工做流程。架构

首先,咱们看下几个核心概念:app

  • Store:保存数据的地方,你能够把它当作一个容器,整个应用只能有一个Store。
  • State:Store对象包含全部数据,若是想获得某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫作State。
  • Action:State的变化,会致使View的变化。可是,用户接触不到State,只能接触到View。因此,State的变化必须是View致使的。Action就是View发出的通知,表示State应该要发生变化了。
  • Action Creator:View要发送多少种消息,就会有多少种Action。若是都手写,会很麻烦,因此咱们定义一个函数来生成Action,这个函数就叫Action Creator。
  • Reducer:Store收到Action之后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫作Reducer。Reducer是一个函数,它接受Action和当前State做为参数,返回一个新的State。
  • dispatch:是View发出Action的惟一方法。

而后咱们过下整个工做流程:

  1. 首先,用户(经过View)发出Action,发出方式就用到了dispatch方法。
  2. 而后,Store自动调用Reducer,而且传入两个参数:当前State和收到的Action,Reducer会返回新的State
  3. State一旦有变化,Store就会调用监听函数,来更新View。

到这儿为止,一次用户交互流程结束。能够看到,在整个流程中数据都是单向流动的,这种方式保证了流程的清晰。

2. 为何要用Redux?

前端复杂性的根本缘由是大量无规律的交互和异步操做。

变化和异步操做的相同做用都是改变了当前View的状态,可是它们的无规律性致使了前端的复杂,并且随着代码量愈来愈大,咱们要维护的状态也愈来愈多。

咱们很容易就对这些状态什么时候发生、为何发生以及怎么发生的失去控制。那么怎样才能让这些状态变化能被咱们预先掌握,能够复制追踪呢?

这就是Redux设计的动机所在。

Redux试图让每一个State变化都是可预测的,将应用中全部的动做与状态都统一管理,让一切有据可循。

若是咱们的页面比较复杂,又没有用任何数据层框架的话,就是图片上这个样子:交互上存在父子、子父、兄弟组件间通讯,数据也存在跨层、反向的数据流。

这样的话,咱们维护起来就会特别困难,那么咱们理想的应用状态是什么样呢?

架构层面上讲,咱们但愿UI跟数据和逻辑分离,UI只负责渲染,业务和逻辑交由其它部分处理,从数据流向方面来讲, 单向数据流确保了整个流程清晰。

咱们以前的操做能够复制、追踪出来,这也是Redux的主要设计思想。

综上,Redux能够作到:

  • 每一个State变化可预测。
  • 动做与状态统一管理。

3. Redux思想追溯

Redux做者在Redux.js官方文档Motivation一章的最后一段明确提到:

Following in the steps of Flux, CQRS, and Event Sourcing , Redux attempts to make state mutations predictable by imposing certain restrictions on how and when updates can happen.

咱们就先了解下Flux、CQRS、ES(Event Sourcing 事件溯源)这几个概念。

3.1. 什么是ES?

  • 不是保存对象的最新状态,而是保存对象产生的事件。
  • 经过事件追溯获得对象最新状态。

举个例子:咱们日常记帐有两种方式,直接记录每次帐单的结果或者记录每次的收入/支出,那么咱们本身计算的话也能够获得结果,ES就是后者。

与传统增删改查关系式存储的区别:

  • 传统的增删是以结果为导向的数据存储,ES是以过程为导向存储。
  • CRUD是直接对库进行操做。
  • ES是在库里存了一系列事件的集合,不直接对库里记录进行更改。

优势:

  • 高性能:事件是不可更改的,存储的时候而且只作插入操做,也能够设计成独立、简单的对象。因此存储事件的成本较低且效率较高,扩展起来也很是方便。
  • 简化存储:事件用于描述系统内发生的事情,咱们能够考虑用事件存储代替复杂的关系存储。
  • 溯源:正由于事件是不可更改的,而且记录了全部系统内发生的事情,咱们能用它来跟踪问题、重现错误,甚至作备份和还原。

缺点:

  • 事件丢失:由于ES存储都是基于事件的,因此一旦事件丢失就很难保证数据的完整性。
  • 修改时必须兼容老结构:指的是由于老的事件不可变,因此当业务变更的时候新的事件必须兼容老结构。

3.2. CQRS(Command Query Responsibility Segregation)是什么?

顾名思义,“命令与查询职责分离”-->”读写分离”。

总体的思想是把Query操做和Command操做分红两块独立的库来维护,当事件库有更新时,再来同步读取数据库。

看下Query端,只是对数据库的简单读操做。而后Command端,是对事件进行简单的存储,同时通知Query端进行数据更新,这个地方就用到了ES。

优势:

  • CQ两端分离,各自独立。
  • 技术代码和业务代码彻底分离。

缺点:

  • 强依赖高性能可靠的分布式消息队列。

3.3. Flux是什么?

Flux是一种架构思想,下面过程当中,数据老是“单向流动”,任何相邻的部分都不会发生数据的“双向流动”,这保证了流程的清晰。Flux的最大特色,就是数据的“单向流动”。

  1. 用户访问View。
  2. View发出用户的Action。
  3. Dispatcher收到Action,要求Store进行相应的更新。
  4. Store更新后,发出一个“change”事件。

介绍完以上以后,咱们来总体作一下对比。

3.3.1. CQRS与Flux

相同:当数据在write side发生更改时,一个更新事件会被推送到read side,经过绑定事件的回调,read side得知数据已更新,能够选择是否从新读取数据。

差别:在CQRS中,write side和read side分属于两个不一样的领域模式,各自的逻辑封装和隔离在各自的Model中,而在Flux里,业务逻辑都统一封装在Store中。

3.3.2. Redux与Flux

Redux是Flux思想的一种实现,同时又在其基础上作了改进。Redux仍是秉承了Flux单向数据流、Store是惟一的数据源的思想。

最大的区别:

  1. Redux只有一个Store。

Flux中容许有多个Store,可是Redux中只容许有一个,相较于Flux,一个Store更加清晰,容易管理。Flux里面会有多个Store存储应用数据,并在Store里面执行更新逻辑,当Store变化的时候再通知controller-view更新本身的数据;Redux将各个Store整合成一个完整的Store,而且能够根据这个Store推导出应用完整的State。

同时Redux中更新的逻辑也不在Store中执行而是放在Reducer中。单一Store带来的好处是,全部数据结果集中化,操做时的便利,只要把它传给最外层组件,那么内层组件就不须要维持State,所有经父级由props往下传便可。子组件变得异常简单。

  1. Redux中没有Dispatcher的概念。

Redux去除了这个Dispatcher,使用Store的Store.dispatch()方法来把action传给Store,因为全部的action处理都会通过这个Store.dispatch()方法,Redux聪明地利用这一点,实现了与Koa、RubyRack相似的Middleware机制。Middleware可让你在dispatch action后,到达Store前这一段拦截并插入代码,能够任意操做action和Store。很容易实现灵活的日志打印、错误收集、API请求、路由等操做。

除了以上,Redux相对Flux而言还有如下特性和优势:

  1. 文档清晰,编码统一。
  2. 逆天的DevTools,可让应用像录像机同样反复录制和重放。

目前,美团外卖后端管理平台的上单各个模块已经逐步替换为React+Redux开发模式,流程的清晰为错误追溯和代码维护提供了便利,现实工做中也大大提升了人效。

源码分析

查看源码的话先从GitHub把这个地址上拷下来,切换到src目录,

看下总体结构:

其中utils下面的Warning.js主要负责控制台错误日志的输出,咱们直接忽略index.js是入口文件,createStore.js是主流程文件,其他4个文件都是辅助性的API。

咱们先结合下流程分析下对应的源码。

首先,咱们从Redux中引入createStore方法,而后调用createStore方法,并将Reducer做为参数传入,用来生成Store。为了接收到对应的State更新,咱们先执行Store的subscribe方法,将render做为监听函数传入。而后咱们就能够dispatchaction了,对应更新view的State。

那么咱们按照顺序看下对应的源码:

4. 入口文件index.js

入口文件,上面一堆检测代码忽略,看红框标出部分,它的主要做用至关于提供了一些方法,这些方法也是Redux支持的全部方法。

而后咱们看下主流程文件:createStore.js。

5. 主流程文件:createStore.js

createStore主要用于Store的生成,咱们先整理看下createStore具体作了哪些事儿。

首先,一大堆类型判断先忽略,能够看到声明了一系列函数,而后执行了dispatch方法,最后暴露了dispatch、subscribe……几个方法。这里dispatch了一个init Action是为了生成初始的State树。

咱们先挑两个简单的函数看下,getState和replaceReducer,其中getState只是返回了当前的状态。replaceReducer是替换了当前的Reducer并从新初始化了State树。这两个方法比较简单,下面咱们在看下其它方法。

订阅函数的主要做用是注册监听事件,而后返回取消订阅的函数,它把全部的订阅函数统一放一个数组里,只维护这个数组。

为了实现实时性,因此这里用了两个数组来分别处理dispatch事件和接收subscribe事件。

store.subscribe()方法总结:

  • 入参函数放入监听队列
  • 返回取消订阅函数

再来看下store.dispatch()-->分发action,修改State的惟一方式。

store.dispatch()方法总结:

  • 调用Reducer,传参(currentState,action)。
  • 按顺序执行listener。
  • 返回action。

到这儿的话,主流程咱们就讲完了,下面咱们讲下几个辅助的源码文件。

6. bindActionCreators.js

bindActionCreators把action creators转成拥有同名keys的对象,使用dispatch把每一个action creator包装起来,这样能够直接调用它们。

实际状况用到的并很少,唯一的应用场景是当你须要把action creator往下传到一个组件上,却不想让这个组件觉察到Redux的存在,并且不但愿把Redux Store或dispatch传给它。

7. combineReducers.js-->用于合并Reducer

这个方法的主要功能是用来合并Reducer,由于当咱们应用比较大的时候Reducer按照模块拆分看上去会比较清晰,可是传入Store的Reducer必须是一个函数,因此用这个方法来做合并。代码不复杂,就不细讲了。它的用法和最后的效果能够看下上面左侧图。

8. compose.js-->用于组合传入的函数

compose这个方法,主要用来组合传入的一系列函数,在中间件时会用到。能够看到,执行的最终结果是把各个函数串联起来。

9. applyMiddleware.js-->用于Store加强

中间件是Redux源码中比较绕的一部分,咱们结合用法重点看下。

首先看下用法:

const store = createStore(reducer,applyMiddleware(…middlewares))
or
const store = createStore(reducer,{},applyMiddleware(…middlewares))

能够看到,是将中间件做为createStore的第二个或者第三个参数传入,而后咱们看下传入以后实际发生了什么。

从代码的最后一行能够看到,最后的执行代码至关于applyMiddleware(…middlewares)(createStore)(reducer,preloadedState)而后咱们去applyMiddleware里看它的执行过程。

能够看到执行方法有三层,那么对应咱们源码看的话最终会执行最后一层。最后一层的执行结果是返回了一个正常的Store和一个被变动过的dispatch方法,实现了对Store的加强。

这里假设咱们传入的数组chain是[f,g,h],那么咱们的dispatch至关于把原有dispatch方法进行f,g,h层层过滤,变成了新的dispatch。

由此的话咱们能够推出中间件的写法:由于中间件是要多个首尾相连的,须要一层层的“加工”,因此要有个next方法来独立一层确保串联执行,另外dispatch加强后也是个dispatch方法,也要接收action参数,因此最后一层确定是action。

再者,中间件内部须要用到Store的方法,因此Store咱们放到顶层,最后的结果就是:

看下一个比较经常使用的中间件redux-thunk源码,关键代码只有不到10行。

做用的话能够看到,这里有个判断:若是当前action是个函数的话,return一个action执行,参数有dispatch和getState,不然返回给下个中间件。

这种写法就拓展了中间件的用法,让action能够支持函数传递。

咱们来总结下这里面的几个疑点。

9.0.1. Q1:为何要嵌套函数?为什么不在一层函数中传递三个参数,而要在一层函数中传递一个参数,一共传递三层?

由于中间件是要多个首尾相连的,对next进行一层层的“加工”,因此next必须独立一层。那么Store和action呢?Store的话,咱们要在中间件顶层放上Store,由于咱们要用Store的dispatch和getState两个方法。action的话,是由于咱们封装了这么多层,其实就是为了做出更高级的dispatch方法,是dispatch,就得接受action这个参数。

9.0.2. Q2:middlewareAPI中的dispatch为何要用匿名函数包裹呢?

咱们用applyMiddleware是为了改造dispatch的,因此applyMiddleware执行完后,dispatch是变化了的,而middlewareAPI是applyMiddleware执行中分发到各个middleware,因此必须用匿名函数包裹dispatch,这样只要dispatch更新了,middlewareAPI中的dispatch应用也会发生变化。

9.0.3. Q3: 在middleware里调用dispatch跟调用next同样吗?

由于咱们的dispatch是用匿名函数包裹,因此在中间件里执行dispatch跟其它地方没有任何差异,而执行next至关于调用下个中间件。

到这儿为止,源码部分就介绍完了,下面总结下开发中的最佳实践。

最佳实践

官网中对最佳实践总结的很到位,咱们重点总结下如下几个:

  • 用对象展开符增长代码可读性。
  • 区分smart component(know the State)和dump component(彻底不须要关心State)。
  • component里不要出现任何async calls,交给action creator来作。
  • Reducer尽可能简单,复杂的交给action creator。
  • Reducer里return state的时候,不要改动以前State,请返回新的。
  • immutable.js配合效果很好(但同时也会带来强侵入性,能够结合实际项目考虑)。
  • action creator里,用promise/async/await以及Redux-thunk(redux-saga)来帮助你完成想要的功能。
  • action creators和Reducer请用pure函数。
  • 请慎重选择组件树的哪一层使用connected component(链接到Store),一般是比较高层的组件用来和Store沟通,最低层组件使用这防止太长的prop chain。
  • 请慎用自定义的Redux-middleware,错误的配置可能会影响到其余middleware.
  • 有些时候有些项目你并不须要Redux(毕竟引入Redux会增长一些额外的工做量)

做者简介

莹莹,美团外卖前端研发工程师,2016年加入美团外卖,负责外卖商家管理平台以及销售人员App蜜蜂的整个上单流程开发。

最后,附上一条硬广,美团外卖长期诚聘高级前端工程师/前端技术专家,欢迎发送简历至:tianhuan02#meituan.com。

相关文章
相关标签/搜索