Redux 简记

引言

根据官网介绍,Redux 是一个 JavaScript 状态管理容器,提供了可预测的状态管理能力。可以构建一致的应用,运行于多种环境下(客户端、服务器和原生应用等),且易于测试(甚至都不要在写界面之前进行测试)。

概念

核心思想

Redux 的核心就是管理状态,所有的状态是在 store 对象树中维护。使用 action 来描述 state 变化,而为了能够将 action 和 state 串联在一块,就有了 reducer 函数,它负责根据不同的 action dispatch 到不同的分支,然后返回新的状态。需要注意的是,reducer 是纯函数,不应该 inplace 那种方式修改 state,而是生成新的 nextState 并返回。
通常一个应用中可能有多个小的 reducers ,我们可以使用 combineReducers 将它们组合在一起。

三大原则

  • 单一数据源:整个应用的 state 是存储在一个 obj tree 中的,并且该 obj tree 只存在于唯一一个 store 中
  • state 是只读的:唯有触发 action 才会改变 state,使用 action 来描述发生了什么
  • 使用纯函数执行修改:需要编写 reducer 实现状态改变

三驾马车

Action

  • Action 是把数据从应用传递到 store 的有效载荷,是 store 数据的唯一来源
  • 一般通过 store.dispatch() 分发 action,传递到 store
  • 当应用规模变大时,建议将 Action 使用单独的模块存放
  • 尽量减少在 action 中传递的数据
  • Action 创建函数:返回 action 的函数,使用 bindActionCreators() 可以自动将多个 action 创建函数绑定到 dispatch() 方法上
  • Action 只是描述了有事情发生了这样的事实,但不会描述应用如何更新 state

Reducer

  • 在 reducer 中响应 action,并产生新的 state,达到改变 state 的目的
  • 为什么叫 reducer?参考 Array.prototype.reduce(reducer, ?initialValue) 里面的回调函数,它们比较类似,都必须是纯函数。reducer 函数不欢迎如下操作:

    • 修改传入参数
    • 执行含有副作用的操作,如 API 请求和路由跳转
    • 调用非纯函数,如 Date.now()Math.random()
  • 可以把所有顶级 reducer 放在独立的文件中,通过 export 暴露每个 reducer 函数:

    1
    2
    3
    4
    import { combineReducers } from 'redux'
    import * as reducers from './reducers'

    const todoApp = combineReducers(recuders)

Store

  • Store 的职责如下:

    • 维持应用的 state
    • 提供 getState() 读取状态
    • 提供 dispatch(action) 方法更新状态
      -提供 subscribe(listener) 方法注册监视器,其返回的函数用于注销监视器
  • Redux 应用的 store 只会有一个,一般会根据业务逻辑来拆分子状态分组,可以通过 reducers 组合完成

  • createStore(reducer, ?initialState) 创建应用 store

示例

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import React, { useRef } from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
console.log('Add todo: ' + action)
return [
...state,
todo(undefined, action)
]
case 'REMOVE_TODO':
console.log('Remove todo: ' + action)
return state.map((item) => todo(item, action))
case 'TOGGLE_TODO':
console.log('Toggle todo: ' + action)
return state.map((item) => todo(item, action))
default:
return state
}
}
function todo(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
id: action.id,
text: action.text,
completed: action.completed,
isDeleted: false,
}
case 'REMOVE_TODO':
if (state.id !== action.id) {
return state
}
return {
...state,
isDeleted: true
}
case 'TOGGLE_TODO':
if (state.id !== action.id) {
return state
}
return {
...state,
completed: !state.completed
}
default:
return state
}
}
const todoApp = combineReducers({
todos
})
const store = createStore(todoApp)
let nextTodoId = 0
function TodoItem({ todo }) {
return (
<li>
<label
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => store.dispatch({ type: 'TOGGLE_TODO', id: todo.id })}
>
{todo.text}
</label>
<button
style={{ marginLeft: 10 }}
onClick={() => { store.dispatch({ type: 'REMOVE_TODO', id: todo.id }) }}
>移除</button>
</li>
)
}
function TodoApp({ todos = [] }) {
const todoItems = todos
.filter((todo) => !todo.isDeleted)
.map((todo) => <TodoItem key={todo.id} todo={todo} />)
const input = useRef()
const handleClick = () => {
const text = input.current.value
if (!text) {
return
}
store.dispatch({ type: 'ADD_TODO', id: nextTodoId++, text: input.current.value })
}
return (
<div>
<input ref={input} />
<button onClick={handleClick} style={{ marginLeft: 10 }}>添加</button>
<ul>
{todoItems}
</ul>
</div>
)
}
const render = () => {
const state = store.getState()
ReactDOM.render(
<TodoApp
todos={state.todos}
/>,
document.getElementById('root')
)
}
store.subscribe(() => {
console.log(store.getState())
render()
})
render()

实践

按照 Redux 提供的几个官方示例,可以看出编写一个简单的 Todo 应用其实要考虑的还是挺多的,尤其是需要在分散的模块中分别定义 actions, reducers, containers, dumb components 等,然后搭配 react-redux 来实现整体功能。
以下是一个经典的 React Redux 应用的示例的文件结构:

1
2
3
4
5
6
7
8
src/
actions/
components/
consts/
containers/
reducers/
sotore.js
index.js

看起来,分层结构很明晰。但这里有几个很大的问题:

  1. components 和 containers 可能会出现相互引用的问题;
  2. 对于某些情形,严格区分 smart components 和 dumb components 比较死板;
  3. 编写起来很复杂、啰嗦、冗余。

针对这个痛点,可以借助 rematch 框架解决。它本质上是一个基于 Redux 封装的框架,目的是提供最佳的 Redux 实践,减少大量的样板代码编写,解放生产力!

使用 Rematch 时,有个非常重要的概念叫做 model。你可以在 models 里面将状态、引起状态变化reducers 及异步 actions 和 action creators 收敛到一起,方便维护和测试。总的来说,model 定义包含如下几个部分:

  • state: 初始状态是什么
  • reducers: 如何改变状态
  • effects: 处理异步的 actions,带有副作用的操作

一个典型的 model 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const count = {
state: 0, // initial state
reducers: {
// 使用同步函数,处理状态变更
increment(state, payload) {
return state + payload
}
},
effects: (dispatch) => ({
// 使用纯函数处理状态变更(异步 actions)
async incrementAsync(payload, rootState) {
await new Promise(resolve => setTimeout(resolve, 1000))
dispatch.count.increment(payload)
}
})
}

有了 models 后,就可以创建 Redux store,并使用 dispatch 来触发 reducers 和 effects 调用(无需手动编写 action creators 啦~)。当然,在展示层,我们依然可以结合 react-redux 来使用。具体可以参考官方示例,更详细的文档可以参考 这里

参考

延伸阅读

0%