背景:
把
由于团队内部的
准备:
安装
1 | $ yarn add react-saga |
引入
1 2 3 4 5 6 7 8 9 10 11 | //main.js import { createStore,applyMiddleware } from 'redux'//applyMiddleware把中间件应用起来,可以放多个 import createSagaMiddleware from 'redux-saga' import rootSaga from './sagas' //使用 `redux-saga` 中间件将 Saga 与 Redux Store 建立连接。 const sagaMiddleware = createSagaMiddleware(); const store = createStore( rootReducer, applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga) |
有多个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //sagas.js import { delay } from 'redux-saga' import { put, takeEvery,all } from 'redux-saga/effects' export function* helloSaga() { console.log('Hello Sagas!'); } export function* incrementAsync() { ... } function* watchIncrementAsync() { ... } export default function* rootSaga() { yield all([ helloSaga(), watchIncrementAsync() ]) } |
使用:
调用函数(call)
我们从 Generator 里
- 先后执行多个任务
1 2 3 4 | import { call } from 'redux-saga/effects' const users = yield call(fetch, '/users'), repos = yield call(fetch, '/repos') |
- 同步执行多个任务
当我们需要
1 2 3 4 5 6 | import { call } from 'redux-saga/effects' const [users, repos] = yield [ call(fetch, '/users'), call(fetch, '/repos') ] |
- 获取执行最快的任务
在
- 可用作超时处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import { race, call, put } from 'redux-saga/effects' import { delay } from 'redux-saga' function* fetchPostsWithTimeout() { //这里的 posts和timeout 只有执行快的那个能取得值 const {posts, timeout} = yield race({ posts: call(fetchApi, '/posts'), timeout: call(delay, 1000) }) if (posts) put({type: 'POSTS_RECEIVED', posts}) else put({type: 'TIMEOUT_ERROR'}) } |
- 自动取消失败的 Effects
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import { race, take, call } from 'redux-saga/effects' function* backgroundTask() { while (true) { ... } } function* watchStartBackgroundTask() { while (true) { yield take('START_BACKGROUND_TASK') //当 CANCEL_TASK 被发起,race 将自动取消 backgroundTask,并在 backgroundTask 中抛出取消错误。 yield race({ task: call(backgroundTask), cancel: take('CANCEL_TASK') }) } } |
发起action(put)
1 2 3 4 5 6 7 8 9 | import { call, put } from 'redux-saga/effects' //... function* fetchProducts() { const products = yield call(Api.fetch, '/products') // 创建并 yield 一个 dispatch Effect //dispatch({ type: 'PRODUCTS_RECEIVED', products }) yield put({ type: 'PRODUCTS_RECEIVED', products }) } |
使用 saga 辅助函数
-
takeEvery: 允许多个
fetchData 实例同时启动。在每一个action 到来时派生一个新的任务。 -
takeLatest:得到最新那个请求的响应。 如果已经有一个任务在执行的时候启动另一个
fetchData ,那之前的这个任务会被自动取消。
1 2 3 4 5 6 7 8 9 10 | //sagas.js import { takeEvery,takeLatest } from 'redux-saga' function* fetchData(action) { ... } function* watchFetchData() { yield* takeEvery('FETCH_REQUESTED', fetchData) } function* watchFetchData1() { yield* takeLatest('FETCH_REQUESTED', fetchData) } |
如果你有多个
1 2 3 4 5 6 7 8 9 10 11 12 13 | import { takeEvery } from 'redux-saga/effects' // FETCH_USERS function* fetchUsers(action) { ... } // CREATE_USER function* createUser(action) { ... } // 同时使用它们 export default function* rootSaga() { yield takeEvery('FETCH_USERS', fetchUsers) yield takeEvery('CREATE_USER', createUser) } |
等待action(take)
- takeEvery
把监听的动作换为通配符
1 2 3 4 5 6 7 8 9 10 | import { select, takeEvery } from 'redux-saga/effects' function* watchAndLog() { yield takeEvery('*', function* logger(action) { const state = yield select() console.log('action', action) console.log('state after', state) }) } |
- take
-
监听单个
action take(‘LOGOUT’)
-
监听多个并发的
action ,只要捕获到多个action 中的一个,就执行take之后的内容。take([‘LOGOUT’, ‘LOGIN_ERROR’])
-
监听所有的
action take(’*’)
1 2 3 4 5 6 7 8 9 10 11 | import { select, take } from 'redux-saga/effects' function* watchAndLog() { while (true) { const action = yield take('*') const state = yield select() console.log('action', action) console.log('state after', state) } } |
我们先把上面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 | import { takeEvery } from 'redux-saga/effects' // FETCH_USERS function* fetchUsers(action) { ... } // CREATE_USER function* createUser(action) { ... } // 同时使用它们 export default function* rootSaga() { yield takeEvery('FETCH_USERS', fetchUsers) yield takeEvery('CREATE_USER', createUser) } |
当两到多个
但是,当
由于
我们可以用
1 2 3 4 5 6 7 8 9 10 11 12 | export default function* loginFlow() { while(true) { yield take('LOGIN') // ... perform the login logic yield take('LOGOUT') // ... perform the logout logic } } export default function* rootSaga() { yield* userSaga() } |
优点
saga 主动拉取action ,可以控制监听的开始和结束- 用同步的风格描述控制流,提高了可读性。
无阻塞调用(fork)和取消调用(cancel)
当
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import { take, put, call, fork, cancel } from 'redux-saga/effects' // ... function* loginFlow() { while(true) { const {user, password} = yield take('LOGIN_REQUEST') // fork return a Task object const task = yield fork(authorize, user, password) const action = yield take(['LOGOUT', 'LOGIN_ERROR']) if(action.type === 'LOGOUT') yield cancel(task) yield call(Api.clearItem('token')) } } |
一旦任务被
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import { take, call, put, cancelled } from 'redux-saga/effects' import Api from '...' function* authorize(user, password) { try { const token = yield call(Api.authorize, user, password) yield put({type: 'LOGIN_SUCCESS', token}) yield call(Api.storeItem, {token}) return token } catch(error) { yield put({type: 'LOGIN_ERROR', error}) } finally { if (yield cancelled()) { // ... put special cancellation handling code here } } } |
??注意:
错误处理
- 使用
try/catch 语法在 Saga 中捕获错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import Api from './path/to/api' import { call, put } from 'redux-saga/effects' // ... function* fetchProducts() { try { const products = yield call(Api.fetch, '/products') yield put({ type: 'PRODUCTS_RECEIVED', products }) } catch(error) { yield put({ type: 'PRODUCTS_REQUEST_FAILED', error }) } } |
- 捕捉 Promise 的拒绝操作,并将它们映射到一个错误字段对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import Api from './path/to/api' import { call, put } from 'redux-saga/effects' function fetchProductsApi() { return Api.fetch('/products') .then(response => ({ response })) .catch(error => ({ error })) } function* fetchProducts() { const { response, error } = yield call(fetchProductsApi) if (response) yield put({ type: 'PRODUCTS_RECEIVED', products: response }) else yield put({ type: 'PRODUCTS_REQUEST_FAILED', error }) } |
测试
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 29 30 31 32 | //saga.spec.js 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 })" ) // 创建一个模拟的 error 对象 const error = {} // 期望一个 dispatch 指令 assert.deepEqual( iterator.throw(error).value, put({ type: 'PRODUCTS_REQUEST_FAILED', error }), "fetchProducts should yield an Effect put({ type: 'PRODUCTS_REQUEST_FAILED', error })" ) |
当测试出现分叉的时候,可以调用
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | import { put, take } from 'redux-saga/effects'; import { cloneableGenerator } from 'redux-saga/utils'; test('doStuffThenChangeColor', assert => { const gen = cloneableGenerator(doStuffThenChangeColor)(); //前面都是一样的 gen.next(); // DO_STUFF gen.next(); // CHOOSE_NUMBER //判断奇偶的时候出现了分叉 assert.test('user choose an even number', a => { const clone = gen.clone(); a.deepEqual( clone.next(chooseNumber(2)).value, put(changeUI('red')), 'should change the color to red' ); a.equal( clone.next().done, true, 'it should be done' ); a.end(); }); assert.test('user choose an odd number', a => { const clone = gen.clone(); a.deepEqual( clone.next(chooseNumber(3)).value, put(changeUI('blue')), 'should change the color to blue' ); a.equal( clone.next().done, true, 'it should be done' ); a.end(); }); }); |
为了运行上面的测试代码,我们需要运行:
1 | $ yarn test |
落地:
