Flux 与 Redux

Flux 与 Redux

最近在学习 Redux 的相关知识,然后在学习的过程中又发现了一个与它关系十分密切的 Flux,而且除了这两个之外,还有一个 React-Redux,为了弄清它们三者之间的差异,所以就打算抽点时间来整理整理 ReduxFluxReact-Redux 这三者的关系与区别,我们就先从一切的起源 Flux 开始看起

什么是 Flux

FluxFacebook 用于构建客户端 Web 应用程序的基本架构,我们可以将 Flux 看做一种应用程序中的数据流的设计模式,而 Redux 正是基于 Flux 的核心思想实现的一套解决方案,Flux 应用中的数据以『单一方向流动』的,它有以下几个特点

  • 视图产生动作消息,将动作传递给调度器
  • 调度器将动作消息发送给每一个数据中心
  • 数据中心再将数据传递给视图

也就是下图当中所示的流程

我们可以将上图简化为以下流程

1
View(视图层) ==> Action(请求层) ==> Dispatcher(传输层) ==> Store(处理层) ==> 最后再次回到 View

比如用户在视图上(view)点击了一个按钮,即发送了一个 action,然后 action 发送到 dispatcher 中(调度器),dispatcher 来分配这个 action(比如要指派给谁去做任务)给 store(在一个 Flux 结构中,store 可以有多个,注意和 React-Redux 区分),在 store 中的作用就是存储并修改数据,然后传递给 view 进行渲染(渲染到虚拟 DOM 当中),单一方向数据流还具有以下特点

  • 集中化管理数据,常规应用可能会在视图层的任何地方或回调进行数据状态的修改与存储,而在 Flux 架构中,所有数据都只放在 store 中进行储存与管理
  • 可预测性,在双向绑定或响应式编程中,当一个对象改变时,可能会导致另一个对象发生改变,这样会触发多次级联更新,对于 Flux 架构来讲,一次 action 触发,只能引起一次数据流循环,这使得数据更加可预测
  • 方便追踪变化,所有引起数据变化的原因都可由 action 进行描述,而 action 只是一个纯对象,因此十分易于序列化或查看

Flux 的工作流

当我们在使用 MVC 或者 MVVM 架构设计模式的时候,有一个缺点,就是当项目越来越大,逻辑越来越复杂的时候,数据间的流动就会显得十分混乱,而 Flux 就是致力于解决数据有序传输问题的架构设计模式,其中最大的哲学就是『数据是单向流动的』

下面我们就来了解一下 Flux 当中的工作流,可以如下图所示

Flux 中我们可以看到会有以下几个角色的出现

  • dispatcher,调度器,接收到 action 并将它们发送给 store
  • action,动作消息,包含动作类型与动作描述
  • store,数据中心,持有应用程序的数据,并会响应 action 消息
  • view,应用视图,可展示 store 数据,并实时响应 store 的更新

下面我们就来分别看看它们各自的作用

Dispatcher

  • dispatcher 接收 action,并且要把这些 action 分派给已经注册到 dispatcherstore
  • 所有的 store 都将接收所有的 action
  • 在每个 App 中,应该确保只有一个 dispatcher 的实例

Store

  • store 是在 App 中持有数据的东西,stores 将要在 Appdispatcher 身上注册,以确保它们可以接收 actions
  • 存在 store 中的数据只能够因为响应 action 才能有所改变
  • store 中不能够有公共的 setter 函数,仅能够有 getter 函数
  • stores 决定了它们愿意响应哪些 actions
  • 无论什么时候,store 中的数据改变了,就会触发一个 change 事件
  • 在一个 App 中可能有很多 store

Action

  • action 定义了我们 App 中内部的 API
  • 它们捕获所有可能改变 App 的任何途径、方法
  • 它们是简单的 JSON 对象,并且要有 type 属性,和其他一些数据属性,也就是下面这样
1
2
3
4
{
type: 'delete-todo',
todoId: '123'
}
  • action 应该具有一个语义化的命名,比如上面我们定义的这个删除操作的 action,我们一眼就可以看出需要执行的是删除操作,它对应的 id123
  • 所有的 store 都将接收同一个 action,并且通过这同一个 actionstore 会知道它们要清除和更新哪些数据

