Redux异步解决方案之Redux-Thunk原理及源码解析

前段时间,咱们写了一篇Redux源码分析的文章,也分析了跟React链接的库React-Redux的源码实现。可是在Redux的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案。本文会讲解Redux官方实现的异步解决方案----Redux-Thunk,咱们仍是会从基本的用法入手,再到原理解析,而后本身手写一个Redux-Thunk来替换它,也就是源码解析。javascript

Redux-Thunk和前面写过的ReduxReact-Redux其实都是Redux官方团队的做品,他们的侧重点各有不一样:前端

Redux:是核心库,功能简单,只是一个单纯的状态机,可是蕴含的思想不简单,是传说中的“百行代码,千行文档”。java

React-Redux:是跟React的链接库,当Redux状态更新的时候通知React更新组件。react

Redux-Thunk:提供Redux的异步解决方案,弥补Redux功能的不足。git

本文手写代码已经上传GitHub,你们能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.jsgithub

基本用法

仍是以咱们以前的那个计数器做为例子,为了让计数器+1,咱们会发出一个action,像这样:编程

function increment() {
  return {
    type: 'INCREMENT'
  }
};

store.dispatch(increment());

原始的Redux里面,action creator必须返回plain object,并且必须是同步的。可是咱们的应用里面常常会有定时器,网络请求等等异步操做,使用Redux-Thunk就能够发出异步的actionredux

function increment() {
  return {
    type: 'INCREMENT'
  }
};

// 异步action creator
function incrementAsync() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  }
}

// 使用了Redux-Thunk后dispatch不只仅能够发出plain object,还能够发出这个异步的函数
store.dispatch(incrementAsync());

下面再来看个更实际点的例子,也是官方文档中的例子:api

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

// createStore的时候传入thunk中间件
const store = createStore(rootReducer, applyMiddleware(thunk));

// 发起网络请求的方法
function fetchSecretSauce() {
  return fetch('https://www.baidu.com/s?wd=Secret%20Sauce');
}

// 下面两个是普通的action
function makeASandwich(forPerson, secretSauce) {
  return {
    type: 'MAKE_SANDWICH',
    forPerson,
    secretSauce,
  };
}

function apologize(fromPerson, toPerson, error) {
  return {
    type: 'APOLOGIZE',
    fromPerson,
    toPerson,
    error,
  };
}

// 这是一个异步action,先请求网络,成功就makeASandwich,失败就apologize
function makeASandwichWithSecretSauce(forPerson) {
  return function (dispatch) {
    return fetchSecretSauce().then(
      (sauce) => dispatch(makeASandwich(forPerson, sauce)),
      (error) => dispatch(apologize('The Sandwich Shop', forPerson, error)),
    );
  };
}

// 最终dispatch的是异步action makeASandwichWithSecretSauce
store.dispatch(makeASandwichWithSecretSauce('Me'));

为何要用Redux-Thunk

在继续深刻源码前,咱们先来思考一个问题,为何咱们要用Redux-Thunk,不用它行不行?再仔细看看Redux-Thunk的做用:缓存

// 异步action creator
function incrementAsync() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  }
}

store.dispatch(incrementAsync());

他仅仅是让dispath多支持了一种类型,就是函数类型,在使用Redux-Thunk前咱们dispatchaction必须是一个纯对象(plain object),使用了Redux-Thunk后,dispatch能够支持函数,这个函数会传入dispatch自己做为参数。可是其实咱们不使用Redux-Thunk也能够达到一样的效果,好比上面代码我彻底能够不要外层的incrementAsync,直接这样写:

setTimeout(() => {
  store.dispatch(increment());
}, 1000);

这样写一样能够在1秒后发出增长的action,并且代码还更简单,那咱们为何还要用Redux-Thunk呢,他存在的意义是什么呢?stackoverflow对这个问题有一个很好的回答,并且是官方推荐的解释。我再写一遍也不会比他写得更好,因此我就直接翻译了:

----翻译从这里开始----

**不要以为一个库就应该规定了全部事情!**若是你想用JS处理一个延时任务,直接用setTimeout就行了,即便你使用了Redux也没啥区别。Redux确实提供了另外一种处理异步任务的机制,可是你应该用它来解决你不少重复代码的问题。若是你没有太多重复代码,使用语言原生方案实际上是最简单的方案。

直接写异步代码

到目前为止这是最简单的方案,Redux也不须要特殊的配置:

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

