前段时间,咱们写了一篇Redux源码剖析的文章,也剖析了跟React连贯的库React-Redux的源码实现。然而在Redux的生态中还有一个很重要的局部没有波及到,那就是Redux的异步解决方案。本文会解说Redux官网实现的异步解决方案----Redux-Thunk,咱们还是会从根本的用法动手,再到原理解析,而后本人手写一个Redux-Thunk来替换它,也就是源码解析。

Redux-Thunk和后面写过的ReduxReact-Redux其实都是Redux官网团队的作品,他们的侧重点各有不同:

Redux:是外围库,性能简略,只是一个单纯的状态机,然而蕴含的思维不简略,是传说中的“百行代码,千行文档”。

React-Redux:是跟React的连贯库,当Redux状态更新的时候告诉React更新组件。

Redux-Thunk:提供Redux的异步解决方案,补救Redux性能的有余。

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

根本用法

还是以咱们之前的那个计数器作为例子,为了让计数器+1,咱们会收回一个action,像这样:

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

原始的Redux外面,action creator必须返回plain object,而且必须是同步的。然而咱们的利用外面常常会有定时器,网络申请等等异步操作,应用Redux-Thunk就能够收回异步的action

function increment() {  return {    type: 'INCREMENT'  }};// 异步action creatorfunction incrementAsync() {  return (dispatch) => {    setTimeout(() => {      dispatch(increment());    }, 1000);  }}// 应用了Redux-Thunk后dispatch不仅仅能够收回plain object,还能够收回这个异步的函数store.dispatch(incrementAsync());

上面再来看个更理论点的例子,也是官网文档中的例子:

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');}// 上面两个是一般的actionfunction makeASandwich(forPerson, secretSauce) {  return {    type: 'MAKE_SANDWICH',    forPerson,    secretSauce,  };}function apologize(fromPerson, toPerson, error) {  return {    type: 'APOLOGIZE',    fromPerson,    toPerson,    error,  };}// 这是一个异步action,先申请网络,胜利就makeASandwich,失败就apologizefunction makeASandwichWithSecretSauce(forPerson) {  return function (dispatch) {    return fetchSecretSauce().then(      (sauce) => dispatch(makeASandwich(forPerson, sauce)),      (error) => dispatch(apologize('The Sandwich Shop', forPerson, error)),    );  };}// 最终dispatch的是异步action makeASandwichWithSecretSaucestore.dispatch(makeASandwichWithSecretSauce('Me'));

为什么要用Redux-Thunk

在持续深刻源码前,咱们先来思考一个问题,为什么咱们要用Redux-Thunk,不必它行不行?再认真看看Redux-Thunk的作用:

// 异步action creatorfunction 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.jsexport function showNotification(text) {  return { type: 'SHOW_NOTIFICATION', text }}export function hideNotification() {  return { type: 'HIDE_NOTIFICATION' }}// component.jsimport { 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.jsfunction showNotification(id, text) {  return { type: 'SHOW_NOTIFICATION', id, text }}function hideNotification(id) {  return { type: 'HIDE_NOTIFICATION', id }}let nextNotificationId = 0export function showNotificationWithTimeout(dispatch, text) {  // 给告诉调配一个ID能够让reducer疏忽非以后告诉的HIDE_NOTIFICATION  // 而且咱们把计时器的ID记录下来以便于前面用clearTimeout()革除计时器  const id = nextNotificationId++  dispatch(showNotification(id, text))  setTimeout(() => {    dispatch(hideNotification(id))  }, 5000)}

当初你的组件能够间接应用showNotificationWithTimeout,再也不必抄来抄去了,也不必放心竞争问题了:

// component.jsshowNotificationWithTimeout(this.props.dispatch, 'You just logged in.')// otherComponent.jsshowNotificationWithTimeout(this.props.dispatch, 'You just logged out.')  

然而为什么showNotificationWithTimeout()要接管dispatch作为第一个参数呢?因为他须要将action发给store。个别组件是能够拿到dispatch的,为了让内部办法也能dispatch,咱们须要给他dispath作为参数。

