Redux 学习 - react-saga

上两篇文章主要说了 redux 的用法。redux 中 reducer 只能处理同步的状态更新,那如果是有异步或者副作用呢,这时候我们就必须对 redux 使用中间件处理了。

redux 本身支持中间件,异步的方案都是通过中间件进行控制的。

在标准 redux 中,要想修改全局状态,组件需要 dispatch 一个 action 到 reducer 上,reducer 同步修改 state。

reducer 是负责同步修改 state,这个逻辑不会改变。

一般的思路是,监听(拦截)dispatch 的 action,如果发现是一个异步 action,就不执行 reducer(或者走到了 reducer 的 default 默认返回逻辑里)。此时执行异步逻辑,完毕后,再 dispatch 一个同步的 action。

处理这类异步或者副作用问题,常见的解决方案有 redux-thunk redux-promise redux-saga 等,其中 redux-saga 我认为算是比较优雅的实现,唯独就是初次理解上有些难度。

我们继续用最早的 redux 的例子,改装一下,实现 saga 的异步处理。

完整代码可以参考 github

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
  import React from "react"
import ReactDOM from "react-dom"
import { Provider } from "react-redux"
- import { createStore } from "redux"
+ import { createStore, applyMiddleware } from "redux"
+ import createSagaMiddleware from "redux-saga"
+ import saga from "./sagas"
import reducer from "./reducer.js"
import Counter from "./Counter"

+ const sagaMiddleware = createSagaMiddleware()
- const store = createStore(reducer)
+ const store = createStore(reducer, applyMiddleware(sagaMiddleware))
+ sagaMiddleware.run(saga)

class App extends React.PureComponent {
render () {
return (
<Provider store={store}>
<Counter />
</Provider>
)
}
}

const rootElement = document.getElementById("root")
ReactDOM.render(<App />, rootElement)

修改 store 的创建方式,增加 saga 的中间件。

这里要注意写法:

1
2
3
4
5
6
7
// 这样写是不可以的
const sagaMiddleware = createSagaMiddleware(saga)

// 必须写成 run 的形式
const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(saga) // run 必须写到 createStore 之后,否则报错

这样调整后,我们就可以直接在组件内 dispatch 异步 action 了,并在 sagas.js 中进行监听。

Counter.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
  import React from "react"
import { connect } from "react-redux"

class Counter extends React.PureComponent {
render () {
const { globalState, dispatch } = this.props
return (
<div>
<p>COUNT:{globalState.count}</p>
<button onClick={() => dispatch({ type: "INCREMENT", payload: 1 })}>
+1
</button>
<button onClick={() => dispatch({ type: "DECREMENT", payload: 1 })}>
-1
</button>
+ <button onClick={() => dispatch({ type: "INCREMENT_ASYNC", payload: 2 })}>
+ +2 async takeEvery
+ </button>
+ <button onClick={() => dispatch({ type: "DECREMENT_ASYNC", payload: 2 })}>
+ -2 async takeLatest
+ </button>
</div>
)
}
}

const mapStateToProps = state => ({ globalState: state })
export default connect(mapStateToProps)(Counter)

组件内增加两个异步的调用按钮。调用类型分别是 INCREMENT_ASYNC DECREMENT_ASYNC

其他不要任何修改。

sagas.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { put, all, takeEvery, takeLatest, delay } from "redux-saga/effects"

function* incrementAsync (action) {
yield delay(1000)
yield put({ type: "INCREMENT", payload: action.payload })
}
function* decrementAsync (action) {
yield delay(1000)
yield put({ type: "DECREMENT", payload: action.payload })
}

function* watchIncrementAsync () {
yield takeEvery("INCREMENT_ASYNC", incrementAsync)
}
function* watchDecrementAsync () {
yield takeLatest("DECREMENT_ASYNC", decrementAsync)
}

function* saga () {
yield all([watchIncrementAsync(), watchDecrementAsync()])
}

export default saga

sagas.js 是全新添加的文件。

这里添加了两个监听函数 watchIncrementAsync watchDecrementAsync,就是监听 dispatch 的内容,一旦命中监听函数,那么就执行对应的异步操作,分别是 incrementAsync decrementAsync

异步操作函数 incrementAsync decrementAsync,为了模拟,使用的 delay 函数,这里可以换成 fetch 等函数。等异步操作完毕后,重新触发同步 action 即可。

最后导出我们的监听函数,正常的项目不可能只有一个异步监听函数,所以我们需要合并后导出。

有可能这种写法更常见:

1
yield all([call(watchIncrementAsync), call(watchDecrementAsync)])

用 call 辅助函数代替直接使用括号运行,此外我还不知道用 call 函数有什么特殊用途或区别。

这个文件导入了很多辅助函数,下面列出一些说明。具体内容参考官方文档

辅助函数 用途
all 合并多个异步监听函数使用
takeEvery 监听函数使用,起到如何监听异步事件。表示每次都监听到,依次执行
takeLatest 监听函数使用,起到如何监听异步事件。表示仅执行最后一次操作
put 相当于 dispatch,触发 action 使用
delay 延时使用,真实项目一般项目用不到

最后,你感兴趣的话,可以在 reducer.js 文件中加入日志,就会发现 dispatch 异步 action 也会调用 reducer,只不过没有命中任何条件。在执行异步任务之后,还会 dispatch 一个同步 action,此时 reducer 再次执行一次。

–END–