(译注:这段代码的功能是显示一个通知,5秒后自动消失,也就是咱们常用的toast效果,原做者一直以这个为例。)

类似的,若是你是在一个链接了Redux组件中使用:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

惟一的区别就是链接组件通常不须要直接使用store,而是将dispatch或者action creator做为props注入,这两种方式对咱们都没区别。

若是你不想写重复的action名字,你能够将这两个action抽取成action creator而不是直接dispatch一个对象:

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
  this.props.dispatch(hideNotification())
}, 5000)

或者你已经经过connect()注入了这两个action creator

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

到目前为止,咱们没有使用任何中间件或者其余高级技巧,可是咱们一样实现了异步任务的处理。

提取异步的Action Creator

使用上面的方式在简单场景下能够工做的很好,可是你可能已经发现了几个问题:

  1. 每次你想显示toast的时候,你都得把这一大段代码抄过来抄过去。
  2. 如今的toast没有id,这可能会致使一种竞争的状况:若是你连续快速的显示两次toast,当第一次的结束时,他会dispatchHIDE_NOTIFICATION,这会错误的致使第二个也被关掉。

为了解决这两个问题,你可能须要将toast的逻辑抽取出来做为一个方法,大概长这样:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  // 给通知分配一个ID可让reducer忽略非当前通知的HIDE_NOTIFICATION
  // 并且咱们把计时器的ID记录下来以便于后面用clearTimeout()清除计时器
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

如今你的组件能够直接使用showNotificationWithTimeout,不再用抄来抄去了,也不用担忧竞争问题了:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')

可是为何showNotificationWithTimeout()要接收dispatch做为第一个参数呢?由于他须要将action发给store。通常组件是能够拿到dispatch的,为了让外部方法也能dispatch,咱们须要给他dispath做为参数。

