redux-saga 初识和使用


Posted in Javascript onMarch 10, 2018

redux-saga 是一个管理 Redux 应用异步操作的中间件,功能类似redux-thunk + async/await, 它通过创建 Sagas 将所有的异步操作逻辑存放在一个地方进行集中处理。

redux-saga 的 effects

redux-saga中的 Effects 是一个纯文本 JavaScript 对象,包含一些将被 saga middleware 执行的指令。这些指令所执行的操作包括如下三种:

  1. 发起一个异步调用(如发一起一个 Ajax 请求)
  2. 发起其他的 action 从而更新 Store
  3. 调用其他的 Sagas

Effects 中包含的指令有很多,具体可以异步API 参考进行查阅

redux-saga 的特点

方便测试,例如:

assert.deepEqual(iterator.next().value, call(Api.fetch, '/products'))
  1. action 可以保持其纯净性,异步操作集中在 saga 中进行处理
  2. watch/worker(监听->执行) 的工作形式
  3. 被实现为 generator
  4. 对含有复杂异步逻辑的应用场景支持良好
  5. 更细粒度地实现异步逻辑,从而使流程更加清晰明了,遇到 bug 易于追踪和解决。
  6. 以同步的方式书写异步逻辑,更符合人的思维逻辑
  7. 从 redux-thunk 到 redux-saga

假如现在有一个场景:用户在登录的时候需要验证用户的 username 和 password 是否符合要求。

使用 redux-thunk 实现

获取用户数据的逻辑(user.js):

// 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 });
  }
}

验证登录的逻辑(login.js):

import request from 'axios';
import { loadUserData } from './user';

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 });
  }
}

redux-saga

异步逻辑可以全部写进 saga.js 中:

export function* loginSaga() {
 while(true) {
  const { user, pass } = yield take(LOGIN_REQUEST) //等待 Store 上指定的 action LOGIN_REQUEST
  try {
   let { data } = yield call(loginRequest, { user, 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(userRequest, `/users/${uid}`);
  yield put({ type: USERDATA_SUCCESS, data });
 } catch(error) {
  yield put({ type: USERDATA_ERROR, error });
 }
}

难点解读

对于 redux-saga, 还是有很多比较难以理解和晦涩的地方,下面笔者针对自己觉得比较容易混淆的概念进行整理:

take 的使用

take 和 takeEvery 都是监听某个 action, 但是两者的作用却不一致,takeEvery 是每次 action 触发的时候都响应,而 take 则是执行流执行到 take 语句时才响应。takeEvery 只是监听 action, 并执行相对应的处理函数,对何时执行 action 以及如何响应 action 并没有多大的控制权,被调用的任务无法控制何时被调用,并且它们也无法控制何时停止监听,它只能在每次 action 被匹配时一遍又一遍地被调用。但是 take 可以在 generator 函数中决定何时响应一个 action 以及 响应后的后续操作。
例如在监听所有类型的 action 触发时进行 logger 操作,使用 takeEvery 实现如下:

import { takeEvery } from 'redux-saga'

function* watchAndLog(getState) {
 yield* takeEvery('*', function* logger(action) {
   //do some logger operation //在回调函数体内
 })
}

使用 take 实现如下:

import { take } from 'redux-saga/effects'

function* watchAndLog(getState) {
 while(true) {
  const action = yield take('*')
  //do some logger operation //与 take 并行 
 })
}

其中 while(true) 的意思是一旦到达流程最后一步(logger),通过等待一个新的任意的 action 来启动一个新的迭代(logger 流程)。

阻塞和非阻塞

call 操作是用来发起异步操作的,对于 generator 来说,call 是阻塞的操作,它在 Generator 调用结束之前不能执行或处理任何其他事情。,但是 fork 却是非阻塞操作,当 fork 调动任务时,该任务会在后台执行,此时的执行流可以继续往后面执行而不用等待结果返回。

例如如下的登录场景:

function* loginFlow() {
 while(true) {
  const {user, password} = yield take('LOGIN_REQUEST')
  const token = yield call(authorize, user, password)
  if(token) {
   yield call(Api.storeItem({token}))
   yield take('LOGOUT')
   yield call(Api.clearItem('token'))
  }
 }
}

若在 call 在去请求 authorize 时,结果未返回,但是此时用户又触发了 LOGOUT 的 action,此时的 LOGOUT 将会被忽略而不被处理,因为 loginFlow 在 authorize 中被堵塞了,没有执行到 take('LOGOUT')那里

同时执行多个任务

如若遇到某个场景需要同一时间执行多个任务,比如 请求 users 数据 和 products 数据, 应该使用如下的方式:

import { call } from 'redux-saga/effects'
//同步执行
const [users, products] = yield [
 call(fetch, '/users'),
 call(fetch, '/products')
]

//而不是
//顺序执行
const users = yield call(fetch, '/users'),
   products = yield call(fetch, '/products')

当 yield 后面是一个数组时,那么数组里面的操作将按照 Promise.all 的执行规则来执行,genertor 会阻塞知道所有的 effects 被执行完成

源码解读

在每一个使用 redux-saga 的项目中,主文件中都会有如下一段将 sagas 中间件加入到 Store 的逻辑:

const sagaMiddleware = createSagaMiddleware({sagaMonitor})
const store = createStore(
 reducer,
 applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

其中 createSagaMiddleware 是 redux-saga 核心源码文件 src/middleware.js 中导出的方法:

export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) {
 ...
 
 function sagaMiddleware({ getState, dispatch }) {
  const channel = stdChannel()
  channel.put = (options.emitter || identity)(channel.put)

  sagaMiddleware.run = runSaga.bind(null, {
   context,
   channel,
   dispatch,
   getState,
   sagaMonitor,
   logger,
   onError,
   effectMiddlewares,
  })

  return next => action => {
   if (sagaMonitor && sagaMonitor.actionDispatched) {
    sagaMonitor.actionDispatched(action)
   }
   const result = next(action) // hit reducers
   channel.put(action)
   return result
  }
 }
 ...
 
 }

这段逻辑主要是执行了 sagaMiddleware(),该函数里面将 runSaga 赋值给 sagaMiddleware.run 并执行,最后返回 middleware。 接着看 runSaga() 的逻辑:

export function runSaga(options, saga, ...args) {
...
 const task = proc(
  iterator,
  channel,
  wrapSagaDispatch(dispatch),
  getState,
  context,
  { sagaMonitor, logger, onError, middleware },
  effectId,
  saga.name,
 )

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

 return task
}

这个函数里定义了返回了一个 task 对象,该 task 是由 proc 产生的,移步 proc.js:

export default function proc(
 iterator,
 stdChannel,
 dispatch = noop,
 getState = noop,
 parentContext = {},
 options = {},
 parentEffectId = 0,
 name = 'anonymous',
 cont,
) {
 ...
 const task = newTask(parentEffectId, name, iterator, cont)
 const mainTask = { name, cancel: cancelMain, isRunning: true }
 const taskQueue = forkQueue(name, mainTask, end)
 
 ...
 
 next()
 
 return task

 function next(arg, isErr){
 ...
   if (!result.done) {
    digestEffect(result.value, parentEffectId, '', next)
   } 
 ...
 }
}

其中 digestEffect 就执行了 effectTriggerd() 和 runEffect(),也就是执行 effect,其中 runEffect() 中定义了不同 effect 执行相对应的函数,每一个 effect 函数都在 proc.js 实现了。

除了一些核心方法之外,redux-saga 还提供了一系列的 helper 文件,这些文件的作用是返回一个类 iterator 的对象,便于后续的遍历和执行, 在此不具体分析。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript比较文档位置
Apr 08 Javascript
js cookies 常见网页木马挂马代码 24小时只加载一次
Apr 13 Javascript
JavaScript window.document的属性、方法和事件小结
Oct 24 Javascript
js判断60秒以及倒计时示例代码
Jan 24 Javascript
jQuery满屏焦点图左右滚动特效代码分享
Sep 07 Javascript
JavaScript编程中实现对象封装特性的实例讲解
Jun 24 Javascript
jQuery Validate让普通按钮触发表单验证的方法
Dec 15 Javascript
Bootstrap栅格系统使用方法及页面调整变形的解决方法
Mar 10 Javascript
bootstrap table实现单击单元格可编辑功能
Mar 28 Javascript
图文介绍Vue父组件向子组件传值
Feb 17 Javascript
Angular-UI Bootstrap组件实现警报功能
Jul 16 Javascript
在vue中使用SockJS实现webSocket通信的过程
Aug 29 Javascript
JS获取input[file]的值并显示在页面的实现方法
Mar 09 #Javascript
vue获取当前点击的元素并传值的实例
Mar 09 #Javascript
vue.js获得当前元素的文字信息方法
Mar 09 #Javascript
vue element-ui 绑定@keyup事件无效的解决方法
Mar 09 #Javascript
jquery点击回车键实现登录效果并默认焦点的方法
Mar 09 #jQuery
在vue里面设置全局变量或数据的方法
Mar 09 #Javascript
vue2 全局变量的设置方法
Mar 09 #Javascript
You might like
yii的CURD操作实例详解
2014/12/04 PHP
Windows下php+mysql5.7配置教程
2017/05/16 PHP
PHP PDOStatement::execute讲解
2019/01/31 PHP
PHP如何防止用户重复提交表单
2020/12/09 PHP
javascript基础知识大集锦(二) 推荐收藏
2011/01/13 Javascript
使用Grunt.js管理你项目的应用说明
2013/04/24 Javascript
JavaScript中圆括号()和方括号[]的特殊用法疑问解答
2013/08/06 Javascript
jQuery让控件左右移动的三种实现方法
2013/09/08 Javascript
jquery easyui中treegrid用法的简单实例
2014/02/18 Javascript
javascript学习笔记(六)数据类型和JSON格式
2014/10/08 Javascript
jquery实现具有嵌套功能的选项卡
2016/02/12 Javascript
Angular 中 select指令用法详解
2016/09/29 Javascript
JS原型继承四步曲及原型继承图一览
2017/11/28 Javascript
JavaScript多态与封装实例分析
2018/07/27 Javascript
vuejs2.0运用原生js实现简单拖拽元素功能
2020/08/21 Javascript
如何从0开始用node写一个自己的命令行程序
2018/12/29 Javascript
微信小程序Echarts覆盖正常组件问题解决
2019/07/13 Javascript
jQuery实现判断滚动条滚动到document底部的方法分析
2019/08/27 jQuery
javascript用defineProperty实现简单的双向绑定方法
2020/04/03 Javascript
python搭建简易服务器分析与实现
2012/12/15 Python
Python通过解析网页实现看报程序的方法
2014/08/04 Python
Python函数可变参数定义及其参数传递方式实例详解
2015/05/25 Python
python图像处理之反色实现方法
2015/05/30 Python
举例讲解Python中的身份运算符的使用方法
2015/10/13 Python
Python搭建FTP服务器的方法示例
2018/01/19 Python
python reverse反转部分数组的实例
2018/12/13 Python
html5的canvas元素使用方法介绍(画矩形、画折线、圆形)
2014/04/14 HTML / CSS
个人工作主要事迹
2014/05/08 职场文书
爱与责任演讲稿
2014/05/20 职场文书
领导干部群众路线教育实践活动剖析材料
2014/10/10 职场文书
学生考试舞弊检讨书
2015/01/01 职场文书
2015年小学校长工作总结
2015/05/19 职场文书
公司新员工欢迎词
2015/09/30 职场文书
党员干部学习十八届五中全会精神心得体会
2016/01/05 职场文书
react使用antd的上传组件实现文件表单一起提交功能(完整代码)
2021/06/29 Javascript
Vue实现跑马灯样式文字横向滚动
2021/11/23 Vue.js