本文非完整的开发指南,具体开发细节请参考官网文档。里面涉及的一些基础概念,翻译自官方文档。
本文主要阐述了 Redux 设计的核心概念,梳理了涉及到的知识结构,方便你能快速地对 Redux 有一个完整的认知。
在此基础上,探索了 Redux 框架的一些基本原理和部分源码的解读。
最后做了一些扩展和延伸。
需要着重申明的是,Redux 本身很简单,简单并不总是意味着容易。Redux 也很强大,作为一种非常简洁优雅的状态管理方案,可以支撑起各种复杂的应用逻辑。随着 Kotlin MultiPlatfrom 的发展,Redux 架构方案也会有更多的应用场景 (纯粹个人预测)。

0. 介绍

一言以蔽之: Redux 是一种可预测的状态管理容器 ^1

安装

Redux 经常和 React 框架一起配合使用,当然 Vue 也是如此。虽然 Redux 本身非常小(只有 2kb),但是却又非常强大的社区生态。

下面简单说一下 Redux 的安装使用(可以直接跳到 总览和概念 这一部分, 并不影响阅读,你可以把下面的部分当做链接导航来使用)。

Redux Toolkit

1
2
3
4
5
# NPM
npm install @reduxjs/toolkit

# Yarn
yarn add @reduxjs/toolkit

Redux Core

1
2
3
4
5
# NPM
npm install redux

# Yarn
yarn add redux

其他补充

假设你在使用 React 框架,那么你很有可能需要下面的依赖:

1
2
npm install react-redux
npm install --save-dev redux-devtools

创建 React Redux App

npx create-react-app my-app --template redux

更多细节参见官方文档1

核心概念

更多细节参见官方文档2

学习资料

更多细节参见官方文档3

社区生态

更多细节参见官方文档4

例子

更多细节参见官方文档5

1. 总览和概念

什么是 Redux

Redux is a pattern and library for managing and updating application state, using events called “actions”. It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.

翻译:Redux 是一种编程模式,主要用来管理应用状态,通过 Action 来更新状态。它包含了贯穿整个应用生命周期的状态,称之为 Store, 还有一些规则来保障状态可以被“可预测的”更新。

为什么用 Redux

官网的章节^2是这么描述的:Redux 帮助你管理整个应用共享的“全局状态“。
Redux 让我们更好的理解状态何时何地以及为什么以及怎样被更新的,并且让我们的应用逻辑可预测。Redux 让我们映红可预测,更方便做测试,怎强了应用的鲁棒性。

什么时候用 Redux

Redux 和其他工具一样,需要我们做出一些折中。需要我们了解更多的概念,代码量也会增加,代码的编程模式也有更多的限制。在如下场景,从长期来看,短期的妥协还是值得:

  • 应用内各模块经常需要状态共享
  • 应用状态更新频繁
  • 更新状态的逻辑复杂
  • 应用属于中等或者重量级规模,而且有多人协作

Redux 并不是银弹,你需要仔细思考Redux 是否解决你目前遇到的问题,再决定是否使用它。

术语和概念

在真正开始之前,我们先解释一下 Redux 相关的概念和术语。

状态管理(State Management)

以计数器(点击按钮,计数器增加 1)为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)

// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}

// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}

例子包含三部分:

  • State,应用的数据源
  • View,基于 State 的数据驱动 UI 组件
  • Actions,用户触发的事件,更新 State

这是一个简单的 ”单项数据流“ 的应用:

  • State 描述了某一个应用的状态
  • UI 的渲染基于 State
  • 对于用户的点击等输入,Sate 会根据具体场景更新当前值
  • UI 的重绘基于新的 State



计数器的例子比较简单,但是当多个组件消费同一个 State 会让应用越来越复杂,尤其是当组件分布在应用的不同部分。当然,可以通过把 State 提升到功共同父组件来解决,但是实际场景可能不总是这样。

另一个解法,可以通过把状态从组件树中抽离到全局,这样,我们的组件树可以看做 View ,包含的各个组件就可以共享 State 了。

通过重新定义 ViewState 的概念,并且把两者区分来看,可以增强代码结构的可读性和可维护性。

这就是 Redux 背后的基础概念: 中心化存储应用共享的全局状态,以及更新状态的规则,以此让应用行为变得可预测。

不可变(Immutability)

字面意思。

Javascript 对象和数组默认都是可变的。

1
2
3
4
5
6
7
8
const obj = { a: 1, b: 2 }
// still the same object outside, but the contents have changed
obj.b = 3

