3.4 redux 异步
在大多数的前端业务场景中,需要和后端产生异步交互,在本节中,将详细讲解 redux 中的异步方案以及一些异步第三方组件,内容有:
- redux 异步流 
- redux-thunk 
- redux-promise 
- redux-saga 
3.4.1 redux 异步流
redux 异步流
redux-thunk
redux-promise
redux-saga
前面讲的 redux 中的数据流都是同步的,流程如下:
view -> actionCreator -> action -> reducer -> newState -> container component 
但同步数据不能满足真实业务开发,真实业务中异步才是主角,那如何将异步处理结合到上边的流程中呢?
3.4.2 实现异步的方式
其实 redux 并未有和异步相关的概念,我们可以用任何原来实现异步的方式应用到 redux 数据流中,最简单的方式就是延迟 dispatch action,以 setTimeout 为例:
this.dispatch({ type: 'SYNC_SOME_ACTION'})
window.setTimeout(() => {
  this.dispatch({ type: 'ASYNC_SOME_ACTION' })
}, 1000)这种方式最简单直接,但是有如下问题:
- 如果有多个类似的 action 触发场景,异步逻辑不能重用 
- 异步处理代码不能统一处理,最简单的例子就是节流 
解决上面两个问题的办法很简单,把异步的代码剥离出来:
someAction.js
function dispatchSomeAction(dispatch, payload) {
    // ..调用控制逻辑...
    dispatch({ type: 'SYNC_SOME_ACTION'})
    window.setTimeout(() => {
      dispatch({ type: 'ASYNC_SOME_ACTION' })
    }, 1000)
}然后组件只需要调用:
import {dispatchSomeAction} from 'someAction.js'
dispatchSomeAction(dispatch, payload);基于这种方式上面的流程就改为了:
view -> asyncActionDispatcher -> wait -> action -> reducer -> newState -> container component
asyncActionDispatcher 和 actionCreator 是十分类似的, 所以简单而言就可以把它理解为 asyncActionCreator , 所以新的流程为:
view -> asyncActionCreator -> wait -> action -> reducer -> newState -> container component
但是上面的方法有一些缺点
同步调用和异步调用的方式不相同:
- 同步的情况: - store.dispatch(actionCreator(payload))
- 异步的情况: - asyncActionCreator(store.dispatch, payload)
幸运的是在 redux 中通过 middleware 机制可以很容易的解决上面的问题
通过 middleware 实现异步
我们已经很清楚一个 middleware 的结构 ,其核心的部分为
function(action) {
    // 调用后面的 middleware
    next(action)
}middleware 完全掌控了 reducer 的触发时机, 也就是 action 到了这里完全由中间件控制,不乐意就不给其他中间件处理的机会,而且还可以控制调用其他中间件的时机。
举例来说一个异步的 ajax 请求场景,可以如下实现:
function (action) {
    // async call
    fetch('....')
      .then(
          function resolver(ret) {
            newAction = createNewAction(ret, action)
            next(newAction)
          },
          function rejector(err) {
            rejectAction = createRejectAction(err, action)
            next(rejectAction)
          })
    });
}任何异步的 javascript 逻辑都可以,如: ajax callback, Promise, setTimeout 等等, 也可以使用 es7 的 async 和 await。
第三方异步组件
上面的实现方案只是针对具体的场景设计的,那如果是如何解决通用场景下的问题呢,其实目前已经有很多第三方 redux 组件支持异步 action,其中如:
这些组件都有很好的扩展性,完全能满足我们开发异步流程的场景,下面来一一介绍
3.4.3 redux-thunk
redux-thunk 介绍
redux-thunk 是 redux 官方文档中用到的异步组件,实质就是一个 redux 中间件,thunk 听起来是一个很陌生的词语,先来认识一下什么叫 thunk
A thunk is a function that wraps an expression to delay its evaluation.
简单来说一个 thunk 就是一个封装表达式的函数,封装的目的是延迟执行表达式
// 1 + 2 立即被计算 = 3let x = 1 + 2;
// 1 + 2 被封装在了 foo 函数内// foo 可以被延迟执行// foo 就是一个 thunk let foo = () => 1 + 2;redux-thunk 是一个通用的解决方案,其核心思想是让 action 可以变为一个 thunk ,这样的话:
- 同步情况:dispatch(action) 
- 异步情况:dispatch(thunk) 
我们已经知道了 thunk 本质上就是一个函数,函数的参数为 dispatch, 所以一个简单的 thunk 异步代码就是如下:
this.dispatch(function (dispatch){
    setTimeout(() => {
       dispatch({type: 'THUNK_ACTION'})
    }, 1000)
})之前已经讲过,这样的设计会导致异步逻辑放在了组件中,解决办法为抽象出一个 asyncActionCreator, 这里也一样,我们就叫 thunkActionCreator 吧,上面的例子可以改为:
//actions/someThunkAction.js
export function createThunkAction(payload) {
    return function(dispatch) {
        setTimeout(() => {
           dispatch({type: 'THUNK_ACTION', payload: payload})
        }, 1000)
    }
}
// someComponent.jsthis.dispatch(createThunkAction(payload))安装和使用
第一步:安装
$ npm install redux-thunk第二步: 添加 thunk 中间件
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);第三步:实现一个 thunkActionCreator
//actions/someThunkAction.js
export function createThunkAction(payload) {
    return function(dispatch) {
        setTimeout(() => {
           dispatch({type: 'THUNK_ACTION', payload: payload})
        }, 1000)
    }
}第三步:组件中 dispatch thunk
this.dispatch(createThunkAction(payload));拥有 dispatch 方法的组件为 redux 中的 container component
thunk 源码
说了这么多,redux-thunk 是不是做了很多工作,实现起来很复杂,那我们来看看 thunk 中间件的实现
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;就这么简单,只有 14 行源码,但是这简短的实现却能完成复杂的异步处理,怎么做到的,我们来分析一下:
- 判断如果 action 是 function 那么执行 action(dispatch, getState, ...) - action 也就是一个 thunk 
- 执行 action 相当于执行了异步逻辑 - action 中执行 dispatch 
- 开始新的 redux 数据流,重新回到最开始的逻辑(thunk 可以嵌套的原因) 
 
- 把执行的结果作为返回值直接返回 
- 直接返回并没有调用其他中间件,也就意味着中间件的执行在这里停止了 
- 可以对返回值做处理(后面会讲如果返回值是 Promise 的情况) 
 
- 如果不是函数直接调用其他中间件并返回 
理解了这个过后是不是对 redux-thunk 的使用思路变得清晰了
thunk 的组合
根据 redux-thunk 的特性,可以做出很有意思的事情
- 可以递归的 dispatch(thunk) => 实现 thunk 的组合; 
- thunk 运行结果会作为 dispatch返回值 => 利用返回值为 Promise 可以实现多个 thunk 的编排; 
thunk 组合例子:
function thunkC() {
    return function(dispatch) {
        dispatch(thunkB())
    }
}
function thunkB() {
    return function (dispatch) {
        dispatch(thunkA())
    }
}
function thunkA() {
    return function (dispatch) {
        dispatch({type: 'THUNK_ACTION'})
    }
}Promise 例子
function ajaxCall() {
    return fetch(...);
}
function thunkC() {
    return function(dispatch) {
        dispatch(thunkB(...))
        .then(
            data => dispatch(thunkA(data)),
            err  => dispatch(thunkA(err))
        )
    }
}
function thunkB() {
    return function (dispatch) {
        return ajaxCall(...)
    }
}
function thunkA() {
    return function (dispatch) {
        dispatch({type: 'THUNK_ACTION'})
    }
}3.4.4 redux-promise
另外一个 redux 文档中提到的异步组件为 redux-promise, 我们直接分析一下其源码吧
import { isFSA } from 'flux-standard-action';
function isPromise(val) {
  return val && typeof val.then === 'function';
}
export default function promiseMiddleware({ dispatch }) {
  return next => action => {
    if (!isFSA(action)) {
      return isPromise(action)
        ? action.then(dispatch)
        : next(action);
    }
    return isPromise(action.payload)
      ? action.payload.then(
          result => dispatch({ ...action, payload: result }),
          error => {
            dispatch({ ...action, payload: error, error: true });
            return Promise.reject(error);
          }
        )
      : next(action);
  };
}大概的逻辑就是:
- 如果不是标准的 flux action,那么判断是否是 promise, 是执行 action.then(dispatch),否执行 next(action) 
- 如果是标准的 flux action, 判断 payload 是否是 promise,是的话 payload.then 获取数据,然后把数据作为 payload 重新 dispatch({ ...action, payload: result}) , 否执行 next(action) 
结合 redux-promise 可以利用 es7 的 async 和 await 语法,简化异步的 promiseActionCreator 的设计, eg:
export default async (payload) => {
  const result = await somePromise;
  return {
    type: "PROMISE_ACTION",
    payload: result.someValue;
  }
}如果对 es7 async 语法不是很熟悉可以看下面两个例子:
- async 关键字可以总是返回一个 Promise 的 resolve 结果或者 reject 结果 
async function foo() {
    if(true)
        return 'Success!';
    elsethrow 'Failure!';
}
// 等价于function foo() {
    if(true)
        return Promise.resolve('Success!');
    elsereturn Promise.reject('Failure!');
}
- 在 async 关键字中可以使用 await 关键字,其目的是 await 一个 promise, 等待 promise resolve 和 reject 
eg:
async function foo(aPromise) {
    const a = await new Promise(function(resolve, reject) {
            // This is only an example to create asynchronismwindow.setTimeout(
                function() {
                    resolve({a: 12});
                }, 1000);
        })
    console.log(a.a)
    return  a.a
}
// in console
> foo()
> Promise {_c: Array[0], _a: undefined, _s: 0, _d: false, _v: undefined…}
> 12可以看到在控制台中,先返回了一个 promise,然后输出了 12
async 关键字可以极大的简化异步流程的设计,避免 callback 和 thennable 的调用,看起来和同步代码一致。
3.4.5 redux-saga
redux-saga 介绍
redux-saga 也是解决 redux 异步 action 的一个中间件,不过和之前的设计有本质的不同
- redux-saga 完全基于 Es6 的 Generator Function 
- 不使用 actionCreator 策略,而是通过监控 action, 然后在自动做处理 
- 所有带副作用的操作(异步代码,不确定的代码)都被放到 saga 中 
那到底什么是 saga
redux-saga 实际也没有解释什么叫 saga ,通过引用的参考:
The term saga is commonly used in discussions of CQRS to refer to a piece of code that coordinates and routes messages between bounded contexts and aggregates.
这个定义的核心就是 CQRS-查询与责任分离 ,对应到 redux-sage 就是 action 与 处理函数的分离。 实际上在 redux-saga 中,一个 saga 就是一个 Generator 函数。
eg:
import { takeEvery, takeLatest } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
import Api from '...'/*
 * 一个 saga 就是一个 Generator Function
 *
 * 每当 store.dispatch `USER_FETCH_REQUESTED` action 的时候都会调用 fetchUser.
 */function* mySaga() {
  yield* takeEvery("USER_FETCH_REQUESTED", fetchUser);
}
/**
 * worker saga: 真正处理 action 的 saga
 *
 * USER_FETCH_REQUESTED action 触发时被调用
 * @param {[type]} action  [description]
 * @yield {[type]} [description]
 */function* fetchUser(action) {
   try {
      const user = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", user: user});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}
一些基本概念
watcher saga
负责编排和派发任务的 saga
worker saga
真正负责处理 action 的函数
saga helper
如上面例子中的 takeEvery,简单理解就是用于监控 action 并派发 action 到 worker saga 的辅助函数
Effect
redux-saga 完全基于 Generator 构建,saga 逻辑的表达是通过 yield javascript 对象来实现,这些对象就是Effects。
这些对象相当于描述任务的规范化数据(任务如执行异步函数,dispatch action 到一个 store),这些数据被发送到 redux-saga 中间件中执行,如:
- put({type: "USER_FETCH_SUCCEEDED", user: user})表示要执行- dispatch({{type: "USER_FETCH_SUCCEEDED", user: user}})任务
- call(fetch, url)表示要执行- fetch(url)
通过这种 effect 的抽象,可以避免 call 和 dispatch 的立即执行,而是描述要执行什么任务,这样的话就很容易对 saga 进行测试,saga 所做的事情就是将这些 effect 编排起来用于描述任务,真正的执行都会放在 middleware 中执行。
安装和使用
第一步:安装
$ npm install --save redux-saga第二步:添加 saga 中间件
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'// 创建 saga 中间件const sagaMiddleware = createSagaMiddleware()
// 添加到中间件中const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
)
// 立即运行 saga ,让监控器开始监控
sagaMiddleware.run(mySaga)
第三步:定义 sagas/index.js
import { takeEvery } from 'redux-saga'
import { put } from 'redux-saga/effects'
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
// 将异步执行 increment 任务
export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}
// 在每个 INCREMENT_ASYNC action 调用后,派生一个新的 incrementAsync 任务
export default function* watchIncrementAsync() {
  yield* takeEvery('INCREMENT_ASYNC', incrementAsync)
}第四步:组件中调用
this.dispatch({type: 'INCREMENT_ASYNC'})redux-saga 基于 Generator 有很多高级的特性, 如:
- 基于 take Effect 实现更自由的任务编排 
- fork 和 cancel 实现非阻塞任务 
- 并行任何和 race 任务 
- saga 组合 ,yield* saga 
3.4 redux 异步的更多相关文章
- 聊一聊 redux 异步流之 redux-saga
		让我惊讶的是,redux-saga 的作者竟然是一名金融出身的在一家房地产公司工作的员工(让我想到了阮老师...),但是他对写代码有着非常浓厚的热忱,喜欢学习和挑战新的事物,并探索新的想法.恩,牛逼的 ... 
