关于前端:Redux异步解决方案之ReduxThunk原理及源码解析

28次阅读

共计 13994 个字符,预计需要花费 35 分钟才能阅读完成。

前段时间,咱们写了一篇 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 creator
function 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');
}

// 上面两个是一般的 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

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

正文完
 0