const arr = ['a', 'b']
// In the same way, we can change the contents of this array
arr.push('c')
arr[1] = 'd'

例子中,我们更改了内存中相同引用的对象或者数组。

通过拷贝对象和数组,我们来实现 “不可变”。

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
const obj = {
a: {
// To safely update obj.a.c, we have to copy each piece
c: 3
},
b: 2
}

const obj2 = {
// copy obj
...obj,
// overwrite a
a: {
// copy obj.a
...obj.a,
// overwrite c
c: 42
}
}

const arr = ['a', 'b']
// Create a new copy of arr, with "c" appended to the end
const arr2 = arr.concat('c')

// or, we can make a copy of the original array:
const arr3 = arr.slice()
// and mutate the copy:
arr3.push('c')

Redux 中约定,状态更新都是“不可变” 更新。

一些术语

Actions

Action 就是一个有 type 字段的普通 JavaScript 对象,它描述了事件的一些元信息。

type 字符串描述了 Action 的名字,例如 todos/todoAdded

Action 对象可以包含其他字段来来做数据负载,根据约定我们一般叫做 payload:

1
2
3
4
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}

Action Creators

创建 Action, 避免每次实例化 Action 手写重复样板代码。

1
2
3
4
5
6
const addTodo = text => {
return {
type: 'todos/todoAdded',
payload: text
}
}

Reducers

纯函数,参数是当前的 stateaction, 输出更新后的 newState, 函数签名(state, action) => newState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/increment') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}

Store

Redux 应用的状态存储一个叫做 store 的对象。

store 是通过传递 reducerconfigureStore 来创建的,并且有一个 getState 的方法返回当前的状态。

1
2
3
4
5
6
import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}

Dispatch

store 有另一个方法 dispatch, 更新 state 的唯一方法就是通过调用 store.dispatch() 函数并且提供一个 action 参数来实现的。store 运行上述我们编写的 reducerstate 的值更新,然后我们可以通过 getState() 获得更新后的值。

1
2
3
4
store.dispatch({ type: 'counter/increment' })

console.log(store.getState())
// {value: 1}

Selectors

Selector 是析构 store 里状态的特定信息的函数。随着应用增长,Selector 可以降低解析同样数据的重复代码。

1
2
3
4
5
const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2

Middleware

Middleware 提供了从 dispatch 一个 action 到被reducer处理之前一种扩展机制,可以用来打印日志,打印错误堆栈,发起异步调用等。

Redux-Thunk

是一种 Middleware,增强 Redux 使其可以 dispatch 一个异步处理函数而非一个简单对象。

代码示例

以官网的 example/async为例。

src 目录结构

1
2
3
4
5
6
7
8
9
10
11
12
src
├── actions
│   └── index.js
├── components
│   ├── Picker.js
│   └── Posts.js
├── containers
│   └── App.js
├── index.js
└── reducers
└── index.js
|── index.js

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import createLogger from 'redux-logger'
import reducer from './reducers'
import App from './containers/App'

const middleware = [thunk, createLogger()]

const store = createStore(
reducer,
applyMiddleware(...middleware)
)

render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

这里主要是做了创建 Store 以及一些和 React 框架绑定的事情(Provider, 可以参考 react-redux )。

这里还涉及到了 middleware 和 thunk,是非常核心的概念,后面会详细解析,但目前不影响理解。

actions/index.js

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
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_REDDIT = 'SELECT_REDDIT'
export const INVALIDATE_REDDIT = 'INVALIDATE_REDDIT'

export function selectReddit(reddit) {
return {
type: SELECT_REDDIT,
reddit
}
}

export function invalidateReddit(reddit) {
return {
type: INVALIDATE_REDDIT,
reddit
}
}

function requestPosts(reddit) {
return {
type: REQUEST_POSTS,
reddit
}
}