- redux异步
		在一个项目中 redux 是必不可少的,redux 中没有提供异步的操作,但是异步又是项目开发中重要的一部分,所以我们的 redux 对此有进行了拓展: 所以我们需要 redux-thunk 的插件, ... 
- Redux异步解决方案之Redux-Thunk原理及源码解析
		前段时间,我们写了一篇Redux源码分析的文章,也分析了跟React连接的库React-Redux的源码实现.但是在Redux的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案.本 ... 
- redux源码解析(深度解析redux+异步demo)
		redux源码解析 1.首先让我们看看都有哪些内容 2.让我们看看redux的流程图 Store:一个库,保存数据的地方,整个项目只有一个 创建store Redux提供 creatStore 函数来 ... 
- 【原】redux异步操作学习笔记
		摘要: 发觉在学习react的生态链中,react+react-router+webpack+es6+fetch等等这些都基本搞懂的差不多了,可以应用到实战当中,唯独这个redux还不能,学习redu ... 
- react_结合 redux - 高阶函数 - 高阶组件 - 前端、后台项目打包运行
		Redux 独立的集中式状态管理 js 库 - 参见 My Git 不是 react 库,可以与 angular.vue 配合使用,通常和 react 用 yarn add redux import ... 
- [React] 14 - Redux: Redux Saga
		Ref: Build Real App with React #14: Redux Saga Ref: 聊一聊 redux 异步流之 redux-saga [入门] Ref: 从redux-thun ... 
