从redux-thunk到redux-saga实践
简介
Redux Saga可以理解为一个和系统交互的常驻进程,其中,Saga可简单定义如下: Saga = Worker + Watcher saga特点:
Redux Saga适用于对事件操作有细粒度需求的场景,同时他们也提供了更好的可测试性。 thunk VS saga这里有一个简单的需求,登录页面,使用 组件部分二者应该是大同小异: 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); 使用redux-thunk登录的action文件 // 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-sagaexport function* loginSaga() { while(true) { const { user,pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST try { let { data } = yield call(request.post,'/login',pass }); //阻塞,请求后台数据 yield fork(loadUserData,data.uid); //非阻塞执行loadUserData yield put({ type: LOGIN_SUCCESS,data }); //发起一个action,类似于dispatch } 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 }); } } 我们使用形式 优点相比Redux Thunk,使用Redux Saga有几处明显的变化:
除开上述这些不同点,Redux Saga真正的威力,在于其提供了一系列帮助方法,使得对于各类事件可以进行更细粒度的控制,从而完成更加复杂的操作。 方便测试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,mockAction) ) // simulate an error result const mockError = 'invalid user/password' assert.deepEqual( iterator.throw(mockError).value,put({ type: LOGIN_ERROR,error: mockError }) ) 注意,我们通过简单地将模拟数据注入迭代器的下一个方法来检查api调用结果。模拟数据比模拟函数更简单。 监听过滤action通过 复杂应用场景假设例如我们要添加以下要求:
你将如何实现这一点与thunk?同时还为整个流程提供全面的测试覆盖? 可是如果你使用redux-saga: 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表示了并发要求。
其他特殊场景同时执行多个任务有时候我们需要在几个ajax请求执行完之后,再执行对应的操作。redux-thunk需要借助第三方的库,而redux-saga是直接实现的。 import { call } from 'redux-saga/effects' // 正确写法,effects 将会同步执行 const [users,repos] = yield [ call(fetch,'/users'),call(fetch,'/repos') ] 当我们需要 yield 一个包含 effects 的数组, generator 会被阻塞直到所有的 effects 都执行完毕,或者当一个 effect 被拒绝 (就像 Promise.all 的行为)。 监听action在redux-saga中,我们可以使用了辅助函数 takeEvery 在每个 action 来到时派生一个新的任务。 这多少有些模仿 redux-thunk 的行为:举个例子,每次一个组件调用 在现实情况中,takeEvery 只是一个在强大的低阶 API 之上构建的辅助函数。 在这一节中我们将看到一个新的 Effect,即 take。take 让我们通过全面控制 action 观察进程来构建复杂的控制流成为可能。 让我们开始一个简单的 Saga 例子,这个 Saga 将监听所有发起到 store 的 action,然后将它们记录到控制台。 使用 takeEvery('')( 代表通配符模式),我们就能捕获发起的所有类型的 action。 import { takeEvery } from 'redux-saga' function* watchAndLog(getState) { yield* takeEvery('*',function* logger(action) { console.log('action',action) console.log('state after',getState()) }) } 现在我们知道如何使用 take Effect 来实现和上面相同的功能: import { take } from 'redux-saga/effects' function* watchAndLog(getState) { while(true) { const action = yield take('*') console.log('action',getState()) }) } take 就像我们更早之前看到的 call 和 put。它创建另一个命令对象,告诉 middleware 等待一个特定的 action。 正如在 call Effect 的情况中,middleware 会暂停 Generator,直到返回的 Promise 被 resolve。 在 take 的情况中,它将会暂停 Generator 直到一个匹配的 action 被发起了。 在以上的例子中,watchAndLog 处于暂停状态,直到任意的一个 action 被发起。 注意,我们运行了一个无限循环的 while(true)。记住这是一个 Generator 函数,它不具备 从运行至完成 的行为 一个简单的例子,假设在我们的 Todo 应用中,我们希望监听用户的操作,并在用户初次创建完三条 Todo 信息时显示祝贺信息。 import { take,put } from 'redux-saga/effects' function* watchFirstThreeTodosCreation() { for(let i = 0; i < 3; i++) { const action = yield take('TODO_CREATED') } yield put({type: 'SHOW_CONGRATULATION'}) } 与 while(true) 不同,我们运行一个只迭代三次的 for 循环。在 take 初次的 3 个 TODO_CREATED action 之后, 任务取消一旦任务被 fork,可以使用 防抖动为了对 action 队列进行防抖动,可以在被 fork 的任务里放置一个 delay。 const delay = (ms) => new Promise(resolve => setTimeout(resolve,ms)) function* handleInput(input) { // 500ms 防抖动 yield call(delay,500) ... } function* watchInput() { let task while(true) { const { input } = yield take('INPUT_CHANGED') if(task) yield cancel(task) task = yield fork(handleInput,input) } } 在上面的示例中,handleInput 在执行之前等待了 500ms。如果用户在此期间输入了更多文字,我们将收到更多的 常用Effect一个 effect 就是一个纯文本 JavaScript 对象,包含一些将被 saga middleware 执行的指令。 使用 redux-saga 提供的工厂函数来创建 effect。 举个例子,你可以使用 从 Saga 内触发异步操作(Side Effect)总是由 yield 一些声明式的 Effect 来完成的 (你也可以直接 yield Promise,但是这会让测试变得困难。使用 Effect 诸如 call 和 put,与高阶 API 如 takeEvery 相结合,让我们实现与 redux-thunk 同样的东西, 但又有额外的易于测试的好处。 task一个 task 就像是一个在后台运行的进程。在基于 redux-saga 的应用程序中,可以同时运行多个 task。通过 fork 函数来创建 task: function* saga() { ... const task = yield fork(otherSaga,...args) ... } Watcher/Worker指的是一种使用两个单独的 Saga 来组织控制流的方式。 Watcher: 监听发起的 action 并在每次接收到 action 时 fork 一个 worker。 Worker: 处理 action 并结束它。 示例: function* watcher() { while(true) { const action = yield take(ACTION) yield fork(worker,action.payload) } } function* worker(payload) { // ... do some stuff } take(pattern)创建一条 Effect 描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 会暂停,直到一个与 pattern 匹配的 action 被发起。 用以下规则来解释 pattern:
put(action)用于触发 action,功能上类似于dispatch。 创建一条
直接使用dispatch: //... function* fetchProducts(dispatch) const products = yield call(Api.fetch,'/products') dispatch({ type: 'PRODUCTS_RECEIVED',products }) } 该解决方案与我们在上一节中看到的从 Generator 内部直接调用函数,有着相同的缺点。如果我们想要测试 fetchProducts 接收到 AJAX 响应之后执行 dispatch, 我们还需要模拟 dispatch 函数。 相反,我们需要同样的声明式的解决方案。只需创建一个对象来指示 middleware 我们需要发起一些 action,然后让 middleware 执行真实的 dispatch。 这种方式我们就可以同样的方式测试 Generator 的 dispatch:只需检查 yield 后的 Effect,并确保它包含正确的指令。 redux-saga 为此提供了另外一个函数 put,这个函数用于创建 import { call,put } from 'redux-saga/effects' //... function* fetchProducts() { const products = yield call(Api.fetch,'/products') // 创建并 yield 一个 dispatch Effect yield put({ type: 'PRODUCTS_RECEIVED',products }) } 现在,我们可以像上一节那样轻易地测试 Generator: import { call,put } from 'redux-saga/effects' import Api from '...' const iterator = fetchProducts() // 期望一个 call 指令 assert.deepEqual( iterator.next().value,call(Api.fetch,'/products'),"fetchProducts should yield an Effect call(Api.fetch,'./products')" ) // 创建一个假的响应对象 const products = {} // 期望一个 dispatch 指令 assert.deepEqual( iterator.next(products).value,put({ type: 'PRODUCTS_RECEIVED',products }),"fetchProducts should yield an Effect put({ type: 'PRODUCTS_RECEIVED',products })" ) call(fn,...args)用于调用异步逻辑,支持 promise 。 创建一条 Effect 描述信息,指示 middleware 调用 fn 函数并以 args 为参数。fn 既可以是一个普通函数,也可以是一个 Generator 函数。 middleware 调用这个函数并检查它的结果。 如果结果是一个 Generator 对象,middleware 会执行它,就像在启动 如果结果是一个 Promise,middleware 会暂停直到这个 Promise 被 resolve,resolve 后 Generator 会继续执行。 或者直到 Promise 被 reject 了,如果是这种情况,将在 Generator 中抛出一个错误。 yield fork(fn ...args) 的结果是一个 Task 对象 —— 一个具备某些有用的方法和属性的对象 fork(fn,...args)创建一条 Effect 描述信息,指示 middleware 以 无阻塞调用 方式执行 fn。 fork 类似于 call,可以用来调用普通函数和 Generator 函数。但 fork 的调用是无阻塞的,在等待 fn 返回结果时,middleware 不会暂停 Generator。 相反,一旦 fn 被调用,Generator 立即恢复执行。 race(effects)创建一条 Effect 描述信息,指示 middleware 在多个 Effect 之间执行一个 race(类似 Promise.race([...]) 的行为)。 apiredux-saga的其他详细API列举如下,API详解可以查看API 参考
参考
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |