前端状态管理框架之Redux

2,281 阅读14分钟

随着应用程序单页面需求的越来越复杂,应用状态的管理也变得越来越混乱。应用的状态不仅包括从服务器获取的数据,还包括本地创建的数据,以及反应本地UI状态的数据,而Redux正是为解决这一复杂问题而存在的。

用Redux官网的话来概括什么是Redux:Redux是针对JavaScript应用的可预测状态容器

这句话虽然简短,但其实是有几个涵义的:

  • 可预测的(predictable): 因为Redux用了reducer与纯函数(pure function)的概念,每个新的state都会由旧的state建来一个全新的state,这样可以作所谓的时光旅行调试。因此,所有的状态修改都是"可预测的"。
  • 状态容器(state container): state是集中在单一个对象树状结构下的单一store,store即是应用程序领域(app domain)的状态集合。
  • JavaScript应用: 这说明Redux并不是单指设计给React用的,它是独立的一个函数库,可通用于各种JavaScript应用。

有些人可能会认为Redux一开始就是Facebook所创建的项目,其实不然,它主要是由Dan Abramov所开始的一个项目,Dan Abramov进入Facebook的React核心小组工作是最近的事情。在此之前,他还有创建另外还有其他相关项目,像React Hot Loader、React DnD,可能比当时的Redux项目还更广为人知,在Facebook发表Flux架构不久之后,许多Flux架构的类似函数库/框架,不论是加强版、进化版、大改版等等非常的多。Redux一开始的对外演示的大型活动,是在2015年的React-Europe研讨会,视频Live React: Hot Reloading with Time Travel。在视频中就有一个简单的说明,Redux用了"Flux + Elm"的概念。

当然除了Flux与Elm之外,还有其他的主要像RxJS中的概念与设计方式,Redux融合了各家的技术于一身,除了更理想的使用在Flux要解决的问题上之外,更延伸了一些不同的设计方式。但是对初学者来说,它也不容易学习,网络上常常见到初学者报怨Redux实在有够难学,这也并不是完全是Redux的问题,基本上来说Flux的架构原本就不是很容易理解,Redux还简化了Flux的流程与开发方式。

所以我们要理解Redux是什么,我们开始可以从这Flux与Elm两大基础来理解,以下分别说明一些基本的概念。

Flux

不论是Flux或其他以Flux架构为基础延伸发展的函数库(Alt、Reflux、Redux...)都是为了要解决同一个问题,这个问题在React应用规模化时会非常明显,简单以一句话来说就是:应用程序领域(app domain)的状态 - 简称为App state

应用程序都需要有App state(应用程序状态),不论是在一个需要用户登录的应用,要有全局的记录着用户登录的状态,或是在应用程序中不同操作介面(组件)或各种功能上的数据沟通,都需要用到它。如果你已经有一些程序语言或应用的开发经验,你应该知道这会像是MVC设计模式中的Model(模型)部份该作的事情。

React应用为什么会出现这个问题?原因主要是来自React组件的本身设计造成的。React被设计为一个相似于MVC架构中的View(视图)的函数库,当然实际上它可以作的事情比MVC中的View(视图)还要更多,但本质上的确React不是一个完整的应用程序开发框架,里面没有额外的架构可以作类似Model(模型)或Controller(控制器)的事情。对小型的组件或应用而言,应用的数据都包含在里面,也就是在View(视图)之中。

有学过React的一些基础的开发者应该会知道,在React中的组件是无法直接更动state(状态)的包含值,要透过setState方法来进行更动,这有很大的原因是为了Virtual DOM(虚拟DOM)的所设计,这是其中一点。另外在组件的树状阶层结构,父组件(拥有者)与子组件(被拥有者)的关系上,子组件是只能由父组件以props(属性)来传递属性值,子组件自己本身无法更改自己的props,这也是为什么一开始在学习React时,都会看到大部份的例子只有在最上层的组件有state,而且都是由它来负责进行当数据改变时的重新渲染工作,子组件通常只有负责呈现数据。

当然,有一个很技巧性的方式,是把父组件中的方法声明由props传递给子组件,然后在子组件触发事件时,调用这个父组件的方法,以此来达到子组件对父组件的沟通,间接来更动父组件中的state。不过这个作法并不直觉,需要事先规范好两边的方法。在简单的应用程序中,这沟通方式还可行,但如果是在有复杂的组件嵌套阶层结构时,例如层级很多或是不同树状结构中的子组件要互相沟通时,这个作法是派不上用场的。

在复杂的组件树状结构时,唯一能作的方式,就是要将整个应用程序的数据整合在一起,然后独立出来,也就是整个应用程序领域的数据部份。另外还需要对于数据的所有更动方式,也要独立出来。这两者组合在一起,就是称之为"应用程序领域的状态",为了区分组件中的状态(state),这个作为应用程序领域的持久性数据集合,会被称为store(存储)。

store(存储)并不是只有应用程序单纯的数据集合而已,它还包含了所有对数据的变更方法。

store(存储)的角色并非只是组件中的state(状态)而已,它也不会只有单纯的记录数据,可能在现今的每种不同的Flux延伸的函数库,对于store的定义与设计都有所不同。在Flux的架构中的store中,它包含了对数据更动的函数/方法,Flux称这些函数/方法为"存储查询(Store Queries)",也把它的角色定位为类似传统MVC的Model(模型),但与传统的Model(模型)最大明显不同之处的是,store只能透过Action(动作)以"间接"的方式来自我刷新。

store的设计可以解决应用程序的状态存放与更动的问题,但它还不能完整的解决整个问题,只是一个开端。最困难的地方在于,要如何在触发动作时,进行store(存储)的更动查询,以及进行呈现数据的更动与最后作整个应用程序的渲染。这一连串的步骤,整合为一个数据流(Data Flow),Flux的名称来由其实就是拉丁文中的Flow,Flux用单向(unidirectional)数据流来设计整个数据流的运作,也就是说整个数据的流动方向都是一致的,从在网页上呈现的操作介面组件,被触发事件后,传送动作到发送器,再到store,最后进行整个应用的重新渲染,都是往单一个方向运行。

所以说,单向数据流是Flux架构的核心设计。下面是Flux简单的流程示意图:

这个数据流的位于最中心的设计是一个AppDispatcher(应用发送器),你可以把它想成是个发送中心,不论来自组件何处的动作都需要经过它来发送。每个store会在AppDispatcher上注册它自己,提供一个callback(回调),当有动作(action)发生时,AppDispatcher(应用发送器)会用这个回调函数通知store。

由于每个Action(动作)只是一个单纯的对象,包含actionType(动作类型)与数据(通常称为payload),我们会另外需要Action Creator(动作创建器),它们是一些辅助函数,除了创建动作外也会把动作传给Dispatcher(发送器),也就是调用Dispatcher(发送器)中的dispatch方法。

Dispatcher(发送器)的用途就是把接收到的actionType与数据(payload),广播给所有注册的callbacks。它这个设计并非是独创的,这在设计模式中类似于pub-sub(发布-订阅)系统,Dispatcher则是类似Eventbus的概念。Dispatcher类的设计很简单,其中有两个核心的方法,这两个是互为相关的函数:

  • dispatch 发送payload(相当于动作)给所有注册的callbacks。组件触发事件时用这个方式来发送动作。
  • register注册在所有payload(相当于动作)发送时要调用的callbacks(回调)。这些callbacks(回调)就是上面说的会用来更动store的Store Queries(存储查询)。

在数据流的最后,store要触发最上层组件的setState,然后进行整体React的重新渲染工作。Flux提出的方式是一种自订事件的监听方式,把store用EventEmitter.prototype对象进行拓展,让store具有监听事件的能力,然后在最上层组件中的生命周期中,加入有更动时的监听事件。这是由于JavaScript中内建的Event、CustomEvent等介面,以及addListener、dispatch等方法,只能实作在具有事件介面的网页DOM元素上。单纯在JavaScript的对象上是没有办法使用,要靠额外的函数库才能这样作,这是一定要使用类似像EventEmitter这种函数库的主要原因。

不过,你可能会觉得为什么不干脆一点直接对store上面作更动就好了,一定要拐这么大一个弯,透过Action(动作)"间接"的方式来作自我刷新?

我想原因之一,是要标准化Action(动作)的规格,也就是所有在应用程序中的组件,都得要按照这些动作来触发事件,发送器中注册的callbacks(回调)也是要写成处理同一种规格的动作。Action(动作)主要由type(类型)与payload(有效数据)组成,Flux Standard Action(Flux标准动作)就是提出来要标准化Action(动作)的格式,有了统一格式的Action对象,在刷新数据时所有刷新方式会具统一性,这样Flux才有办法把整个数据流运作完成一个循环再接着下一个。就像网络的传输协定一样,数据的格式与运作的流程,都有标准的规范,不是随随便便就可以进行传输。当然还有一些其它的原因,例如要避免Event Chains(事件连锁)的发生。

其整个流程可以用下面的方式表示:

