陈帅华的个人网站 - 探索技术艺术与国学之美

学习Redux

全篇共 2690 字。按500字/分钟阅读,预计用时 5.4 分钟。

Redux 是管理应用状态并且使状态变得可预测的 JavaScript 库。随着单页面应用交互越来越复杂,其中需要管理的状态越来越多。Redux 专为像 React 等需要前端工程师关心“状态如何驱动视图”的库而诞生,但不止于前端MVVM,Redux 对状态管理的思考对任何使用场景下的产品都有普适价值。Redux 只有 2KB 大小,但其潜在的生态和影响力不小。

npm i redux
npm i react-redux
npm i -D redux-devtools

状态树与动作描述

下面是一个简单的待办事项应用在初始化前或在用户使用过程中会产生的状态数据。Redux 并不高深,它将对状态的每一次更新(包括因UI操作直接触发的和间接触发的)都描述为一个“动作”,如果状态发生了变化,开发者就能清楚的知道是什么“动作”造成了什么状态的变化。确定了应用有哪些状态,接下来要定义哪些“动作”会改变状态。

{
  todos: [{
    text: 'Eat food',
    completed: true
  }, {
    text: 'Exercise',
    completed: false
  }],
  visibilityFilter: 'SHOW_COMPLETED'
}

下方的3个对象就代表3个“动作”,每个对象都至少包含一个 type 属性。第一个“动作”是为待办事项的数据内添加一条待办事项。第二个“动作”代表设置某条事项的完成与否,“TOOGLE”的意思就是:如果该事项是已完成状态那么就设置为未完成状态,相反,如果未完成那么就设置为已完成状态,可以理解为总是将这次的状态设置为上一个状态的“反状态”。第三个“动作”代表将满足条件的事项筛选为可见,默认“SHOW_ALL”现实全部事项。细心的你一定发现了 type 的属性值是字符串,且都是大写字母,常量常用大写。

{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }

上面,有了状态,又有了描述操作状态的“动作”。为了将状态和动作联系在一起,Redux 说开发者还需要按照特定的规范自行编写了一个称为“reducer”的函数,但函数名叫什么由开发者自己决定,但一定是按“语义化”原则要易读易于其他人理解。这个叫“reducer”的函数和 Redux 一样没啥高深的,它只是一个将状态和动作作为参数并返回应用的下一个状态的函数。“reducer” 函数总是返回应用的全部或部分状态。

只用一个“reducer”函数来管理一整个应用庞大的状态不利于后期维护且不易读。推荐按功能划分或状态属性多写几个“reducer”函数把整个应用的状态拆分管理。Redux 官方文档给出的例子就是这样做的,通过调用对应的维护状态的这两个 reducer 来编写另一个 reducer 来管理应用的完整状态:

function todoApp(state = {}, action) {
  return {
    todos: todos(state.todos, action),
    visibilityFilter: visibilityFilter(state.visibilityFilter, action)
  }
}

纯函数和reducer

纯函数是指那些仅通过参数决定返回的数据且执行后不修改任何外部数据(无副作用)的函数。纯函数应该总是有返回值,不然纯函数没意义。如果参数是非原始值的数组、对象,也不能操作。

Redux 官方文档中提到的 reducer 就是纯函数。更确切的说,开发者应该按照纯函数的标准编写 reducer 函数中的代码,不得在 reducer 函数中操作对象类型的 state 和 action 参数。Redux 文档中最常提及一个单词——“mutate”,并且总是给这个单词添加否定前缀 “… do not mutate the …”。换句话说就是希望开发者多写纯函数,从 reducer 函数直接返回全新的状态树对象,一定不要直接在旧的状态树对象(state)上“突变”数据,避免产生不在预期中的副作用。

JavaScript 语言核心的内置类,如构造函数 Array。它的有些实例方法看起来像纯函数。比如高阶函数 forEach、map 和 slice。而 splice 看起来就不像纯函数,因为它直接操作原数组,而不是返回新数组。但按纯函数的严格定义来说,这3个实例方法都不是纯函数。纯函数要求所有函数内必要的“素材”都是通过参数传递进来,而 forEach、map 和 slice 这些像是纯函数的实例方法在函数内部使用 this 访问原数组,而不是通过传入参数访问的原数组,除非 this 也能算作参数的话。

调用作为 Function 的实例——数组实例的方法——的 call、apply 和 bind 时,第一个显式传入的参数就是宿主对象,这才像纯函数会干的事。对于像 slice、forEach 和 map 这种隐式使用 this 的方式(没有将原数组作为参数传入但却在函数内用到了指向原数组的 this 的行为)我觉得不是纯函数会干的事。

叫什么不重要,是什么才重要。就像 Redux 的作者界定了什么是 reducer 一样。是不是纯函数当然由提出什么是纯函数的人来界定,界定 forEach 等 JavaScript 内置构造函数的实例方法中哪些是纯函数那些又不是这种“名可名非”的事,交给 JavaScript 规范的制定者们界定吧。

第三方JavaScript库

结语

也许你在使用 Redux 等类似库前,就已经在自己的代码中“注入”自成体系的状态管理的思维,那还有必要学习和在项目中使用 Redux 吗?不止是 Redux,对于所有让前端的世界既变得纷繁复杂了又变得颇具生机活力的框架、库、CLI以及各种脚手架来说,用或不用,用哪个,怎么用,都由你决定。如果你是项目中的一员,协作开发往往需要一套规范来约束每一位开发者的代码风格。在这种情况下,学习和使用业界已经公认的有可取价值的“标准”,无论是对提升协作开发效率还是保证项目自身的健壮与可维护性都是好处多于坏处的。