上一篇文章我们分析了Redux-Thunk的源码,可以看到他的代码非常简单,只是让dispatch可以处理函数类型的action,其作者也承认对于复杂场景,Redux-Thunk并不适用,还推荐了Redux-Saga来处理复杂副作用。本文要讲的就是Redux-Saga,这个也是我在实际工作中使用最多的Redux异步解决方案。Redux-Saga比Redux-Thunk复杂得多,而且他整个异步流程都使用Generator来处理,Generator也是我们这篇文章的前置知识,如果你对Generator还不熟悉,可以看看这篇文章。
本文仍然是老套路,先来一个Redux-Saga的简单例子,然后我们自己写一个Redux-Saga来替代他,也就是源码分析。
本文可运行的代码已经上传到GitHub,可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga
简单例子网络请求是我们经常需要处理的异步操作,假设我们现在的一个简单需求就是点击一个按钮去请求用户的信息,大概长这样:
这个需求使用Redux实现起来也很简单,点击按钮的时候dispatch出一个action。这个action会触发一个请求,请求返回的数据拿来显示在页面上就行:
import React from 'react'; import { connect } from 'react-redux'; function App(props) { const { dispatch, userInfo } = props; const getUserInfo = () => { dispatch({ type: 'FETCH_USER_INFO' }) } return ( <div className="App"> <button onClick={getUserInfo}>Get User Info</button> <br></br> {userInfo && JSON.stringify(userInfo)} </div> ); } const matStateToProps = (state) => ({ userInfo: state.userInfo }) export default connect(matStateToProps)(App);上面这种写法都是我们之前讲Redux就介绍过的,Redux-Saga介入的地方是dispatch({ type: 'FETCH_USER_INFO' })之后。按照Redux一般的流程,FETCH_USER_INFO被发出后应该进入reducer处理,但是reducer都是同步代码,并不适合发起网络请求,所以我们可以使用Redux-Saga来捕获FETCH_USER_INFO并处理。
Redux-Saga是一个Redux中间件,所以我们在createStore的时候将它引入就行:
// store.js import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import reducer from './reducer'; import rootSaga from './saga'; const sagaMiddleware = createSagaMiddleware() let store = createStore(reducer, applyMiddleware(sagaMiddleware)); // 注意这里,sagaMiddleware作为中间件放入Redux后 // 还需要手动启动他来运行rootSaga sagaMiddleware.run(rootSaga); export default store;注意上面代码里的这一行:
sagaMiddleware.run(rootSaga);sagaMiddleware.run是用来手动启动rootSaga的,我们来看看rootSaga是怎么写的:
import { call, put, takeLatest } from 'redux-saga/effects'; import { fetchUserInfoAPI } from './api'; function* fetchUserInfo() { try { const user = yield call(fetchUserInfoAPI); yield put({ type: "FETCH_USER_SUCCEEDED", payload: user }); } catch (e) { yield put({ type: "FETCH_USER_FAILED", payload: e.message }); } } function* rootSaga() { yield takeEvery("FETCH_USER_INFO", fetchUserInfo); } export default rootSaga;上面的代码我们从export开始看吧,export的东西是rootSaga这个Generator函数,这里面就一行:
yield takeEvery("FETCH_USER_INFO", fetchUserInfo);这一行代码用到了Redux-Saga的一个effect,也就是takeEvery,他的作用是监听每个FETCH_USER_INFO,当FETCH_USER_INFO出现的时候,就调用fetchUserInfo函数,注意这里是每个FETCH_USER_INFO。也就是说如果同时发出多个FETCH_USER_INFO,我们每个都会响应并发起请求。类似的还有takeLatest,takeLatest从名字都可以看出来,是响应最后一个请求,具体使用哪一个,要看具体的需求。
然后看看fetchUserInfo函数,这个函数也不复杂,就是调用一个API函数fetchUserInfoAPI去获取数据,注意我们这里函数调用并不是直接的fetchUserInfoAPI(),而是使用了Redux-Saga的call这个effect,这样做可以让我们写单元测试变得更简单,为什么会这样,我们后面讲源码的时候再来仔细看看。获取数据后,我们调用了put去发出FETCH_USER_SUCCEEDED这个action,这里的put类似于Redux里面的dispatch,也是用来发出action的。这样我们的reducer就可以拿到FETCH_USER_SUCCEEDED进行处理了,跟以前的reducer并没有太大区别。
// reducer.js const initState = { userInfo: null, error: '' }; function reducer(state = initState, action) { switch (action.type) { case 'FETCH_USER_SUCCEEDED': return { ...state, userInfo: action.payload }; case 'FETCH_USER_FAILED': return { ...state, error: action.payload }; default: return state; } } export default reducer;通过这个例子的代码结构我们可以看出:
action被分为了两种,一种是触发异步处理的,一种是普通的同步action。
异步action使用Redux-Saga来监听,监听的时候可以使用takeLatest或者takeEvery来处理并发的请求。
具体的saga实现可以使用Redux-Saga提供的方法,比如call,put之类的,可以让单元测试更好写。