Engineering
2016 年 7 月 29 日
深入浅出 Redux 的异步 Actions
Redux 是一个超棒的 state 容器,它几乎已经成为了 React 前端项目的必备 state 管理库,当然 Redux 可以用于任何应用 JavaScript 应用中,让我们看看这个很棒的库的代码情况吧。
➜ src git:(master) ✗ pwd
/Users/ming/Codes/redux/src
➜ src git:(master) ✗ cloc .
7 text files.
7 unique files.
0 files ignored.
http://cloc.sourceforge.net v 1.65 T=0.03 s (212.3 files/s, 18136.0 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Javascript 7 62 199 337
-------------------------------------------------------------------------------
SUM: 7 62 199 337
-------------------------------------------------------------------------------
这么棒的东西, 其实就几百行代码而已,不到两个小时你就几乎可以阅读完了,当然要深入理解其理念,还是需要一些时间的,让我们慢慢来。
基本
import { createStore } from 'redux'
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
let store = createStore(counter)
store.subscribe(() =>
console.log(store.getState())
)
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
这就是一个很完整的基于 Redux 的 JavaScript 应用了, 我们可以看到这是一个 Counter, 这个 Counter 可以进行 INCREMENT, DECREMENT 操作。 这不是就是简单的观察者模式吗,有人可能会说,是的,其实就是一个简单的观察者模式。还有一些有经验的工程师可能会说,这代码完全没有办法重用和拓展啊,别急,我们慢慢来。
让我们简单重构一下我们的代码吧:
reducer.js
const initState = 0;
export default function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
actions.js
export function incr() {
return {
type: 'INCREMENT',
};
}
export function decr() {
return {
type: 'DECREMENT',
}
}
main.js
import { createStore } from 'redux'
let store = createStore(counter)
store.subscribe(() =>
console.log(store.getState())
)
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
这样是不是感觉熟悉了很多呢,很多时候,简单遵循一些原则,代码就好看很多,比如这里的单一责任原则(Single responsibility principle).
从上面的代码可以看到,我们的 actions 全都是返回 plain object 的 function, 因为 store 只能 dispatch 纯的 Object, 而且携带这 type 属性。那么当我们有 actions 是的异步的怎么呢,比如我们有一个 ADD 的 action, 需要先去取到一个 value 然后才能进行添加操作,那么我们应该怎么做呢?
异步 actions
function add(value) {
return {
type: 'ADD',
value,
}
}
function fetchValue() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(2)
}, 1000)
})
}
function asyncAdd() {
return function(dispatch) {
fetchValue().then((val) => {
dispatch(add(val))
});
}
}
可以看到,我们在 dispatch 这个 add action之前,需要先得到需要 add 的 value, 而这个 value 的获取是异步的。所以我需要 dispatch 的是 asyncAdd() 这个 action, 但是这个 action 返回不是一个 plain object, 而是一个 function, 那么我们怎么才能让 store 能够直接 dispatch 一个 function 呢,我们需要一个中间件。
Redux 提供了
redux-thunk
这个中间件来帮组我们做这件事情。import { createStore, applyMiddleware } from 'redux'
const store = createStore(redux, applyMiddleware(thunk));
store.dispatch(asyncAdd());
可以看到我们 dispatch 了一个返回 function 的 action, 这个返回的 function 接收 dispatch 作为参数,在进行了异步操作之后, dispatch 一个 type 为了 ADD 的 action, 这个action 会被 reducer 捕捉然后更新 store. 一句话来总结 redux-thunk 的作用,那就是: 让 store 可以 dispatch 一个 function, 而不仅仅是 plain object.
对于简单的应用来, redux-thunk 也许可以完全胜任我们对于异步 action 的需求,但是它有着很明显的问题:
- actions 不在仅仅是一些返回 plain object 的function.
- dispatch 散落在不同的地方
- 测试变得复杂
是否可以有一种更友好进步的方式来管理异步 action 呢,所有 dispatch 出来的 action, 实际上都是广播形式,所有 store 的 listeners 都可以监听到,那么我们就可以让中间件去处理集中管理所有的异步操作, 等到特定结果的时候才 dispatch 相应的结果 action 出来,reducer 监听特定的 actions 去做 state 的更新操作, 这就是 redux-saga 这个第三方中间件的作用.
saga.js
import { takeEvery } from 'redux-saga'
import { put } from 'redux-saga/effects'
function* addWorker() {
const value = yield call([fetchValue]);
yield put({ type: 'ADD_SUCCESS', value });
}
function* addWatcher() {
yield* takeEvery('ADD', addWorker);
}
export default addWatcher;
main.js
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(addWatcher);
这个代码,可以让我们处理逻辑就变得十分清晰.
- watcher 监听目标 actions, 然后分发到相应的 worker 去执行
- worker 负责真正的异步操作,完成之后发布特定的 action 到 reducer
- actions 全部都是 plain object
而且我们的代码非常容易进行测试,actions 全是 plain object, 表征应用的行为,而 saga 负责异步调用, 其测试只需要测试 watcher 是否能正确监听 actions, 然后分发到正确的 worker, 而 worker 是否完成异步操作之后发布正确的 action.
当然这样做有一个不好的地方就是,所有的 action,都会同时被 saga 和 reducer 所监听,所以当你的 actions 设计不当,容易造成循环依赖。这个问题曾经在我的项目出现过一次,debug 起来较为麻烦, 所以在设计 actions 的时候一定要区分好那些 action 是 reducer 去 handle, 而那些 actions 是 saga 去 handle.
测试
其实关于 redux-saga 的测试,估计很少有人写,因为其实用 redux-saga 的其实应该不多, 更多人应该用 redux-thunk, 但其实 redux-thunk 很多人也没有写测试吧, 让我们来看看如何优雅的写好 redux-saga 的单元测试吧。
function constructExpectCallReturn(func, args) {
return (
{
'@@redux-saga/IO': true,
CALL: {
context: {
server: 'http://localhost:3000',
baseURL: 'http://localhost:3000/v1',
},
fn: func,
args,
},
}
);
}
describe('watcher', () => {
const action = { type: 'ADD' };
it('should take on ADD action ', () => {
actualYield = addWatcher.next().value;
expectedYield = take(ADD, addWorker);
expect(expectedYield).to.eql(actualYield);
});
it('should fork the saga handler with action', () => {
expectedYield = fork(addWorker, action);
actualYield = addWatcher.next(action).value;
expect(expectedYield).to.eql(actualYield);
});
it('should return to capturing the FETCH action again', () => {
actualYield = addWatcher.next().value;
expectedYield = take(ADD, addWorker);
expect(actualYield).to.eql(expectedYield);
});
});
describe('worker', () => {
it('should handle add', () => {
const action = { type: ADD };
const addIterator = addWorker(action);
expectedYield = constructExpectCallReturn(fetchValue, []);
actualYield = addIterator.next().value;
expect(expectedYield).to.eql(actualYield);
});
});
可以看到我们的代码完全是可测试, 而且责任分的十分清楚,模块化程度十分高。 上面的代码都是在非 React 的应用中,可以看出我的初衷,也就是 Redux 其实适用于所有的 JavaScript 应用,当然在 React 中,更是缺之不可啊,当然用这些优秀工具的同时,我们更重要的是理解其中的理念,知道这样做,也要知道为什么需要这样做,这样我们才能在没有这些东西的时候,我们如何解构我们的代码,甚至是做出自己的类 Redux, 类 redux-thunk, redux-saga 的工具。