function receivePosts(reddit, json) {
return {
type: RECEIVE_POSTS,
reddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}

function fetchPosts(reddit) {
return dispatch => {
dispatch(requestPosts(reddit))
return fetch(`https://www.reddit.com/r/${reddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(reddit, json)))
}
}

function shouldFetchPosts(state, reddit) {
const posts = state.postsByReddit[reddit]
if (!posts) {
return true
}
if (posts.isFetching) {
return false
}
return posts.didInvalidate
}

export function fetchPostsIfNeeded(reddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), reddit)) {
return dispatch(fetchPosts(reddit))
}
}
}

这里只要定义了 action, 以及发送请求的部分。

reducers/index.js

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
import { combineReducers } from 'redux'
import {
SELECT_REDDIT, INVALIDATE_REDDIT,
REQUEST_POSTS, RECEIVE_POSTS
} from '../actions'

function selectedReddit(state = 'reactjs', action) {
switch (action.type) {
case SELECT_REDDIT:
return action.reddit
default:
return state
}
}

function posts(state = {
isFetching: false,
didInvalidate: false,
items: []
}, action) {
switch (action.type) {
case INVALIDATE_REDDIT:
return {
...state,
didInvalidate: true
}
case REQUEST_POSTS:
return {
...state,
isFetching: true,
didInvalidate: false
}
case RECEIVE_POSTS:
return {
...state,
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
}
default:
return state
}
}

function postsByReddit(state = { }, action) {
switch (action.type) {
case INVALIDATE_REDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return {
...state,
[action.reddit]: posts(state[action.reddit], action)
}
default:
return state
}
}

const rootReducer = combineReducers({
postsByReddit,
selectedReddit
})

export default rootReducer

主要是 reducer 纯函数部分。

其他

containers/App.jscomponents/*.js 主要是一些 UI 逻辑,本身并不复杂,这里可自行看官网代码。

2. Redux 原理解读

Redux 数据流向

首先来看一张没有 Middleware 时候的数据流向图,这对后面理解 Middleware 非常有帮助。




图 2.1 Data Flow 示意图

Middleware

首先来完整的介绍一下 Middleware。

Middleware 提供了从 dispatch 一个 action 到被reducer处理之前一种扩展机制,可以用来打印日志,打印错误堆栈,发起异步调用等。

Middleware 主要用来处理异步逻辑(Redux 本身其实并不关心异步逻辑,所有异步操作都需要放到 Store 之外执行)。

  • 当 action 被 dispatch 的时候,处理额外的逻辑,比如打日志记录 action 和 state
  • 暂定,修改,延时,替换, 停止 action
  • 添加额外代码,可以访问 dispatch 和 getState
  • 改造 dispatch,使其可以接受除了 action 对象之外的类型,比如函数或者 promise,其原理是拦截这些新的类型,然后转换成真正的 action 对象

Redux 有很多 middleware,最常见的就是 redux-thunk 了,允许你编写含有异步逻辑的函数。

前面章节我们分析了 Redux 的数据流是怎样运转的,当我们引入异步概念后,在 dispatch action 之前我们可以在 middleware 中编写类似发送网络请求的逻辑。更新后的数据流如图 2.2 所示。





图 2.2 Middleware 示意图

使用 Middleware

1
2
3
4
5
6
7
8
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();

const store = createStore(
reducer,
applyMiddleware(logger)
);

Redux 提供了 applyMiddleware 方法,该方法支持多个 Middleware 参数传入,例如 applyMiddleware(thunk, promise, logger) , 并且对于参数的顺序敏感,后面会提到为什么,这里只要记住 logger 要放到最后一个才能正常工作。

applyMiddleware 调用结束后,会返回 store 的增强版本,然后传递给 createStore 就完成了 Middleware 的应用。

下面将详解这中间发生的过程。

剖析 Middleware

首先我们来看一下 applyMiddleware.js 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []

var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

该方法主要作用是用来增强 Store 的 dispatch 方法,也叫做 store enhancer。该方法的入参是 middlware 的列表,返回值就是增强后的 Store 了,而且紧紧替换了 dispatch 方法。

虽然代码不多,但是初看确实不好理解。在真正分析源码之前我们首先要了解函数式编程里的两个概念:柯里化和函数组合。

柯里化(Currying)

柯里化接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

说人话,假设有函数 f(x,y) = x + y,未经柯里化之前 f(2,3) = 2 + 3 = 5
对于柯里化我们可以这样处理:

1
2
3
首先定义 f-2 = (2, y) = 2 + y
因此
f(2,3) = f-2(y)= f-2(3) = 2 + 3 = 5

函数组合(Composing Functions)

函数组合 是通过把一个个简单函数作为参数传递给下一个函数来组合成复杂函数的机制。

假设有:

1
2
f(x) = x^2 + 3x + 1
g(x) = 2x

那么

1
(f * g)(x) = f(g(x)) = f(2x) = 4x^2 + 6x + 1

在 JavaScript 实现函数组合效果是通过 Array.prototype.reduceRight 来实现的。

reduceRight() 方法接受一个函数作为累加器(accumulator)和数组的每个值(从右到左)将其减少为单个值。

举例:

1
2
3
4
5
const array1 = [[0, 1], [2, 3], [4, 5]].reduceRight(
(accumulator, currentValue) => accumulator.concat(currentValue)
);
console.log(array1);
// expected output: Array [4, 5, 2, 3, 0, 1]

我们再来看一下 compose.js 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
}

composed 就是上一次累计的结果,初始值是 last(...args)。执行顺序从右到左,因此第一次 reduce的累加器 (composed, f) => f(composed) 展开代码:
(last(...args), f) => f(last(..args)), 以此类推。

来一个小 demo 验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var hello = function(x) { 
return `Hello, ${ x }`
};
var world = function(x) {
return `${x}!`
};
var compose = function(f, g) {
return function(x) {
return f(g(x));
}
}
var helloworld = compose(hello, world);
helloworld("world")

最终输出结果 "Hello, world!"

再看 applyMiddleware.js 代码

有了前面的铺垫后,就万事俱备了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = []

var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

首先 applyMiddleware 返回的是一个匿名函数,在使用 Middleware 一节我们提到 applyMiddleware 是作为 createStore 参数传递下去的。

1
2
3
4
const store = createStore(
reducer,
applyMiddleware([ myMiddleware1, myMiddleware2, myMiddleware3])
);

我们来看一下 createStore.jscreateStore 的核心代码:

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
export default function createStore(reducer, preloadedState, enhancer) {
//部分防护代码删掉
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
//部分防护代码删掉...
//其他关键部分
function getState() {...}
function subscribe(listener) {...}
function dispatch(action) {...}
function replaceReducer(nextReducer) {...}
function observable() {}
}
...
dispatch({ type: ActionTypes.INIT })
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}

applyMiddleware 返回的匿名函数就是 enhancer 这个参数,因此代码会执行到:

return enhancer(createStore)(reducer, preloadedState)

这里 createStore 把自身引用传递到了 applyMiddleware,即匿名函数的第一个参数 createStore,applyMiddleware 代码此时会执行到:

1
var store = createStore(reducer, preloadedState, enhancer)

知识点来了哦… 此时调用栈代码又回到了 createStore 方法,但此刻 enhancer 参数为空,因此此时 createStore 不会提前 return, 而是老老实实初始化,最终返回一个 Store 对象:

1
2
3
4
5
6
7
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}

记住此刻,store 是未经过增强的,执行完毕后,再回到 applyMiddleware 函数。

1
2
3
4
5
6
7
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

上述几行代码,主要是创建了 middleware 的标准入参(也就是构造 middleware 时候的标准 API),然后遍历 applyMiddleware 传递进来的 middleware 列表,把 {getState, dispatch} 传递给各个 middleware, 最终返回内存中的 middleware 实例。

原本 middleware 的函数声明只这样子的:

1
2
3
4
5
6
({ dispatch, getState }) => (next) => (action) => {
//执行下一个 Middleware 之前以及 dispatch action 之前执行的操作
let retValue = next(action);
//执行完下一个 Middleware 以及 dispatch action 之后执行的操作
return retValue;
};

经过上述 chain = middlewares.map(middleware => middleware(middlewareAPI)) 操作后(柯里化的参数应用,消灭掉第一个参数 { dispatch, getState }),
内存中的 middleware 转化成如下结构:

1
2
3
4
5
6
(next) => (action) => {
//执行下一个 Middleware 之前以及 dispatch action 之前执行的操作
let retValue = next(action);
//执行完下一个 Middleware 以及 dispatch action 之后执行的操作
return retValue;
};

假设我们有三个 Middleware myMiddleware1, myMiddleware2, myMiddleware3 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const myMiddleware1 = ({ dispatch, getState }) => (next) => (action) => {
console.log('myMiddleware 1 start');
let retValue = next(action);
console.log('myMiddleware 2 end');
return retValue;
};

const myMiddleware2 = ({ dispatch, getState }) => (next) => (action) => {
console.log('myMiddleware 2 start');
let retValue = next(action);
console.log('myMiddleware 2 end');
return retValue;
};

const myMiddleware3 = ({ dispatch, getState }) => (next) => (action) => {
console.log('myMiddleware 3 start');
let retValue = next(action);
console.log('myMiddleware 3 end');
return retValue;
};

首先经过第一个参数部分求值后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const myMiddleware1 = (next) => (action) => {
console.log('myMiddleware 1 start');
let retValue = next(action);
console.log('myMiddleware 2 end');
return retValue;
};

const myMiddleware2 = (next) => (action) => {
console.log('myMiddleware 2 start');
let retValue = next(action);
console.log('myMiddleware 2 end');
return retValue;
};

const myMiddleware3 = (next) => (action) => {
console.log('myMiddleware 3 start');
let retValue = next(action);
console.log('myMiddleware 3 end');
return retValue;
};

接下来就是函数组合,也是 Redux 中精华的部分了:

1
dispatch = compose(...chain)(store.dispatch)

转换后的 dispatch 变成如下结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function dispatch (action) {
var next = (next) => {
var next = (next) => {
return (action) => {
console.log('middleware 3 start');
(store.dispatch)(action)
console.log('middleware 3 end');
};
};
return (action) => {
console.log('middleware 2 start');
next(action)
console.log('middleware 2 end');
};
};
return (action) => {
console.log('middleware 1 start');
next(action)
console.log('middleware 2 end');
};
};

根据展开后的代码可以得出结果,当我们 dispatch 一个 action 的时候,
首先会按照从做到右的顺序执行 Middleware, 直到最后一个 Middleware,而最后一个 Middleware 的 next 参数已经在 compose 的过程中被赋值为 (store.dispatch), 注意此时的 dispatch 是未经过修饰的。因此当 middleware3 执行完毕后,此时 action 才会真正被送到 reducer 进行状态变更。

可以结合图 2.2 来理解。





图 2.2 Middleware 运行机制示意

因此控制台输出结果为:

1
2
3
4
5
6
7
8
//state
middleware 1 start
middleware 2 start
middleware 3 start
//nextState
middleware 3 end
middleware 2 end
middleware 1 end

最后 applyMiddleware 函数返回加强后的整个应用的 store 实例:

1
2
3
4
return {
...store,
dispatch
}

Middleware 运行机制

剥洋葱图。

为什么 applyMiddleware 对入参顺序敏感

以 redux-logger 为例,必须放到最后一个,否则它只能记录 thunk 或者 promise,而非最终的 action,具体参见

原理就是如果不放置到最后一个的话,当前的 middlware 并不发送 action,而只是调用 next 传递给下一个 middleware,结合图 2.2,只有 middleware 3 才能记录真正经过 middleware 1 & 2 处理后发送的 action。

Thunk

Thunk中文翻译有转换程序的意思。下面我们看一下 redux-thunk 到底转换了什么。

剖析 Redux-Thunk [^8]

嗯,redux-thunk 代码就这么多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

核心逻辑 typeof action === 'function' 判断如果是 function,则调用 function。这使得 action 脱离了简单对象的约束,现在可以返回 function 对象。该 function 接受 getState,dispatch 作为参数,可以用来延时 dispatch action。

例如如下 action creator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const INCREMENT_COUNTER = 'INCREMENT_COUNTER';

function increment() {
return {
type: INCREMENT_COUNTER,
};
}

function incrementAsync() {
return (dispatch) => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}

其他 Middleware

Redux-Logger

去掉各种细节逻辑,处理逻辑也很简单,就是在 returnedValue 前后记录一些信息。

1
2
3
4
5
6
7
({ getState }) => next => (action) => {
log....
let returnedValue;
returnedValue = next(action);
log....
return returnedValue;
};

细节实现参见redux-logger代码

Redux-Saga

redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

可以想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。

redux-saga 源码稍微有点多,后面考虑单独开坑来写。

Redux-Promise

支持 promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action) ? action.then(dispatch) : next(action);
}

return isPromise(action.payload)
? action.payload
.then(result => dispatch({ ...action, payload: result }))
.catch(error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
})
: next(action);
};
}

更多信息参考redux-promise 代码

2. Think in Redux

动机

当 “变化” 和 “异步” 混在一起,会让我们的应用越来越复杂,就类似曼妥思和可乐混在一起,后果可想而知。

类似于 React 框架分离 View 和对 DOM 的操作,但是应用状态的管理缺撒手不管,这就是 Redux 存在的意义。

随着应用复杂度的提高,我们需要管理越来越多的状态,Redux 让我们重新掌握对状态管理的控制权。

三个原则

单一来源 (Single Source of Truth)

应用的全局状态只存在单一对象 store 中。

仅可读状态 (State is Read-Only)

更改应用状态的唯一方法就是 dispatch 一个 action

一切变更来自纯函数(Changes are made with pure functions)

根据 action 转换 state 的方法(也就是 reducer) 必须是纯函数。

3. 更多教程

实际应用开发中可能会用到的一些案例。

4. 支持平台

本文中默认的都是 JavaScript 版本,由于 Redux 只是一种编程模式,因此可以应用 Android 和 iOS 以及其他 Desktop 的开发技术栈。

JavaScript 版本

https://github.com/reduxjs/redux

Kotlin MultiPlatform

https://github.com/reduxkotlin/redux-kotlin

延伸阅读

参考文献