事件触发 -> 
由Action Creator调用Dispatcher.dispatch(action) -> 
Dispatcher调用已注册的回调(callback) -> 
调用对应的存储查询(Store Queries) -> 
触发Store更动事件 -> 
进行整个应用的重新渲染

总结来说,Flux使用了单向数据流的设计架构,是为了要解决React的应用程序领域状态的问题。Flux的实作并不容易,有许多实作上的细节与开发步骤上都有分割不明确的问题,所以在此并不讨论Flux的实作部份。在Flux发表之后(约为2014年中),陆陆续续出现了许多函数库与框架,都是基于Flux的基本设计概念,都是为了要改善、简化或自动化其中的实作步骤为主,而Redux也是其中一套。在经过一段时间之后,目前较热门的与较多人使用的,就属Redux,它有很多的设计概念都来自于Flux,能多加理解Flux的基本设计概念,对于学习Redux是绝对有帮助的。

Elm

或许你有听过函数式程序开发(functional programming, FP)的开发风格,FP是什么?用下面的一句话来说明,摘译自这篇教程文档: 函数式程序开发就是只使用"纯粹函数"与"不可改变的值"来撰写软件应用的一种方式。

FP是现今相当热门的一种程序开发风格,在很早之前就已经有一些纯函数式程序开发的语言例如Haskell与OCaml,Elm也是一个纯函数式程序开发的语言,它是一个很年轻的语言,Elm是专门用来开发网站应用程序的程序语言,最终编译为JavaScript在网页上运行,它与JavaScript语言有多差异很大的设计,例如:

  • Elm是强(静态)数据类型的,它的数据类型也满多样的;
  • Elm是纯FP的语言;
  • Elm-Architecture是包含在Elm的应用框架,它是单向数据流的架构。

React与Flux中有许多设计,都有应用到FP的设计,与Elm中一部份设计相当类似。而Redux又使用更多Elm中的设计,尤其是Elm-Architecture而来的,例如:

  • 不可改变性(Immutability): 所有的值在Elm中都是不可改变的,Redux中的纯函数(pure function)与Reducer的设计很类似,React的设计中也有这类的概念
  • 时光旅行调试(Time Traveling Debugger): 在Elm有这个设计,Redux学了过来

说明:Redux作者使用了FP(函数式程序开发)与Elm的架构,改进或简化原本的Flux架构

Redux特性

Redux是目前最热门的、最多人使用的Flux架构类的函数库,虽然Redux也可以用于其他的函数库,但基本上它是专门为了React应用所打造的。如果你真的要学会React,并用它来开发一个稍有规模的应用,学习Redux说是一条必经之路,当然也有其他的Flux架构类函数库可以选择,不同的函数库有可能使用的解决方式与样式相差会非常大。目前来说Redux的开发社群是最庞大也是最活跃的,而且不见得其他的函数库就会更容易学习与使用,毕竟用得人多,你会遇到的问题大概都有人遇过,也都能找得到解决方式,这是开源码生态圈的红利。

Redux会受欢迎不是没有原因的,以下分析几个Redux的优点:

1,使用了FP(函数式程序开发)与React可以配合得很好

Redux不同于Flux架构,它改采几乎是纯FP(函数式程序开发)的解决方式,目的是为了要简化Flux中数据流的处理实作,也的确可以与React中的组件渲染配合得很好,这证明了它是找到了一个较为理想的与React应用能密切合作的解决方式。FP(函数式程序开发)也是目前JavaScript界的热门主题,Redux也因此吸引到不少开发者的目光。

2,时光旅行调试/热重新加载

Redux一开始就附了时光旅行调试工具与热重新加载(hot reloading)的工具来提升开发体验,这对开发者有很大的吸引力,这也代表在Redux应用上的数据变动,可以更容易的测试与调试,这是其他Flux架构类函数库或框架中所没有的见到的。

3,更简化的代码,更多可能的延伸应用

Redux一开始的版本只有99行代码,这可能比一开始的Flux架构使用的API更要少,不过代码少不见得概念就简单,FP的撰写风格多半追求的是更简短的代码,这需要高超的技巧、深度的概念与不少的基础。Redux一开始就可以很容易的使用于服务器端渲染,而且也不限于使用于React应用上,这也吸引了更多的开发者使用意愿。

4,更多的文件,发展良好的生态圈

Redux作者一开始就撰写非常多的文件与教程,让许多开发者能更快捷地掌握Redux的应用技术,Redux作者也是技术讨论区的常客,常常可以看到他在讨论区上回覆相关的问题。Redux的项目也是相当活跃的,有非常多的参与者在讨论与解决问题,对于重大效能/臭虫问题也是很快捷地解决。