前段時間,咱們寫了一篇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,你們能夠拿下來玩玩:github.com/dennis-jian…github
仍是以咱們以前的那個計數器做爲例子,爲了讓計數器+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)
複製代碼
到目前爲止,咱們沒有使用任何中間件或者其餘高級技巧,可是咱們一樣實現了異步任務的處理。
使用上面的方式在簡單場景下能夠工做的很好,可是你可能已經發現了幾個問題:
- 每次你想顯示
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
的工做原理有了一個基本的概念,若是你須要更多的例子,能夠看這裏:redux.js.org/introductio…。
你可能會發現不少例子都返回了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**對這個問題的回答實在太細緻,太到位了,以至於我看了以後都不敢再寫這個緣由了,以此翻譯向大神致敬,再貼下這個回答的地址:stackoverflow.com/questions/3…。
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,你們能夠拿下來玩玩:github.com/dennis-jian…
Redux-Thunk文檔:github.com/reduxjs/red…
Redux-Thunk源碼: github.com/reduxjs/red…
Dan Abramov在StackOverflow上的回答: stackoverflow.com/questions/3…
文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
歡迎關注個人公衆號進擊的大前端第一時間獲取高質量原創~
「前端進階知識」系列文章:juejin.cn/post/684490…
「前端進階知識」系列文章源碼GitHub地址: github.com/dennis-jian…