若是你有一个单例的store,你也可让showNotificationWithTimeout直接引入这个store而后dispatch action

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => {
    store.dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.')

这样作看起来不复杂,也能达到效果,**可是咱们不推荐这种作法!**主要缘由是你的store必须是单例的,这让Server Render实现起来很麻烦。在Server端,你会但愿每一个请求都有本身的store,比便于不一样的用户能够拿到不一样的预加载内容。

一个单例的store也让单元测试很难写。测试action creator的时候你很难mock store,由于他引用了一个具体的真实的store。你甚至不能从外部重置store状态。

因此从技术上来讲,你能够从一个module导出单例的store,可是咱们不鼓励这样作。除非你肯定加确定你之后都不会升级Server Render。因此咱们仍是回到前面一种方案吧:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')

这个方案就能够解决重复代码和竞争问题。

Thunk中间件

对于简单项目,上面的方案应该已经能够知足需求了。

可是对于大型项目,你可能仍是会以为这样使用并不方便。

好比,彷佛咱们必须将dispatch做为参数传递,这让咱们分隔容器组件和展现组件变得更困难,由于任何发出异步Redux action的组件都必须接收dispatch做为参数,这样他才能将它继续往下传。你也不能仅仅使用connect()来绑定action creator,由于showNotificationWithTimeout()并非一个真正的action creator,他返回的也不是Redux action

还有个很尴尬的事情是,你必须记住哪一个action cerator是同步的,好比showNotification,哪一个是异步的辅助方法,好比showNotificationWithTimeout。这两个的用法是不同的,你须要当心的不要传错了参数,也不要混淆了他们。

这就是咱们为何须要找到一个“合法”的方法给辅助方法提供dispatch参数,而且帮助Redux区分出哪些是异步的action creator,好特殊处理他们

若是你的项目中面临着相似的问题,欢迎使用Redux Thunk中间件。

简单来讲,React Thunk告诉Redux怎么去区分这种特殊的action----他实际上是个函数:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

const store = createStore(
  reducer,
  applyMiddleware(thunk)
)

// 这个是普通的纯对象action
store.dispatch({ type: 'INCREMENT' })

// 可是有了Thunk,他就能够识别函数了
store.dispatch(function (dispatch) {
  // 这个函数里面又能够dispatch不少action
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // 异步的dispatch也能够
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

若是你使用了这个中间件,并且你dispatch的是一个函数,React Thunk会本身将dispatch做为参数传进去。并且他会将这些函数action“吃了”,因此不用担忧你的reducer会接收到奇怪的函数参数。你的reducer只会接收到纯对象action,不管是直接发出的仍是前面那些异步函数发出的。

这个看起来好像也没啥大用,对不对?在当前这个例子确实是的!可是他让咱们能够像定义一个普通的action creator那样去定义showNotificationWithTimeout

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

注意这里的showNotificationWithTimeout跟咱们前面的那个看起来很是像,可是他并不须要接收dispatch做为第一个参数。而是返回一个函数来接收dispatch做为第一个参数。

那在咱们的组件中怎么使用这个函数呢,咱们固然能够这样写:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

这样咱们直接调用了异步的action creator来获得内层的函数,这个函数须要dispatch作为参数,因此咱们给了他dispatch参数。

然而这样使用岂不是更尬,还不如咱们以前那个版本的!咱们为啥要这么干呢?

我以前就告诉过你:只要使用了Redux Thunk,若是你想dispatch一个函数,而不是一个纯对象,这个中间件会本身帮你调用这个函数,并且会将dispatch做为第一个参数传进去。

因此咱们能够直接这样干:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

最后,对于组件来讲,dispatch一个异步的action(实际上是一堆普通action)看起来和dispatch一个普通的同步action看起来并无啥区别。这是个好现象,由于组件就不该该关心那些动做究竟是同步的仍是异步的,咱们已经将它抽象出来了。

注意由于咱们已经教了Redux怎么区分这些特殊的action creator(咱们称之为thunk action creator),如今咱们能够在任何普通的action creator的地方使用他们了。好比,咱们能够直接在connect()中使用他们:

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

在Thunk中读取State

一般来讲,你的reducer会包含计算新的state的逻辑,可是reducer只有当你dispatchaction才会触发。若是你在thunk action creator中有一个反作用(好比一个API调用),某些状况下,你不想发出这个action该怎么办呢?

若是没有Thunk中间件,你须要在组件中添加这个逻辑:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

可是咱们提取action creator的目的就是为了集中这些在各个组件中重复的逻辑。幸运的是,Redux Thunk提供了一个读取当前store state的方法。那就是除了传入dispatch参数外,他还会传入getState做为第二个参数,这样thunk就能够读取store的当前状态了。

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // 不像普通的action cerator,这里咱们能够提早退出
    // Redux不关心这里的返回值,没返回值也不要紧
    if (!getState().areNotificationsEnabled) {
      return
    }

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

可是不要滥用这种方法!若是你须要经过检查缓存来判断是否发起API请求,这种方法就很好,可是将你整个APP的逻辑都构建在这个基础上并非很好。若是你只是用getState来作条件判断是否要dispatch action,你能够考虑将这些逻辑放到reducer里面去。

下一步

如今你应该对thunk的工做原理有了一个基本的概念,若是你须要更多的例子,能够看这里:https://redux.js.org/introduction/examples#async

你可能会发现不少例子都返回了Promise,这个不是必须的,可是用起来却很方便。Redux并不关心你的thunk返回了什么值,可是他会将这个值经过外层的dispatch()返回给你。这就是为何你能够在thunk中返回一个Promise而且等他完成:

dispatch(someThunkReturningPromise()).then(...)

另外你还能够将一个复杂的thunk action creator拆分红几个更小的thunk action creator。这是由于thunk提供的dispatch也能够接收thunk,因此你能够一直嵌套的dispatch thunk。并且结合Promise的话能够更好的控制异步流程。

在一些更复杂的应用中,你可能会发现你的异步控制流程经过thunk很难表达。好比,重试失败的请求,使用token进行从新受权认证,或者在一步一步的引导流程中,使用这种方式可能会很繁琐,并且容易出错。若是你有这些需求,你能够考虑下一些更高级的异步流程控制库,好比Redux Saga或者Redux Loop。能够看看他们,评估下,哪一个更适合你的需求,选一个你最喜欢的。

最后,不要使用任何库(包括thunk)若是你没有真实的需求。记住,咱们的实现都是要看需求的,也许你的需求这个简单的方案就能知足:

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

不要跟风尝试,除非你知道你为何须要这个!

----翻译到此结束----

StackOverflow的大神**Dan Abramov**对这个问题的回答实在太细致,太到位了,以至于我看了以后都不敢再写这个缘由了,以此翻译向大神致敬,再贴下这个回答的地址:https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

PS: Dan Abramov是Redux生态的核心做者,这几篇文章讲的ReduxReact-ReduxRedux-Thunk都是他的做品。

源码解析

上面关于缘由的翻译其实已经将Redux适用的场景和原理讲的很清楚了,下面咱们来看看他的源码,本身仿写一个来替换他。照例咱们先来分析下要点:

  1. Redux-Thunk是一个Redux中间件,因此他遵照Redux中间件的范式。
  2. thunk是一个能够dispatch的函数,因此咱们须要改写dispatch让他接受函数参数。

Redux中间件范式

在我前面那篇讲Redux源码的文章讲过中间件的范式以及Redux中这块源码是怎么实现的,没看过或者忘了的朋友能够再去看看。我这里再简单提一下,一个Redux中间件结构大概是这样:

function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

这里注意几个要点:

  1. 一个中间件接收store做为参数,会返回一个函数
  2. 返回的这个函数接收老的dispatch函数做为参数(也就是代码中的next),会返回一个新的函数
  3. 返回的新函数就是新的dispatch函数,这个函数里面能够拿到外面两层传进来的store和老dispatch函数

仿照这个范式,咱们来写一下thunk中间件的结构:

function thunk(store) {
  return function (next) {
    return function (action) {
      // 先直接返回原始结果
      let result = next(action);
      return result
    }
  }
}

处理thunk

根据咱们前面讲的,thunk是一个函数,接收dispatch getState两个参数,因此咱们应该将thunk拿出来运行,而后给他传入这两个参数,再将它的返回值直接返回就行。

function thunk(store) {
  return function (next) {
    return function (action) {
      // 从store中解构出dispatch, getState
      const { dispatch, getState } = store;

      // 若是action是函数,将它拿出来运行,参数就是dispatch和getState
      if (typeof action === 'function') {
        return action(dispatch, getState);
      }

      // 不然按照普通action处理
      let result = next(action);
      return result
    }
  }
}

接收额外参数withExtraArgument

Redux-Thunk还提供了一个API,就是你在使用applyMiddleware引入的时候,可使用withExtraArgument注入几个自定义的参数,好比这样:

const api = "http://www.example.com/sandwiches/";
const whatever = 42;

const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument({ api, whatever })),
);

function fetchUser(id) {
  return (dispatch, getState, { api, whatever }) => {
    // 如今你可使用这个额外的参数api和whatever了
  };
}

这个功能要实现起来也很简单,在前面的thunk函数外面再包一层就行:

// 外面再包一层函数createThunkMiddleware接收额外的参数
function createThunkMiddleware(extraArgument) {
  return function thunk(store) {
    return function (next) {
      return function (action) {
        const { dispatch, getState } = store;

        if (typeof action === 'function') {
          // 这里执行函数时,传入extraArgument
          return action(dispatch, getState, extraArgument);  
        }

        let result = next(action);
        return result
      }
    }
  }
}

而后咱们的thunk中间件其实至关于没传extraArgument

const thunk = createThunkMiddleware();

而暴露给外面的withExtraArgument函数就直接是createThunkMiddleware了:

thunk.withExtraArgument = createThunkMiddleware;

源码解析到此结束。啥,这就完了?是的,这就完了!Redux-Thunk就是这么简单,虽然背后的思想比较复杂,可是代码真的只有14行!我当时也震惊了,来看看官方源码吧:

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;

总结

  1. 若是说Redux是“百行代码,千行文档”,那Redux-Thunk就是“十行代码,百行思想”。
  2. Redux-Thunk最主要的做用是帮你给异步action传入dispatch,这样你就不用从调用的地方手动传入dispatch,从而实现了调用的地方和使用的地方的解耦。
  3. ReduxRedux-Thunk让我深深体会到什么叫“编程思想”,编程思想能够很复杂,可是实现可能并不复杂,可是却很是有用。
  4. 在咱们评估是否要引入一个库时最好想清楚咱们为何要引入这个库,是否有更简单的方案。

本文手写代码已经上传GitHub,你们能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.js

参考资料

Redux-Thunk文档:https://github.com/reduxjs/redux-thunk

Redux-Thunk源码: https://github.com/reduxjs/redux-thunk/blob/master/src/index.js

Dan Abramov在StackOverflow上的回答: https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。

做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

做者掘金文章汇总:https://juejin.im/post/5e3ffc85518825494e2772fd

我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢迎关注~

QRCode

相关文章
相关标签/搜索