前段時間,我們寫了一篇Redux原始碼分析的文章,也分析了跟React連接的庫React-Redux的原始碼實作,但是在Redux的生態中還有一個很重要的部分沒有涉及到,那就是Redux的異步解決方案,本文會講解Redux官方實作的異步解決方案----Redux-Thunk,我們還是會從基本的用法入手,再到原理決議,然后自己手寫一個Redux-Thunk來替換它,也就是原始碼決議,
Redux-Thunk和前面寫過的Redux和React-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前我們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
我也搞了個公眾號[進擊的大前端],不打廣告,不寫水文,只發高質量原創,歡迎關注~
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/945.html
標籤:JavaScript
上一篇:蒲公英 · JELLY技術周刊 Vol.21 -- 技術周刊 · React Hooks vs Vue 3 + Composition API
下一篇:ES6特性整理
