前言
之前文章提过,Redux 是 Flux 架构与函数式编程结合的产物。
正文
一、Redux Flow
Redux 的数据流大致如下:
UI Component → Action → Reducer → State → UI Component
Redux Flow
用户访问页面,然后通过 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 的数据流大致如下:
Redux Flow With Middleware
在含副作用的 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 redux-thunk@2.3.0
- 调整
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 ( <div> <h3>About Component!</h3> <h5>Get User: {this.props.user.status || ''}</h5> {/* 我们发现这里并不是传了一个标准的 Action 对象,而是一个函数 */} <button onClick={() => { this.props.dispatch(getUser(false)) }}>Get User Fail</button> <button onClick={() => { this.props.dispatch(getUser(true)) }}>Get User Success</button> </div> ) } } 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 有:
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
的逻辑了。
<div> <h3>About Component!</h3> <h5>Get User: {this.props.user.name || ''}</h5> <button onClick={() => { this.props.dispatch({ type: 'FETCH_REQUEST', status: 'requesting' }) }}>Fetch Data</button> </div>
看结果:
至此
Redux + Middleware 基本的已经介绍完了,但我不认为我讲好了。建议大家看看以下几篇文章来加深理解。
还有 Redux 搭配中间件的我认为要学习的 API 很多,有点费劲。有空看下另一个解决方案: MobX
接下来终于可以介绍 react-hot-loader 热更新了,关于 react-router、redux、react-redux、redux-saga 等内容花了好多篇幅。