Views

  • store 中来的数据将被展示在 view
  • view 层可以使用任何框架
  • 当一个视图想要使用从某一个 store 中来的数据,它必须订阅 subscribe(订阅)一下该 storechange 事件
  • store 发射(emit)了 change 事件,此时 view 就能够得到新的数据并且重新渲染
  • 如果一个组件要使用 store,但是没有订阅这个 store,就会出现问题(bug
  • action 最常见的产生原因实在 App 的某一个部分因用户的交互行为,而被此 view dispatch(派发) 出来了

什么是 Redux

在知道了什么是 Flux 以后,我们再来看看 Redux 的相关内容,简单来说,Redux 就是 Flux 思想在 React 当中的实现,所谓的 Redux 可以简单的理解为一个可以预测状态的 JavaScriptApp 容器,而 App 中的全部 state 都被存储在一个单独的 store 中,形式是 object-treeJSON),唯一更改 state 的途径就是 emit 一个 action,这个 action 描述了发生了什么

为了指定这些 actions 如何改变 state tree,必须书写简单的、纯净的 reducers,所谓的纯净的 reducers 就是类似下面这样伪代码,它不继承任何东西,并且无论何时返回的值都是固定的

1
2
3
4
function reducers(state, action) {
// 返回一个新的 state
return newState
}

上面就是一个 reducer,是一个纯函数,接收 stateaction 两个参数,返回新的 state 表达式,如果有使用过 Flux,在这里我们可以发现有一个重要的区别

即在 Redux 当中没有 dispatcher 的概念(store 自己负责 dispatch 某个 action 到自己身上),也不允许有多个 store,所以一般来说,Redux 比较适合用于有强的全局数据概念的 Web 应用(比如商城,购物车等)

Redux 中只有一个唯一的 store,使用唯一的 reducing function,随着项目增长的时候也不要去增加 store,而是应该切割当前的 store 为一个个小的 store,即 store 应该只有一个,类似于 React 当中只允许使用一个根节点,但是根节点是由众多的节点组成,我们下面将会分别进行讨论

为什么要用 Redux

这个需要视当前的使用场景来决定的,当然除了 Redux 还有 FluxRefluxMobx 等状态管理库可供选择,下面就是一个实际场景,比如在控制台上记录用户的每个动作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 后端,比如使用 Express 中实现一个简单的 Logger 
var loggerMiddleware = function (req, res, next) {
console.log('[Logger]', req.method, req.originalUrl)
next()
}
...
app.use(loggerMiddleware)


// 前端,jQuery
$('#loginBtn').on('click', function (e) {
console.log('[Logger] 用户登录')
...
})

$('#logoutBtn').on('click', function () {
console.log('[Logger] 用户退出登录')
...
})

然后现在又需要在上述需求的基础上,记录用户的操作时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 后端,只需要稍微修改一下原来的中间件即可
var loggerMiddleware = function (req, res, next) {
console.log('[Logger]', new Date(), req.method, req.originalUrl)
next()
}
...
app.use(loggerMiddleware)


// 前端,需要一个一个去改
$('#loginBtn').on('click', function (e) {
console.log('[Logger] 用户登录', new Date())
...
})

$('#logoutBtn').on('click', function () {
console.log('[Logger] 用户退出登录', new Date())
...
})

又比如说,在正式上线的时候,把控制台中有关 Logger 的输出全部去掉,亦或是自动收集 bug,很明显的可以看出前后端对于这类需求的处理竟然大相径庭,原因在于,后端具有统一的入口与统一的状态管理(数据库),因此可以引入中间件机制来统一实现某些功能,而前端也可以使用 MVC 的开发思维,将应用中所有的动作与状态都统一管理,让一切有据可循

Store

我们首先要区分 storestate 之间的区别,state 是应用的状态,一般本质上是一个普通对象,例如我们有一个 Web App,包含计数器和待办事项两大功能,那么我们可以为该应用设计出对应的存储数据结构(应用初始状态)

1
2
3
4
5
/』应用初始 state『/
{
counter: 0,
todos: []
}

store 则是应用状态 state 的管理者,包含下列四个函数

  • getState(),获取整个 state
  • dispatch(action),触发 state 改变的【唯一途径】
  • subscribe(listener),可以理解成是 DOM 中的 addEventListener
  • replaceReducer(nextReducer),一般在 Webpack Code-Splitting 按需加载的时候用(使用较少)

二者的关系是 state = store.getState()

  • Redux,规定,一个应用只应有一个单一的 store,其管理着唯一的应用状态 state
  • Redux,还规定,不能直接修改应用的状态 state,也就是说,下面的行为是不允许的