如果你有一个单例的store,你也能够让showNotificationWithTimeout间接引入这个store而后dispatch action

// store.jsexport default createStore(reducer)// actions.jsimport store from './store'// ...let nextNotificationId = 0export function showNotificationWithTimeout(text) {  const id = nextNotificationId++  store.dispatch(showNotification(id, text))  setTimeout(() => {    store.dispatch(hideNotification(id))  }, 5000)}// component.jsshowNotificationWithTimeout('You just logged in.')// otherComponent.jsshowNotificationWithTimeout('You just logged out.') 

这样做看起来不简单,也能达到成果,然而咱们不举荐这种做法!次要起因是你的store必须是单例的,这让Server Render实现起来很麻烦。在Server端,你会心愿每个申请都有本人的store,比便于不同的用户能够拿到不同的预加载内容。

一个单例的store也让单元测试很难写。测试action creator的时候你很难mock store,因为他援用了一个具体的实在的store。你甚至不能从内部重置store状态。

所以从技术上来说,你能够从一个module导出单例的store,然而咱们不激励这样做。除非你确定加必定你当前都不会降级Server Render。所以咱们还是回到后面一种计划吧:

// actions.js// ...let nextNotificationId = 0export function showNotificationWithTimeout(dispatch, text) {  const id = nextNotificationId++  dispatch(showNotification(id, text))  setTimeout(() => {    dispatch(hideNotification(id))  }, 5000)}// component.jsshowNotificationWithTimeout(this.props.dispatch, 'You just logged in.')// otherComponent.jsshowNotificationWithTimeout(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))// 这个是一般的纯对象actionstore.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.jsfunction showNotification(id, text) {  return { type: 'SHOW_NOTIFICATION', id, text }}function hideNotification(id) {  return { type: 'HIDE_NOTIFICATION', id }}let nextNotificationId = 0export function showNotificationWithTimeout(text) {  return function (dispatch) {    const id = nextNotificationId++    dispatch(showNotification(id, text))    setTimeout(() => {      dispatch(hideNotification(id))    }, 5000)  }}

留神这里的showNotificationWithTimeout跟咱们后面的那个看起来十分像,然而他并不需要接管dispatch作为第一个参数。而是返回一个函数来接管dispatch作为第一个参数。

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

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

这样咱们间接调用了异步的action creator来失去内层的函数,这个函数须要dispatch做为参数,所以咱们给了他dispatch参数。

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

我之前就通知过你:只有应用了Redux Thunk,如果你想dispatch一个函数,而不是一个纯对象,这个中间件会本人帮你调用这个函数,而且会将dispatch作为第一个参数传进去。

所以咱们能够间接这样干:

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

最初,对于组件来说,dispatch一个异步的action(其实是一堆一般action)看起来和dispatch一个一般的同步action看起来并没有啥区别。这是个好景象,因为组件就不应该关怀那些动作到底是同步的还是异步的,咱们曾经将它形象进去了。

留神因为咱们曾经教了Redux怎么辨别这些非凡的action creator(咱们称之为thunk action creator),当初咱们能够在任何一般的action creator的中央应用他们了。比方,咱们能够间接在connect()中应用他们:

// actions.jsfunction showNotification(id, text) {  return { type: 'SHOW_NOTIFICATION', id, text }}function hideNotification(id) {  return { type: 'HIDE_NOTIFICATION', id }}let nextNotificationId = 0export function showNotificationWithTimeout(text) {  return function (dispatch) {    const id = nextNotificationId++    dispatch(showNotification(id, text))    setTimeout(() => {      dispatch(hideNotification(id))    }, 5000)  }}// component.jsimport { 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.jsif (this.props.areNotificationsEnabled) {  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')}

然而咱们提取action creator的目标就是为了集中这些在各个组件中反复的逻辑。侥幸的是,Redux Thunk提供了一个读取以后store state的办法。那就是除了传入dispatch参数外,他还会传入getState作为第二个参数,这样thunk就能够读取store的以后状态了。

let nextNotificationId = 0export 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

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