Redux入门教程(二):异步 Action和中间件

一般action作为一个对象传入store,reducer直接计算好state结果即可。
但是,有一种常用的情况就是向服务端获取数据,这种异步操作该怎么办?

一、异步 Action

首先,当调用异步 API 时,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻。这两个时刻都可能会更改应用的 state,比如:开始请求时需要页面展示一个loading状态,收到数据后展示新数据。所以,我需要在不同时刻 dispatch 不同类型的 action,比如开始,成功,异常的action等等。

如何把action和网络请求结合起来呢?
站在框架作者的角度,看了看action,reducer,store三个模块,想来想去决定修改包装一下store中的store.dispatch()方法,就是要在action发出后,执行reducer之前动手脚,重新包装下dispatch,在这个过程中处理异步请求。

image.png

我们可能遇到如下这种情况,点击一个按钮,发送异步请求后再修改状态,也就是当action是个函数的时候:

//  点击出发asyncAdd执行
const asyncAdd = () => {
    store.dispatch((dispatch) => {
        setTimeout(() => {
            dispatch({ type: "add" })
        }, 1000);
    });
}

那么就需要引入中间件来帮我们解决这个问题。

二、中间件

接着上面,Action一般是个对象,当 Action为函数时,reducer就不好处理了,所以需要中间件处理这个返回的函数。
比如,这个函数会被 Redux Thunk middleware 执行。此外,这个函数还可以 dispatch 一个action,就像 dispatch 前面定义的同步 action 一样,中间件redux-thunk究竟长什么样才能完成这个功能,又是如何使用的呢?

使用中间件

import { createStore, applyMiddleware } from "redux";
import logger from "redux-logger";
import thunk from "redux-thunk";

function reducer(currentState = 0, action) {
    switch (action.type) {
        case 'add':
            return currentState + 1
            break;
        case 'minus':
            return currentState - 1
            break;
        default:
            return currentState
            break;
    }
}
// 使用redux提供的applyMiddleware将中间件进行聚合
const store = createStore(reducer, applyMiddleware(thunk, logger));
export default store;

createStore:redux中暴露的一个方法,定义一个store对象,并定义了store的方法,如store.dispatch;
reducer:根据项目需求自己写的纯函数;
thunk:redux-thunk中间件函数;
applyMiddleware:redux中暴露的一个方法,用于引入中间件,返回一个可以对createStore进行加强的函数,我们可以称之为enhancer;
接下来的问题来了,applyMiddleware,thunk,createStore分别长什么样,是怎么一起配合的?

createStore源码如下:

import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
  }
  // enhancer应该为一个函数
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    //enhancer 接受 createStore 作为参数,对  createStore 的能力进行增强,并返回增强后的  createStore 。
    //  然后再将  reducer 和  preloadedState 作为参数传给增强后的  createStore ,最终得到生成的 store
    return enhancer(createStore)(reducer, preloadedState)
  }
  // reducer必须是函数
  if (typeof reducer !== 'function') {
    throw new Error('Expected the reducer to be a function.')
  }

 // 初始化参数
  let currentReducer = reducer   // 当前整个reducer
  let currentState = preloadedState   // 当前的state,也就是getState返回的值
  let currentListeners = []  // 当前的订阅store的监听器
  let nextListeners = currentListeners // 下一次的订阅
  let isDispatching = false // 是否处于 dispatch action 状态中, 默认为false

  // 这个函数用于确保currentListeners 和 nextListeners 是不同的引用
  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  // 返回state
  function getState() {
    if (isDispatching) {
      throw new Error(
        ......
      )
    }
    return currentState
  }

  // 添加订阅
  function subscribe(listener) {
  ......
    }
  }
// 分发action
  function dispatch(action) {
    ......
  }

  //这个函数主要用于 reducer 的热替换,用的少
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }
    // 替换reducer
    currentReducer = nextReducer
    // 重新进行初始化
    dispatch({ type: ActionTypes.REPLACE })
  }

  // 没有研究,暂且放着,它是不直接暴露给开发者的,提供了给其他一些像观察者模式库的交互操作。
  function observable() {
    ......
  }

  // 创建一个store时的默认state
  // 用于填充初始的状态树
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

上面代码先不管其他的,就看这一句:
return enhancer(createStore)(reducer, preloadedState);
我们知道enhancer是applyMiddleware(thunk, promise, logger)返回的增强函数,这个函数可以处理createStore,enhancer(createStore)得到的是一个已经被加强了的createStore,这个加强了的createStore函数可以接收一些参数来创建store,包装dispatch,并返回store。

applyMiddleware源码如下:

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    // 利用传入的createStore和reducer和创建一个store
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
      )
    }
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

这里又出现了一个compose,我们来看看它有什么用?

compose 源码如下:

compose 可以接受一组函数参数,将这一组函数从右到左一层层嵌套,然后返回一个组合函数。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

上面代码中...funcs对应了一组函数序列,是个解构赋值的写法,funcs代表一个函数数组,数组方法reduce进行数组遍历迭代,将数组前两项a,b组合成一个组合函数,使b函数执行完返回的值能作为a函数的参数,以此类推。
例如:

compose(funcA, funcB, funcC)

得到的是:

(...args) =>funcA(funcB(funcC(...args)))

此时再与applyMiddleware源码联系在一起看,这个...args参数就是store.dispatch,其用意在于,先将其传给funcC就行改造,改造完成后将改造后的dispatch方法传给funcB,以此类推,目的在于可以后一个中间件可以接着处理前一个中间件改造后的dispatch

中间件redux-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;

上面代码,next代表下一个中间件函数,action代表上面Action Creator返回的那个函数。
redux-thunk 中间件的功能也很简单,createThunkMiddleware返回一个函数,这个函数检查参数 action 的类型,如果是函数的话,就执行这个 action 函数,并把 dispatch, getState, extraArgument 作为参数传递进去,否则就调用 next 让下一个中间件继续处理 action 。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342