1
2
3
4
var state = store.getState()

// 禁止在业务逻辑中直接修改 state
state.counter = state.counter + 1

若要改变 state,必须 dispatch 一个 action,这是修改应用状态的不二法门

  • 针对 action,暂时只需要记住,action 就是一个包含 type 属性的普通对象,例如 { type: 'INCREMENT' }
  • store,我们需要调用 Redux 提供的的 createstore() 方法,如下
1
2
3
4
5
6
import { createStore } from 'redux'

// ...

// 只需记住 store 是靠传入 reducer 来生成的
const store = createStore(reducer, initialState)
  • 针对 reducer,暂时只需要记住,reducer 是一个 函数,负责更新并返回一个新的 state 即可
  • 而第二个参数 initialState 主要用于前后端同构的数据同步(详情请关注 React 服务端渲染)(可暂时不用管)

Action

action(动作)实质上是包含 type 属性的普通对象,这个 type 是我们实现用户行为追踪的关键,例如增加一个待办事项的 action 可能是像下面一样

1
2
3
4
5
6
7
8
{
type: 'ADD_TODO',
payload: {
id: 1,
content: '待办事项1',
completed: false
}
}

action 的形式是多种多样的,唯一的约束仅仅就是包含一个 type 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 下面这些 action 都是合法的,但就是不够规范
{
type: 'ADD_TODO',
id: 1,
content: '待办事项1',
completed: false
}

{
type: 'ADD_TODO',
abc: {
id: 1,
content: '待办事项1',
completed: false
}
}

具体规范可见 flux-standard-action

Action Creator

Action Creatoraction 的创造者,本质上就是一个函数,返回值是一个 action(对象)(可以是同步的,也可以是异步的),例如下面就是一个新增一个待办事项的 Action Creator

1
2
3
4
5
6
7
8
9
10
11
var id = 1
function addTodo(content) {
return {
type: 'ADD_TODO',
payload: {
id: id++,
content: content, // 待办事项内容
completed: false // 是否完成的标识
}
}
}

简单来说,Action Creator 就是用于绑定到用户的操作(比如点击按钮等),其返回值 action 用于之后的 dispatch(action)

Reducer

需要注意的是,reducer 必须是同步的『纯函数』,简单来说分为以下三步

  • 用户每次 dispatch(action) 后,都会触发 reducer 的执行
  • reducer 的实质是一个函数,根据 action.type 来更新 state 并返回 nextState
  • 最后会用 reducer 的返回值 nextState 完全替换掉原来的 state

几个需要注意的地方

  • 所谓的更新并不是指 reducer 可以直接对 state 进行修改
  • Redux 规定,须先复制一份 state,在副本 nextState 上进行修改操作
  • 例如,可以使用 lodashcloneDeep,也可以使用 Object.assign/map/filter ... 等返回副本的函数

例如下面这个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var initState = {
counter: 0,
todos: []
}

function reducer(state, action) {
// 应用的初始状态是在第一次执行 reducer 时设置的
if (!state) state = initState

switch (action.type) {
case 'ADD_TODO':
var nextState = _.cloneDeep(state)
nextState.todos.push(action.payload)
return nextState

default:
// 由于 nextState 会把原 state 整个替换掉
// 若无修改,必须返回原 state(否则就是 undefined)
return state
}
}

简单的理解就是,reducer 返回什么,state 就被替换成什么

Redux 的整体流程

  • storeReduxcreatestore(reducer) 生成
  • state 通过 store.getState() 获取,本质上一般是一个存储着整个应用状态的对象
  • action 本质上是一个包含 type 属性的普通对象,由 action Creator(函数) 产生
  • 改变 state 必须 dispatch 一个 action
  • reducer 本质上是根据 action.type 来更新 state 并返回 nextState 的函数
  • reducer 必须返回值,否则 nextState 即为 undefined
  • 实际上,state 就是所有 reducer 返回值的汇总

大致流程如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Action Creator 

==>

Action

==>

store.dispatch(action)

==>

reducer(state, action)

==>

state(`原`)

==>

nextState(`新`)

Redux 官方示例剖析

下面我们就通过一个例子来深入的了解一下 Redux 的工作流程,示例参考的是官方提供的 counter-vanilla(见 redux/examples/counter-vanilla/index.html

1
2
3
4
5
6
7
8
9
10
11
// reducer
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}

