前段時間,咱們寫了一篇Redux源碼分析的文章,也分析了跟React
鏈接的庫React-Redux
的源碼實現。可是在Redux
的生態中還有一個很重要的部分沒有涉及到,那就是Redux的異步解決方案。本文會講解Redux
官方實現的異步解決方案----Redux-Thunk
,咱們仍是會從基本的用法入手,再到原理解析,而後本身手寫一個Redux-Thunk
來替換它,也就是源碼解析。javascript
Redux-Thunk
和前面寫過的Redux
和React-Redux
其實都是Redux
官方團隊的做品,他們的側重點各有不一樣:前端
Redux:是核心庫,功能簡單,只是一個單純的狀態機,可是蘊含的思想不簡單,是傳說中的「百行代碼,千行文檔」。React-Redux:是跟
React
的鏈接庫,當Redux
狀態更新的時候通知React
更新組件。javaRedux-Thunk:提供
Redux
的異步解決方案,彌補Redux
功能的不足。react
本文手寫代碼已經上傳GitHub,你們能夠拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.jsgit
仍是以咱們以前的那個計數器做爲例子,爲了讓計數器+1
,咱們會發出一個action
,像這樣:github
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());
下面再來看個更實際點的例子,也是官方文檔中的例子:redux
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
的做用:segmentfault
// 異步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
,直接這樣寫:api
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)
到目前爲止,咱們沒有使用任何中間件或者其餘高級技巧,可是咱們一樣實現了異步任務的處理。
使用上面的方式在簡單場景下能夠工做的很好,可是你可能已經發現了幾個問題:
- 每次你想顯示
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.')
這個方案就能夠解決重複代碼和競爭問題。
對於簡單項目,上面的方案應該已經能夠知足需求了。
可是對於大型項目,你可能仍是會以爲這樣使用並不方便。
好比,彷佛咱們必須將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)
一般來講,你的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
是一個函數,接收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 } } }
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
我也搞了個公衆號[進擊的大前端],不打廣告,不寫水文,只發高質量原創,歡迎關注~