您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
探索React异步解决方案之Redux-saga
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
探索React异步解决方案之Redux-saga
京东ZERO团队
2021-01-07
IP归属:未知
86600浏览
React
### 1.redux-saga是什么? <img src="https://img-1301192312.cos.ap-shanghai.myqcloud.com/1609228840979.jpg" alt="image-20201229144723252" style="zoom: 100%;" /> > `redux-saga` is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures. 顾名思义saga与redux相关,redux-saga是一个以redux中间件形式存在的一个库,主要是为了更优雅地 **管理** Redux 应用程序中的 **副作用(Side Effects)**,执行更高效,**测试更简单**,在处理故障时更容易。同样的,从logo也可以看出saga于redux的关系。 关于saga的由来,它出自康奈尔大学的一篇论文([链接](http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf)),是为了解决分布式系统中的长时运行事务(LLT)的数据一致性的问题。 ### 2.什么是SideEffects? > Side effects are the most common way that a program interacts with the outside world (people, filesystems, other computers on networks). 映射在 Javascript 程序中,Side Effects 主要指的就是:**异步网络请求**、**本地读取 localStorage/Cookie** 等外界操作: > Asynchronous things like **data fetching** and impure things like **accessing the browser cache** 在 Web 应用,侧重点在于 Side Effects 的**优雅管理(manage)**,而不是 **消除(eliminate)**。 ### 3.saga与thunk有什么不同? ![image-20201130161733674](https://img14.360buyimg.com/imagetools/jfs/t1/153295/25/12536/303074/5feaefbeE57140b7b/b4c204b21a069640.png) 首先,比较了saga与thunk的包体积大小,二者相差**10倍**之多。 无论是redux-thunk也好还是redux-saga也好,都是redux的中间件。而redux作为主体,为每个中间件,提供了统一格式,下发getState、dispatch,以及调用dispatch,收集action。 ```js //compose.js function compose(..funcs) { if (funcs.length === 0) { retyrb arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args) => a(b(...args))) } //applyMiddleware.js function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloaderState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, diapatch: (action) => dispatch(action) } chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } } ``` 接着,我们先再来看看thunk函数,在阮大大的文章中有介绍到thunk函数: ```js function f(m){ return m * 2; } f(x + 5); // 等同于 var thunk = function () { return x + 5; }; function f(thunk){ return thunk() * 2; } ``` 编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。**在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。** 然后我们再来看看thunk的源码 ```js function createThunkMiddleware(extraArgument) { //dispath,可以用来dispatch新的action //getState,可以用于访问当前的state return ({dispatch, getState}) => (next) => (action) => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; ``` redux-thunk是个中间件,去监控传入系统中的每一个`action`,如果是个函数的话,那么它就会调用那个函数。这就是`redux-thunk`的职责。redux-thunk 选择以 middleware 的形式来增强 redux store 的 dispatch 方法(即:支持了 `dispatch(function)`),从而在拥有了异步获取数据能力的同时,又可以进一步将数据获取相关的业务逻辑 从 View 层分离出去。 接着来看看redux-saga,saga模式是以命令/答复的形式与各个saga之间进行通讯,当接收到指令时会执行对应的saga,如图所示: ![Command/Orchestration flow](https://img10.360buyimg.com/imagetools/jfs/t1/156504/6/3016/334582/5feaefd7E3c158454/e6a84c2877935058.png) saga模式将各个服务隔离开,采用集中分布式事务的编排,能够避免服务之间的循环依赖并有利于测试。同时减少了参与者的复杂性,因为他们只需要执行/回复命令。但是,saga会产生很多无用的action.type。 综上,redux-thunk与redux-saga都是redux的中间件,但是他们的设计思想不同,因此他们的使用方法也不同,首先来看redux-thunk的写法: ```js // action.js // --------- // actionCreator(e.g. fetchData) 返回 function // function 中包含了业务数据请求代码逻辑 // 以回调的方式,分别处理请求成功和请求失败的情况 export function fetchData(someValue) { return (dispatch, getState) => { myAjaxLib.post("/someEndpoint", { data: someValue }) .then(response => dispatch({ type: "REQUEST_SUCCEEDED", payload: response }) .catch(error => dispatch({ type: "REQUEST_FAILED", error: error }); }; } // component.js // ------------ // View 层 dispatch(fn) 触发异步请求 // 这里省略部分代码 this.props.dispatch(fetchData({ hello: 'saga' })); ``` 再来看redux-saga的写法,以及架构图: ```js // saga.js // ------- // worker saga // 它是一个 generator function // fn 中同样包含了业务数据请求代码逻辑 // 但是代码的执行逻辑:看似同步 (synchronous-looking) function* fetchData(action) { const { payload: { someValue } } = action; try { const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue }); yield put({ type: "REQUEST_SUCCEEDED", payload: response }); } catch (error) { yield put({ type: "REQUEST_FAILED", error: error }); } } // watcher saga // 监听每一次 dispatch(action) // 如果 action.type === 'REQUEST',那么执行 fetchData export function* watchFetchData() { yield takeEvery('REQUEST', fetchData); } // component.js // ------- // View 层 dispatch(action) 触发异步请求 // 这里的 action 依然可以是一个 plain object this.props.dispatch({ type: 'REQUEST', payload: { someValue: { hello: 'saga' } } }); ``` ![img](https://img11.360buyimg.com/imagetools/jfs/t1/160315/2/395/226033/5feaefd7E28947969/0e7d50b3f9f5d24c.png) 综上可以看出,redux-saga相较于redux-thunk有这几点不同 1.数据获取相关的业务逻辑被转移到单独的saga.js中,不再是参杂在action.js或component.js中。 2.每一个saga都是一个generator function,代码采用同步书写的方式来处理异步逻辑,代码变得更易读。 ### 4.学习saga使用 saga总共提供了两个**MiddlewareAPI**,为createSagaMiddleware、middleware.run。 createSagaMiddleware(options): 创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。其中options支持的选项有(可不提供): - sagaMontior:用于接收middleware传递的监视事件。 - emmiter:用于从redux向redux-saga进给actions - logger:自定义日志方法(默认情况下,middleware会把所有的错误和警告记录到控制台中)。 - onError:当提供该方法时,middleware将带着Sagas中未被捕获的错误调用它。 middleware.run(saga, ...args): 动态地运行 saga。只能用于在 applyMiddleware 阶段之后执行Saga,其中args为提供给saga的参数。 在安装完所有依赖后,首先将store 与saga的关联,并在最后去执行rootsaga。 ```javascript import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import rootSaga from './sagas' import rootReducer from './reducers' const sgagMiddleware = createSagaMiddleware(); const enhancer = applyMiddleware(sagaMiddleware); const store = createStore(rootReducer, enhancer); //执行rootSaga,通常是程序的初始化操作。 sagaMiddleWare.run(rootSaga); ``` 然后,再介绍saga中比较重要的几个概念,分别为:Task、Channel、Buffer、SagaMonitor。 ##### 1.Task Task 接口指定了通过 `fork`,`middleare.run` 或 `runSaga` 运行 Saga 的结果,并提供了相应的函数方法。 ![image-20201223112205449](https://img12.360buyimg.com/imagetools/jfs/t1/155129/10/12474/62175/5feaefd7Ed2e6d52f/f912907ae77be24c.png) ![image-20201223112119677](https://img14.360buyimg.com/imagetools/jfs/t1/159848/30/348/212059/5feaefd7E69b6ab7e/f8eb9262db1e45e3.png) ##### 2.Channel channel 是用于在任务间发送和接收消息的对象。在被感兴趣的接收者请求之前,来自发送者的消息将被放入(put)队列;在信息可用之前,已注册的接收者将被放入队列。 Channel 接口定义了 3 个方法:`take`,`put` 和 `close` `Channel.take(callback):` 用于注册一个 taker。 `Channel.put(message):` 用于在 buffer 上放入消息。 `Channel.flush(callback):` 用于从 channel 中提取所有被缓存的消息。 `Channel.close():` 关闭 channel,意味着不再允许做放入操作。 ##### 3.Buffer 用于为 channel 实现缓存策略。Buffer 接口定义了 3 个方法:`isEmpty`,`put` 和 `take` - `isEmpty()`: 如果缓存中没有消息则返回。每当注册了新的 taker 时,channel 都会调用该方法。 - `put(message)`: 用于往缓存中放入新的消息。请注意,缓存可以选择不存储消息。(例如,一个 dropping buffer 可以丢弃超过给定限制的任何新消息) - `take()`:用于检索任何被缓存的消息。请注意,此方法的行为必须与 `isEmpty` 一致。 ##### 4.SagaMonitor 用于由 middleware 发起监视(monitor)事件。实际上,middleware 发起 5 个事件: - 当一个 effect 被触发时(通过 `yield someEffect`),middleware 调用 `sagaMonitor.effectTriggered` - 如果该 effect 成功地被 resolve,则 middleware 调用 `sagaMonitor.effectResolved` - 如果该 effect 因一个错误被 reject,则 middleware 调用 `sagaMonitor.effectRejected` - 如果该 effect 被取消,则 middleware 调用 `sagaMonitor.effectCancelled` - 最后,当 Redux action 被发起时,middleware 调用 `sagaMonitor.actionDispatched` 接着再来介绍redux-saga中的Effect创建器,在redux-saga中主要通过effect来维护,关于Effect的描述如下: > An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware. effect 本质上是一个普通对象,包含着一些指令信息,这些指令最终会被 saga middleware 解释并执行(实际上是一个发布订阅模式)。源码解析可参考文章(https://juejin.cn/post/6885223002703822855#heading-5) 以take为例,take是一个Effect创建器,用以创建Effect,源码如下: ![image-20201223111205002](https://img14.360buyimg.com/imagetools/jfs/t1/153794/22/12527/412336/5feaefd8E87caf8dc/665203ea5e8a2206.png) ![image-20201223111034645](https://img10.360buyimg.com/imagetools/jfs/t1/159903/31/390/86966/5feaefd8Ed8f9d2b2/4c646e71e3077a88.png) 官方解释: - 以下每个Effect创建函数都会返回一个普通 Javascript 对象(plain JavaScript object),并且不会执行任何其它操作。 - 执行是由 middleware 在上述迭代过程中进行的。 - middleware 会检查每个 Effect 的描述信息,并进行相应的操作 接下去简单解释一下各个Effect创建器以及Effect组合器、辅助函数的作用: Take: 创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action。 在发起与 `pattern` 匹配的 action 之前,Generator 将暂停。 Put: 创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。 Call: 创建一个 Effect 描述信息,用来命令 middleware 以参数 `args` 调用函数 `fn` 。 Apply: 类似Call。 Fork: 创建一个 Effect 描述信息,用来命令 middleware 以 **非阻塞调用** 的形式执行 `fn`。 Spawn: 与fork类似,但创建的是被分离的任务。被分离的任务与其父级任务保持独立。 Join: 创建一个 Effect 描述信息,用来命令 middleware 等待之前的一个分叉任务的结果。 Cancel:创建一个 Effect,用以取消任务。 Select: 创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器(即返回 selector(getState(), ...args) 的结果)。 ActionChannel: 创建一个 Effect,用来命令 middleware 通过一个事件 channel 对匹配 `pattern` 的 action 进行排序。 Flush: 创建一个 Effect,用来命令 middleware 从 channel 中冲除所有被缓存的数据。被冲除的数据会返回至 saga,这样便可以在需要的时候再次被利用。 Cancelled: 创建一个 Effect,用来命令 middleware 返回该 generator 是否已经被取消。 setContext: 创建一个 effect,用来命令 middleware 更新其自身的上下文。 getContext: 创建一个 effect,用来命令 middleware 返回 saga 的上下文中的一个特定属性。 ##### Effect组合器 Race: 创建一个 Effect 描述信息,用来命令 middleware 在多个 Effect 间运行 竞赛(Race)(与 Promise.race([...]) 的行为类似)。 All: 创建一个 Effect 描述信息,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成。这是与标准的 Promise#all 相当对应的 API。 ##### Saga辅助函数 TakeEvery: 在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。 TakeLatest: 在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。并自动取消之前所有已经启动但仍在执行中的 saga 任务。 TakeLeading: 在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。 它将在派生一次任务之后阻塞,直到派生的 saga 完成,然后又再次开始监听指定的 pattern。 Throttle: 在发起到 Store 并且匹配 pattern 的一个 action 上派生一个 saga。 它在派生一次任务之后,仍然将新传入的 action 接收到底层的 buffer 中,至多保留(最近的)一个。但与此同时,它在 ms 毫秒内将暂停派生新的任务 —— 这也就是它被命名为节流阀(throttle)的原因。其用途,是在处理任务时,无视给定的时长内新传入的 action。 ### 5.Redux-Saga测试 由于redux-saga将每个副作用细化到一个较小的维度,并使各个服务之间的耦合性较小。因此非常利于进行单元测试,案例如下: ```javascript function* callApi(url) { const someValue = yield select(somethingFromState) try { const result = yield call(myApi, url, someValue) yield put(success(result.json())); return result.status; } catch (e) { yield put(error(e)); return -1; } } ``` ```javascript const dispatched = []; const saga = runSaga({ dispatch: (action) => dispatched.push(action), getState: () => ({ value: 'test' }), }, callApi, 'http://url'); ``` ```javascript import sinon from 'sinon'; import * as api from './api'; test('callApi', async (assert) => { const dispatched = []; sinon.stub(api, 'myApi').callsFake(() => ({ json: () => ({ some: 'value' }) })); const url = 'http://url'; const result = await runSaga({ dispatch: (action) => dispatched.push(action), getState: () => ({ state: 'test' }), }, callApi, url).done; assert.true(myApi.calledWith(url, somethingFromState({ state: 'test' }))); assert.deepEqual(dispatched, [success({ some: 'value' })]); }); ``` 最后再推荐两个,阅读官方文档后觉得比较好的小技巧的使用。 ### 6.Redux-Saga使用技巧 ##### 1.ajax重试 ```javascript import { call, put, take, delay, delay } from 'redux-saga/effects' function* updateApi(data) { while (true) { try { const apiResponse = yield call (apiRequest, { data }) return apiResponse; } catch(error) { yield put({ type: 'UPDATE_RETRY', error }) yield delay(2000) } } } function* updateResource({ data }) { const apiResponse = yield call(updateApi, data); yield put({ type: 'UPDATE_SUCCESS', payload: apiResponse.body, }); } export function* watchUpdateResource() { yield takeLatest('UPDATE_START', updateResource); } ``` ##### 2.撤销 ```javascript import { take, put, call, spawn, race, delay } from 'redux-saga/effects' import { updateThreadApi, actions } from 'somewhere' function* onArchive(action) { const { threadId } = action const undoId =`UNDO_ARCHIVE_${threadId}` const thread = { id: threadId, archived: true} yield put(actions.showUndo(undoId)) yield put(actions.updateThread(thread)) const { undo, archive } = yield race({ undo: take(action => action.type === 'UNDO' && action.undoId === undoId), archive: delay(5000) }) yield put(actions.hideUndo(undoId)) if (undo) { yield put(actions.updateThread({ id: threadId, archived: false})) } else if (archive) { yield call(updateThreadApi,thread) } } function* main() { while (true) { const action = yield take(`ARCHIVE_THREAD`) yield spawn(onArchive, action) } } ``` ##### 参考文章: 1.[Redux-Saga 漫谈](https://zhuanlan.zhihu.com/p/35437092) 2.[Saga Pattern](https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part-2/) 3.[Redux-Saga官方文档](https://redux-saga-in-chinese.js.org/) 4.[Why saga](https://juejin.cn/post/6844903984038150152#heading-4) 5.[手写Redux-Saga源码](https://juejin.cn/post/6885223002703822855#heading-5)
原创文章,需联系作者,授权转载
上一篇:浅谈ES6之class
下一篇:Web 字体 font-family 浅谈
相关文章
【技术干货】企业级扫描平台EOS关于JS扫描落地与实践!
开发也要防沉迷--IDEA插件教程
京东mPaaS平台之Android组件化系统私有化部署改造实践!
京东ZERO团队
文章数
39
阅读量
91754
作者其他文章
01
webpack打包组件配置(React版本)
这篇文章是以打包react插件的形式,介绍webpack的一些配置信息。如果写简单插件的话还是推荐使用rollup,但是可以用写插件的形式去学习一下webpack的一些东西。(适用于初中级webpack学者)
01
webpack核心概念与基本实现
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。**
01
Typescript合成Webpack中
TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript,简称ts。相对于ES6,TypeScript最大的改善是增加了类型系统,国内外很多大型工程都用它,如AngularJs,白鹭引擎、Antd。
01
小程序加载svg图片
小程序的[组件](https://developers.weixin.qq.com/miniprogram/dev/component/)中是没有支持`SVG`标签的。 但是在前端小伙伴的实际开发中,UED经常提供SVG图片过来,如果不想用引入`iconfont`的话,那么妹子我将介绍个很好用的方法。
最新回复
丨
点赞排行
共0条评论
京东ZERO团队
文章数
39
阅读量
91754
作者其他文章
01
webpack打包组件配置(React版本)
01
webpack核心概念与基本实现
01
Typescript合成Webpack中
01
小程序加载svg图片
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号