现在有很多关于 redux 镇最新的小子redux-saga/redux-saga 的讨论。它使用生成器函数来监听/调度动作。
在开始讨论之前,我想知道redux-saga使用 async/await 而不是下面使用redux-thunkasync/await 的方法的优缺点。
redux-saga
redux-thunk
一个组件可能看起来像这样,像往常一样调度操作。
import { login } from 'redux/auth'; class LoginForm extends Component { onClick(e) { e.preventDefault(); const { user, pass } = this.refs; this.props.dispatch(login(user.value, pass.value)); } render() { return (<div> <input type="text" ref="user" /> <input type="password" ref="pass" /> <button onClick={::this.onClick}>Sign In</button> </div>); } } export default connect((state) => ({}))(LoginForm);
然后我的动作看起来像这样:
// auth.js import request from 'axios'; import { loadUserData } from './user'; // define constants // define initial state // export default reducer export const login = (user, pass) => async (dispatch) => { try { dispatch({ type: LOGIN_REQUEST }); let { data } = await request.post('/login', { user, pass }); await dispatch(loadUserData(data.uid)); dispatch({ type: LOGIN_SUCCESS, data }); } catch(error) { dispatch({ type: LOGIN_ERROR, error }); } } // more actions...
// user.js import request from 'axios'; // define constants // define initial state // export default reducer export const loadUserData = (uid) => async (dispatch) => { try { dispatch({ type: USERDATA_REQUEST }); let { data } = await request.get(`/users/${uid}`); dispatch({ type: USERDATA_SUCCESS, data }); } catch(error) { dispatch({ type: USERDATA_ERROR, error }); } } // more actions...
在 redux-saga 中,与上面的示例等效的是
export function* loginSaga() { while(true) { const { user, pass } = yield take(LOGIN_REQUEST) try { let { data } = yield call(request.post, '/login', { user, pass }); yield fork(loadUserData, data.uid); yield put({ type: LOGIN_SUCCESS, data }); } catch(error) { yield put({ type: LOGIN_ERROR, error }); } } } export function* loadUserData(uid) { try { yield put({ type: USERDATA_REQUEST }); let { data } = yield call(request.get, `/users/${uid}`); yield put({ type: USERDATA_SUCCESS, data }); } catch(error) { yield put({ type: USERDATA_ERROR, error }); } }
首先要注意的是,我们使用表单调用 api 函数yield call(func, ...args)。call不执行效果,它只是创建一个普通对象,如{type: 'CALL', func, args}. 执行被委托给 redux- saga 中间件,该中间件负责执行函数并使用其结果恢复生成器。
yield call(func, ...args)
call
{type: 'CALL', func, args}
主要优点是您可以使用简单的相等检查在 Redux 之外测试生成器
const iterator = loginSaga() assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST)) // resume the generator with some dummy action const mockAction = {user: '...', pass: '...'} assert.deepEqual( iterator.next(mockAction).value, call(request.post, '/login', mockAction) ) // simulate an error result const mockError = 'invalid user/password' assert.deepEqual( iterator.throw(mockError).value, put({ type: LOGIN_ERROR, error: mockError }) )
请注意,我们通过简单地将模拟数据注入next迭代器的方法来模拟 api 调用结果。模拟数据比模拟函数简单得多。
next
要注意的第二件事是调用yield take(ACTION). 动作创建者在每个新动作(例如LOGIN_REQUEST)上调用 Thunks。即动作不断地 被推 送到 thunk,而 thunk 无法控制何时停止处理这些动作。
yield take(ACTION)
LOGIN_REQUEST
在 redux-saga 中,生成器 拉下 一个动作。即他们可以控制何时监听某些动作,何时不监听。在上面的例子中,流指令被放置在一个while(true)循环中,所以它会监听每个传入的动作,这在某种程度上模仿了 thunk push 行为。
while(true)
拉式方法允许实现复杂的控制流。例如,假设我们要添加以下要求
处理 LOGOUT 用户操作
在第一次成功登录时,服务器返回一个令牌,该令牌在存储在expires_in字段中的某个延迟后过期。我们必须每expires_in毫秒在后台刷新授权
expires_in
考虑到在等待 api 调用的结果(初始登录或刷新)时,用户可能会在中间注销。
您将如何使用 thunk 实现它?同时还为整个流程提供完整的测试覆盖?以下是 Sagas 的外观:
function* authorize(credentials) { const token = yield call(api.authorize, credentials) yield put( login.success(token) ) return token } function* authAndRefreshTokenOnExpiry(name, password) { let token = yield call(authorize, {name, password}) while(true) { yield call(delay, token.expires_in) token = yield call(authorize, {token}) } } function* watchAuth() { while(true) { try { const {name, password} = yield take(LOGIN_REQUEST) yield race([ take(LOGOUT), call(authAndRefreshTokenOnExpiry, name, password) ]) // user logged out, next while iteration will wait for the // next LOGIN_REQUEST action } catch(error) { yield put( login.error(error) ) } } }
在上面的示例中,我们使用race. 如果take(LOGOUT)赢得比赛(即用户点击了注销按钮)。比赛会自动取消authAndRefreshTokenOnExpiry后台任务。如果在通话过程authAndRefreshTokenOnExpiry中被阻止,call(authorize, {token})它也会被取消。取消自动向下传播。
race
take(LOGOUT)
authAndRefreshTokenOnExpiry
call(authorize, {token})
您可以找到上述流程的可运行演示