- 前端(十):使用redux管理数据
		react本身能够完成动态数据的监听和更新,如果不是必要可以不适用redux. 安装redux: cnpm install redux --save,或者yarn add redux. 一.react ... 
- redux和react-redux的使用详解
		我自己的理解redux就跟vue中的vuex差不多,都是数据管理器,话不多说,我们从经典的计数器案例开始讲解 使用redux实现计数器 创建如下的react项目,我习惯把每一个模块分块,才有这么多文件 ... 
随机推荐
- Linux中的特殊权限s、t、i、a
			文件权限除了r.w.x外还有s.t.i.a权限:s:文件属主和组设置SUID和GUID,文件在被设置了s权限后将以root身份执行.在设置s权限时文件属主.属组必须先设置相应的x权限,否则s权限并不能 ... 
- 解决(Oracle)ORA-12528: TNS: 监听程序: 所有适用例程都无法建立新连接 问题
			解决(Oracle)ORA-12528: TNS: 监听程序: 所有适用例程都无法建立新连接 问题通过在CMD下用lsnrctl status 查看出的问题:发现BLOCKEDORACLE启动步骤:s ... 
- 【纯净软件】三款照片EXIF信息删除软件 Clear Exif、JPEG & PNG Stripper、Easy Exif Delete 非专业横向对比
			商业软件:需支付费用后方可使用. 共享软件:需支付费用,但可以先免费试用(有使用期限.功能限制). 免费软件:无需支付费用,无使用期限,无功能限制. 纯净软件:无广告.无联网行为的免费软件. 自由软件 ... 
- CSS Sprites技术原理和使用
			在分析各个网站的CSS时,我们经常可以看到一些网站有很多的元素共享了一张背景图片,而这张背景图片包含了所有这些元素需要的背景,这种技术就叫做CSS Sprites. 淘宝的css sprites ... 
- mysql中BLACKHOOL的作用
			MySQL在5.x系列提供了Blackhole引擎–"黑洞". 其作用正如其名字一样:任何写入到此引擎的数据均会被丢弃掉, 不做实际存储:Select语句的内容永远是空. 和Lin ... 
- Electron 常见问题
			导读: 以下记录了作者在实践中遇到的问题和最后的解决方法,如果有错误或者更新更完美的解决方案,欢迎留言指正.交流. 1.jQuery/RequireJS/Meteor/AngularJS 的问题 jQ ... 
- centos7 安装python虚拟环境
			本篇主要介绍centos7系统下,安装python3虚拟环境.环境:系统centos7,源代码安装python3,/usr/bin/python3为自己安装的. 安装支持包 yum install p ... 
- psql 命令
			(1)使用命令行连接数据库 psql -U postgres -h localhost -p 5433 (2)列出所有的数据库 \l -- 查看所有数据库 (3)进入某个数据库 \c name -- ... 
- Java中date和calendar的用法
			获取现在系统的时间和日期看起来是一件非常神奇的事情,但是当使用date和calendar之后发现仍然非常神奇. 1.date 使用date日期之前需要导入包: import java.text.Sim ... 
- shell变量替换扩展 字符串计数截取
