之前文章提过,Redux 是 Flux 架构与函数式编程结合的产物。
一、Redux Flow
Redux 的数据流大致如下:
UI Component → Action → Reducer → State → UI Component
用户访问页面,然后通过 View
发出 Action
(一个原始的 JavaScript 对象),由 Dispatcher
进行派发,Reducer
(一个纯函数)接收到后进行处理并返回状态 State
(存储 State
的容器叫做 Store
),然后通知 View
更新页面。
对于同步且没有副作用的操作,上述数据流可以起到管理数据,从而控制视图更新的目的。
那么遇到含有副作用的操作时(比如 Ajax 异步请求),我们应该怎么做?
答案是使用中间件。
二、中间件的概念
对于中间件或者异步操作的思想,我不展开赘述,可以看一下阮一峰老师的这篇中间件与异步操作的文章。我对文中内容有多少疑惑,但又不知道怎么说,可能是我造诣不够深。
我是这样理解的,类似 redux-thunk、redux-promise、redux-saga 等中间件是帮助我们在异步操作结束后,使得 Reducer 自动执行。
其实中间件的实现是对 store.dispatch()
的改造,在发出 Action
和执行 Reducer
之间,添加了其他功能。
例如:
let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action)
next(action)
console.log('next state', store.getState())
}
上面的代码,对 store.dispatch()
进行了重定义,在发送 Action
前后添加了打印功能,这就是中间件的雏形。
加入中间件后,Redux 的数据流大致如下:
在含副作用的 Action
与原始 Action
之间增加了中间件的处理。其中中间件的作用转换异步操作,生成原始的 Action
对象,后面的流程不变。
在此之前,其实我们已经使用到了中间件,那就是 redux-logger
。
三、redux-thunk
我们先看看 redux-thunk 的源码。
// redux-thunk 源码
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;
对吧,看起来并不难。当 action
为函数时,就调用该函数。
下面我们写一个案例吧。
- 安装
redux-thunk
。
$ yarn add [email protected]
- 调整
store/index.js
,引入redux-thunk
中间件。这里我们暂时把此前redux-saga
的配置注释掉,并改成redux-thunk
配置。
// src/js/store/index.js
import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import logger from 'redux-logger'
import reducers from '../reducers'
const initialState = { count: 0, status: 'offline' }
const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
const store = createStore(reducers, initialState, composeEnhancers(applyMiddleware(thunkMiddleware, logger)))
export default store
- 新建一个
userHelper.js
文件。这里我们不用fetch
或者XHR
来创建一个异步请求,而是使用setTimeout
方法来实现一个异步请求用户数据的场景。
// src/js/utils/userHelper.js
class UserHelper {
// 延迟函数
delay(time) {
return new Promise(resolve => setTimeout(resolve, time))
}
// 获取数据
fetchData(status) {
return new Promise((resolve, reject) => {
const response = { status: 'success', response: { name: 'Frankie', age: 20 } }
const error = { status: 'error', error: 'oops' }
status ? resolve(response) : reject(error)
})
}
// 请求用户数据(异步场景)
async getUser(data) {
try {
const res = await this.fetchData(data)
await this.delay(2000)
return res
} catch (e) {
await this.delay(1000)
throw e
}
}
}
export default new UserHelper()
- 新建一个
userActions.js
,这是redux-thunk
的关键。
// src/js/action/userActions.js
import userHelper from '../utils/userHelper'
export const getUser = (data, callback) => {
return (dispatch, getState) => {
dispatch({ type: 'FETCH_REQUEST', status: 'requesting' })
userHelper.getUser(data).then(res => {
dispatch({ type: 'FETCH_SUCCESS', ...res })
callback && callback(res)
}).catch(err => {
dispatch({ type: 'FETCH_FAILURE', ...err })
})
}
}
- 修改
About
组件,需要注意的是,在这里我们传给store.dispatch()
的不是一个原始对象(plain object
),而是getUser
函数。这就是redux-thunk
的特点,它可让store.dispatch()
接受一个函数作为参数,而这个函数叫做Action Creator
。
// src/js/pages/about/index.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getUser } from '../../actions/userActions'
class About extends Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
return (
About Component!
Get User: {this.props.user.status || ''}
{/* 我们发现这里并不是传了一个标准的 Action 对象,而是一个函数 */}
)
}
}
const mapStateToProps = state => {
return { user: state.user }
}
export default connect(mapStateToProps)(About)
我们点击 Get User Success 按钮,即可看到如下效果。
redux-thunk 的缺点
根据源码我们知道,它仅仅帮我们执行了这个函数,而不在乎函数主体内部是什么。实际情况下,它这个函数可能要复杂得很多。再者,如果每一个异步操作都需要如此定义一个 Action Creator
,显然它是不易维护的。
-
Action
的形式不统一 - 异步操作太分散,分散在各个
Action
当中
所以本项目将不会使用它,只是因为 redux-thunk
在之前做项目的时候用过,就拿出来讲一下。
四、redux-saga
关于接下来 redux-saga 部分的内容,我认为我可能讲解得不太好,建议看文章最后的链接。
redux-saga
是一个用于管理异步操作的中间件。它通过创建 Saga 将所有的异步操作逻辑收集在一个地方集中处理,Saga 负责协调那些复杂或异步的操作,Reducer 还是负责处理 Action 和 State 的更新。
Saga 是通过 Generator
函数创建的,如果还不太熟悉 Generator
函数,请看阮一峰老师的 ES6 入门教程中对 Generator 函数的介绍。
Saga 不同于 Thunk,Thunk 是在 Action 被创建时调用,而 Saga 只会在应用启动时调用(但初始启动的 Saga 可能会动态调用其他 Saga),Saga 可以被看作是在后台运行的进程,Saga 监听发起的 Action,然后决定基于这个 Action 来做什么:是发起一个异步调用(如 fetch 请求),还是发起其他的 Action,甚至是调用其他的 Saga。
在 redux-saga
的世界里,所有的任务都通过 yield Effects
来完成(Effect
可以看作是 redux-saga
的任务单元)。Effects
是简单的 JavaScript 对象,包含了要被 Saga middleware 执行的信息(比如,我们 Redux Action 其实是一个包含了执行信息({ type, ... }
)的原始的 JavaScript 对象 ),redux-saga
为各项任务提供了各种 Effect
创建器,比如调用一个异步函数,发起一个 Action 到 Store,启动一个后台任务或者等待一个满足某些条件的未来的 Action。
redux-saga 核心 API
1. Saga 辅助函数
redux-saga 提供了一些辅助函数,用来在一些特定的 Action 被发起到 Store 时派生任务,下面先讲解两个辅助函数:takeEvery
和 takeLatest
。
- takeEvery
例如:每次点击 Fetch 按钮是,我们发起一个FETCH_REQUESTED
的 Action。我们想通过启动一个任务从服务器获取一些数据来处理这个 Action。
// src/
import { call, put, takeEvery } from 'redux-saga/effects'
import userHelper from '../utils/userHelper'
// 创建一个异步任务
function* fetchData(action) {
try {
// call([context, fnName], ...args)
const data = yield call([userHelper, userHelper.getUser], action.flag)
yield put({ type: 'FETCH_SUCCESS', ...data })
} catch (e) {
yield put({ type: 'FETCH_FAILURE', ...e })
}
}
// 每次 FETCH_REQUESTED Action 被发起时启动上面的任务
export function* watchFetchData(a) {
yield takeEvery('FETCH_REQUEST', fetchData)
// 等同于
// while (true) {
// const action = yield take('FETCH_REQUEST')
// yield fork(fetchData, action)
// }
}
- takeLatest
在上面的例子,takeEvery 允许多个 fetchData 实例同时启动,在某个特定的时刻,我们可以启动新的 fetchData 任务,尽管此前还有一个或者多个 fetchData 尚未结束。
如果只想得到最新的那个请求的响应,我们可以使用 takeLatest 辅助函数
和 takeEvery 不同的是,在任何时刻 takeLatest 只允许一个 fetchData 任务,并且这个任务时最后被启动的那个。如果此前已经有一个任务在执行,那么此前这个任务会自动被取消。
import { takeLatest } from 'redux-saga/effects'
export function* watchFetchData(a) {
yield takeLatest('FETCH_REQUEST', fetchData)
}
2. Effect 创建器
Saga 是由一个个的 effect 组成的,那么 effect 是什么?
redux-saga 官网的解释:一个 effect 就是一个 Plain Object JavaScript 对象,包含一些将被 saga middleware 执行的指令。redux-saga 提供了很多 effect 创建器,如
call
、put
、take
等。
比如 call
:
import { call } from 'redux-saga/effects'
function* fetchData() {
yield call(fetch)
}
call(userHelper.getUser)
生成的就是一个 effect,类似如下:
{
isEffect: true,
type: 'CALL',
fn: fetch
}
常用的 effect 有:
- take(pattern)
- put(action)
- call(fn, ...args)
- fork(fn, ...args)
- select(selector, ...args)
3. 常用 Effect 方法
(1)take
take 这个方法是用来监听未来的 Action,它创建一个命令对象,告诉 Middleware 等待一个特定的 Action,Generator 函数会暂停,直到一个与 pattern 匹配的 action 被发起,才会继续执行下面的语句。也就是说,take 是一个阻塞的 effect。
export function* watchFetchData(a) {
while (true) {
// 监听一个 type 为 'FETCH_REQUEST' 的 Action 的执行,直到这个 Action被触发,
// 才会执行下面的 yield fork(fetchData) 语句。
yield take('FETCH_REQUEST')
yield fork(fetchData)
}
}
(2)put
它是用来发送 Action 的 effect,你可以简单地理解成 redux 框架中的 dispatch 函数。当 put 一个 Action 后,reducer 就会计算新的 state 并返回。put 也是阻塞的 effect。
结合 take 和 put 方法,举个例子:
// *********************** 辅助理解 ***********************
// 在 redux 中,我们发起这样一个 Action
const fetchAction = { type: 'FETCH_REQUEST' }
store.dispatch(fetchAction)
// 使用 Saga 如何处理呢?
// 需要注意的是:以下 Saga 方法实现,并不是一个完整可执行的逻辑,仅用以举例说明,辅助理解而已。
//
// 1. 首先,在我们启动 Saga 时,使用 take 来监听 type 为 'FETCH_REQUEST' 的 Action
const fetchAction = yield take('FETCH_REQUEST')
// 2. 从 UI 向 Saga 中间件传递一个 Action
this.props.dispatch({ type: 'FETCH_REQUEST' })
// 3. 此时我们的 Saga 监听到 'FETCH_REQUEST',接着开始执行 take('FETCH_REQUEST') 后面的逻辑
yield put(fetchAction)
// 4. put 方法,可以发出 Action,且发出的 Action 会被 Reducer 监听到。从而返回一个新状态
(3)call/apply
call(fn, ...args)
// 支持传递 this 上下文给 fn。在调用对象方法时很有用。
call([context, fn], ...args)
// 支持用字符串传递 fn。在调用对象的方法时很有用。
// 例如 yield call([localStorage, 'getItem'], 'redux-saga')。
call([context, fnName], ...args)
// call([context, fn], ...args) 的另一种写法
apply(context, fn, [args])
语法与 JS 中的 call/apply 相似。
可以把它简单的理解为调用其他函数的函数,它命令 middleware 以参数
args
来调用fn
函数。注意:
fn
既可以是一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数。还有,call 是阻塞的 effect。
(4)fork
fork(fn, ...args)
fork 类似于 call,可以用来调用普通函数和 Generator 函数。不过,fork 的调用是非阻塞的,Generator 不会在等待
fn
返回结果的时候被 middleware 暂停;恰恰相反地,它在fn
被调用时便会立即恢复执行。
(5)select
select(selector, ...args)
// 如果 select 的参数为空会取得完整的 state(与调用 getState() 的结果相同)
// yield select()
// 返回 state 的一部分数据可以这样获取
// yield select(state => state.user)
select 函数是用来指示 middleware 调用提供的选择器获取 Store 上的 state 数据。你也可以简单的把它理解为 redux 框架中获取 store 上的 state 数据一样的功能(
store.getState()
)
4. Middleware API
createSagaMiddleware()
创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。middleware.run(saga, ...args)
动态地运行saga
,只能用于在applyMiddleware
阶段之后执行 Saga。
sagas
中的每个函数都必须返回一个 Generator 对象,middleware 会迭代这个 Generator 并执行所有 yield 后的 Effect。(Effect 可以看作是 redux-saga 的任务单元)
五、Saga 案例实现
下面写一个处理 Fetch 请求的异步处理场景。
首先,实现 Saga 处理场景:
import { call, fork, put, select, take, delay, race, takeEvery, takeLatest } from 'redux-saga/effects'
// fetch 请求
function fetch() {
return new Promise((resolve, reject) => {
window
.fetch('http://192.168.1.124:7701/config')
.then(response => response.json())
.then(res => {
// 请求成功,返回一个 JSON 数据:{"name":"Frankie","age":20}
resolve(res)
})
.catch(err => {
reject(err)
})
})
}
// saga 处理异步场景
function* fetchData() {
try {
// race 与 Promise.race 类似,这里做一个超时处理
const { result, timeout } = yield race({
result: call(fetch),
timeout: delay(30000)
})
if (timeout) throw new Error('请求超时!')
yield put({ type: 'FETCH_SUCCESS', ...result })
} catch (e) {
console.warn(e)
yield put({ type: 'FETCH_FAILURE', status: 'error', error: 'oops' })
}
}
export function* watchFetchData() {
// 每次 Saga 监听到 'FETCH_REQUEST' 类型的 Action,都会触发 fetchData 函数
yield takeEvery('FETCH_REQUEST', fetchData)
}
接着,我们在 UI 中派发一个 FETCH_REQUEST
的 Action,然后 Saga 监听到之后,就会执行 fetchData
的逻辑了。
About Component!
Get User: {this.props.user.name || ''}
看结果:
至此
Redux + Middleware 基本的已经介绍完了,但我不认为我讲好了。建议大家看看以下几篇文章来加深理解。
还有 Redux 搭配中间件的我认为要学习的 API 很多,有点费劲。有空看下另一个解决方案: MobX
接下来终于可以介绍 react-hot-loader 热更新了,关于 react-router、redux、react-redux、redux-saga 等内容花了好多篇幅。
参考
- redux-saga 中文文档
- 彻彻底底教会你使用 Redux-saga
- redux-saga 框架使用详解及 Demo 教程