action 所对应的字段一般都是约定成俗的使用大写字母来进行表示,它描述了一个 action 如何使当前 state 改变为下一个 statestate 的形式取决于你,它可以是一个基本类型值,可以是一个数组,也可以是一个对象等等,唯一需要注意的就是,永远不要去更改当前的 state,而是应该返回一个新的 state 对象

1
var store = redux.createstore(counter)

首先创建一个 Reduxstore,用它来持有 AppstorestoreAPI 及其简单,就三个,subscribedispatchgetState

  • subscribe,让 store 去注册一个视图
  • dispatch,分发一个命令
  • getState,返回一个状态
1
store.subscribe(render)

使用 storesubscribe() 方法,将 store 订阅了视图,render 是一个函数,其实简单来说就是,每次当 state 变化的时候就会执行该函数,通常情况下是与 React 来配合使用,调整上面的示例,添加一个每次点击增加 2 的按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// reducer
function counter(state = 0, action) {
switch (action.type) {
case 'ADDTWO':
return state + 2
default:
return state
}
}

// 创建一个 store,有三个方法,subscribe,dispatch 和 getState
var store = Redux.createStore(counter)

// 得到 span 元素
var valueEl = document.getElementById('value')

// 渲染函数并且调用
function render() {
valueEl.innerHTML = store.getState().toString()
}

render()
store.subscribe(render)

// 加 2
document.getElementById('addTwo').onclick = function () {
store.dispatch({ type: 'ADDTWO' })
}

调整示例,添加一个输入框,然后点击的时候加上输入框内的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function counter(state = 0, action) {
switch (action.type) {
case 'ADDNUMBER':
return state + action.number
default:
return state
}
}

// ...

// 增加输入框内的值
document.getElementById('addNumber').onclick = function () {
var number = Number(document.getElementById('textNumber').value)
store.dispatch({ type: 'ADDNUMBER', number: number })
}

综合以上示例,点击按钮的时候,使 storedisptch 一个命令,这时需要注意了,数据存储在 store 中,然后 store 给自己 dispatch 了一条命令,然后自己再去识别给自己发送的命令(case),然后改变存储在自己 store 中的 statereturn

之所以这样设计,就是因为在 reducer 中可以看见整个程序的 state 会发生怎样的变化,虽然不知道什么时候会变化,但是知道其可以做出什么样的变化,知道其不能够做出什么样的变化,这就是 Redux 的哲学,让 state 可以被预期,这也就是下面的 reducer 存在的意义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// reducer 清单
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'ADDNUMBER':
return state + action.number
case 'DECREMENT':
return state - 1

case 'aaa':
return state + xxx
case 'bbb':
return state - xxx
case 'ccc':
return state * xxx
case 'ddd':
return state / xxx

// ...

default:
return state
}
}

综上

  • 我们不是直接去修改 state,而是指定了一个简单的 JSON 对象(类似指令,type)去描述我们想要什么事情发生,这个 JSON 称之为 action
  • 然后声明一个特定的 reducer 的函数去指定每一个 action 要如何改变整个 Appstore

注意这个『整个』,看下面的示例,我们先将 state 默认值设置为一个对象(不再是简单的数字)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// reducer
function counter(state, action) {

if (state == undefined) {
state = { 'm': 5, 'n': 10 }
}

switch (action.type) {
case 'INCREMENT':
return state.m + 1
case 'ADDNUMBER':
return state.m + action.number
default:
return state
}
}

直接使用类似上面的 return state.m + 1 是没有效果的,这时需要返回的是整个 state 的值(建议使用 ES6 中的 ... 运算符)即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// reducer
function counter(state, action) {

if (state == undefined) {
state = { 'm': 5, 'n': 10 }
}

// switch (action.type) {
// case 'INCREMENT':
// return { 'm': state.m + 1 }
// case 'DECREMENT':
// return { 'm': state.m - 1 }
// case 'ADDNUMBER':
// return { 'm': state.m + action.number }
// default:
// return state
// }

switch (action.type) {
case 'INCREMENT':
return { ...state, 'm': state.m + 1 }
case 'DECREMENT':
return { ...state, 'm': state.m - 1 }
case 'ADDNUMBER':
return { ...state, 'm': state.m + action.number }
default:
return state
}
}

// ...

// 渲染函数并且调用
function render() {
valueEl.innerHTML = store.getState().m
}
# React

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×