前段时间,咱们写了一篇Redux源码分析的文章,也分析了跟React
链接的库React-Redux
的源码实现。可是在Redux
的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案。本文会讲解Redux
官方实现的异步解决方案----Redux-Thunk
,咱们仍是会从基本的用法入手,再到原理解析,而后本身手写一个Redux-Thunk
来替换它,也就是源码解析。javascript
Redux-Thunk
和前面写过的Redux
和React-Redux
其实都是Redux
官方团队的做品,他们的侧重点各有不一样:前端
Redux:是核心库,功能简单,只是一个单纯的状态机,可是蕴含的思想不简单,是传说中的“百行代码,千行文档”。java
React-Redux:是跟
React
的链接库,当Redux
状态更新的时候通知React
更新组件。reactRedux-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
就能够发出异步的action
:redux
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
前咱们dispatch
的action
必须是一个纯对象(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
使用上面的方式在简单场景下能够工做的很好,可是你可能已经发现了几个问题:
- 每次你想显示
toast
的时候,你都得把这一大段代码抄过来抄过去。- 如今的
toast
没有id
,这可能会致使一种竞争的状况:若是你连续快速的显示两次toast
,当第一次的结束时,他会dispatch
出HIDE_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
只有当你dispatch
了action
才会触发。若是你在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
生态的核心做者,这几篇文章讲的Redux
,React-Redux
,Redux-Thunk
都是他的做品。
源码解析
上面关于缘由的翻译其实已经将Redux
适用的场景和原理讲的很清楚了,下面咱们来看看他的源码,本身仿写一个来替换他。照例咱们先来分析下要点:
Redux-Thunk
是一个Redux
中间件,因此他遵照Redux
中间件的范式。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 } } }
这里注意几个要点:
- 一个中间件接收
store
做为参数,会返回一个函数- 返回的这个函数接收老的
dispatch
函数做为参数(也就是代码中的next
),会返回一个新的函数- 返回的新函数就是新的
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;
总结
- 若是说
Redux
是“百行代码,千行文档”,那Redux-Thunk
就是“十行代码,百行思想”。 Redux-Thunk
最主要的做用是帮你给异步action
传入dispatch
,这样你就不用从调用的地方手动传入dispatch
,从而实现了调用的地方和使用的地方的解耦。Redux
和Redux-Thunk
让我深深体会到什么叫“编程思想”,编程思想能够很复杂,可是实现可能并不复杂,可是却很是有用。- 在咱们评估是否要引入一个库时最好想清楚咱们为何要引入这个库,是否有更简单的方案。
本文手写代码已经上传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
我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢迎关注~