dva effects 进阶

这是去年写的一篇总结了,一直没搬到我的博客上来,最近才想起。

本文相关代码 https://github.com/wave52/react-practise/blob/master/src/redux/dvaApp/index.js

问题:

  1. 使用最新的请求结果
  2. 取消一个耗时很长的请求(手动/超时自动)
  3. 轮询(循环定时发起)
  4. 排队请求(循环排队发起)
  5. 跨 model 时序问题

分析:

一.redux-saga

1.redux-saga是个中间件

那源码是否可以找到 redux-thunk 这样的中间件写法?(redux-thunk 源码简析之前文章做过了,14 行代码很简单)

可以,就在 /packages/core/src/internal/middleware.js(0.16.0 src/internal/middleware.js)

sagaMiddlewareFactory 这个函数等同于 createThunkMiddleware

其主要逻辑是

1
2
3
4
5
6
7
8
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
channel.put(action)
return result
}

使用时,对比thunk:

1
2
3
4
5
// thunk
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
1
2
3
4
// saga
const sagaMiddleware = createSagaMiddleware()
createStore(rootReducer, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga)

上面的 thunk 中间件这样用以后,变化的是 reducer,本来 action 是一个 plainObject,现在的 action 可以是个方法了;而用了 saga 中间件以后,之前的 reducer 没有变化,action 还是一个 plainObject 表示 state 的变化,而异步的都放在 sagas(上面的 rootSaga)中处理了,至于 channel.put(action) 可以理解成 channel 是一个发布订阅模式,channel.put 就是发布,而 saga 中会订阅(take)

也就是说 saga 的程序逻辑会存在两个位置:

  • Reducers 负责处理 action 的 state 转换
  • Sagas 负责策划统筹合成和异步操作

源码中也能体现 const result = next(action),不像 thunk 还做了拦截处理,而 saga 的处理其实是在 run 函数,
run 函数源码对应 runSaga/packages/core/src/internal/runSaga.js

1
const iterator = saga(...args)

这里 saga 的 args 参数可能有各种情况,本例 saga 对应 rootSaga,...args 是空的。

确实,rootSaga 是一个 generator,他执行后返回一个 IterableIterator 类型的对象 iterator

1
2
3
export default function* root() {
yield all([fork(getAllProducts), fork(watchGetProducts), fork(watchCheckout)])
}

那我们看看怎么处理这个 iterator 的

1
2
3
4
5
6
7
8
9
return immediately(() => {
const task = proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, noop)

if (sagaMonitor) {
sagaMonitor.effectResolved(effectId, task)
}

return task
})

作为参数传给了 proc 函数,外面包的这层函数先不用管,这里可以看成 (()=>{proc})() 一个立即执行函数,再看看 proc,这是重点了
run(rootSaga)执行的调用栈(从proc开始,前面的都分析了)
proc()

-> next()

-> result = iterator.next(arg) - (看过 generator 应该知道此时来到了第一个 yield all(), 打印一下{value:{type: ALL,....},done: false},判断 done 还不为 true,直接打印 console.log(all()); 也会看到这个结构)

-> digestEffect()

-> finalRunEffect() - (finalRunEffect()是在runSaga中封装了runeffect,runsaga可以接受一个effectMiddlewares参数进行封装TODO1)

-> runEffect()

-> 然后去 effectRunnerMap.js

-> runAllEffect()
-> forEach 调用 digestEffect() 同上第一个是 fork
-> runForkEffect()
-> proc

当前 iterator 就变成了 getAllProducts

更多 saga 原理与实现 http://frontend.dev-ag.xxx.com/blog/2018/08/28/redux-saga.html

我们主要关注 redux-saga 的使用与 dva 的原理和实现

只需强调几个地方帮助我们理解:
1.watch-and-fork 模式
2.阻塞(take,call…)与非阻塞(fork,put…)
3.saga-helper

2.dva 封装了啥

dva 调用 run
store.runSaga = sagaMiddleware.run;
injectModel 中 store.runSaga
参数应该是 rootSaga,那 dva 中是啥
是一个 getSaga 方法 dva/packages/dva-core/src/getSaga.js
返回是一个 funtion*() 就像 rootSaga 一样但里面没有用 all() 包装,而是一个 for 循环,毕竟 all() 中也是 forEach,然后

1
2
const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts);
const task = yield sagaEffects.fork(watcher);

第一行每一个 saga 我们都用一个 sagaHelper 包装一下

第二行包装后的 saga 我们 fork 一下,就像前面的例子一样

这里就需要理解一下 sagaHelper 和 fork 的意义了

fork 是非阻塞的 call(https://redux-saga-in-chinese.js.org/docs/api/)

另外,createEffects return { ...sagaEffects, put, take }; 这里可以发现,dva 中是可以使用 redux-saga 中所有的 effects 的,不过要注意版本

3.saga helper(saga辅助函数)

saga helper 对应源码 io-helpers debounceretrytakeEverytakeLatesttakeLeadingthrottle,saga 辅助函数是由effect创建器组合而来的高级API

takeEvery 是一个使用 takefork 构建的高级 API。下面演示了这个辅助函数是如何由低级 Effect 实现的

1
2
3
4
5
6
const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {
while (true) {
const action = yield take(patternOrChannel)
yield fork(saga, ...args.concat(action))
}
})

dva 的 "redux-saga": "^0.16.0",包含的 saga helper 只有 takeEverytakeLatestthrottle,对应 sagaHelpers 这个目录

dva 中的 saga helper 作为 effects type 存在

https://blog.csdn.net/wangweiren_get/article/details/89043113#_1

getWatcher 源码中默认用 takeEvery 包裹,所以一般没有写,问题 3 就可以用takeLatest来解决

我们具体来看看每一个 saga helper 的用途

1).先看看 redux-saga 本身的

takeEvery: 每次 dispatch action 都会触发

takeLatest: 每次 dispatch action 会销毁之前的,再触发

throttle: 节流,不多解释

2).dva依赖redux-saga@0.16.x,下面这些新的saga-helper你用dva时应该没有见过:

takeLeading: 每次dispatch触发一次,当执行完了才能在被触发,中间dispatch不会触发

debounce: 防抖,不多解释

retry: 失败自动重试

3).另外由于 saga helper 就是简单的 effects 创建器组合而成的高级API,所以 dva 在封装时也自己做了几个 saga helper(在dva里叫effect type)

watcher: 其实就就是没有用 saga-helper,初始化启动,这个名字来源是由于 saga 常见模式 watch-and-fork 模式,dva 只对其做了简单的 try catch 封装

poll(2.6beta版): 顾名思义是做轮询用的(解决了开头的问题),现在我们用的是 dva@2.4 版本还没有这个 effect,可以看下他怎么做的,总之是由简单的 effect 创建器组合而成,solvay 中就自己实现过

4.effect 创建器

https://redux-saga-in-chinese.js.org/docs/basics/DeclarativeEffects.html

作用:为了方便测试,把直接调用改为使用 effect 创建器包装调用

take、fork

5.channel

本文相关代码 https://github.com/wave52/react-practise/blob/master/src/redux/dvaApp/index.js