1. 程式人生 > >Redux非同步解決方案之Redux-Thunk原理及原始碼解析

Redux非同步解決方案之Redux-Thunk原理及原始碼解析

前段時間,我們寫了一篇[Redux原始碼分析的文章](https://juejin.im/post/6845166891682512909),也[分析了跟`React`連線的庫`React-Redux`的原始碼實現](https://juejin.im/post/6847902222756347911)。但是在`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](https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.js)** ## 基本用法 還是以我們之前的[那個計數器作為例子](https://juejin.im/post/6847902222756347911#heading-0),為了讓計數器`+1`,我們會發出一個`action`,像這樣: ```javascript function increment() { return { type: 'INCREMENT' } }; store.dispatch(increment()); ``` 原始的`Redux`裡面,`action creator`必須返回`plain object`,而且必須是同步的。但是我們的應用裡面經常會有定時器,網路請求等等非同步操作,使用`Redux-Thunk`就可以發出非同步的`action`: ```javascript function increment() { return { type: 'INCREMENT' } }; // 非同步action creator function incrementAsync() { return (dispatch) => { setTimeout(() => { dispatch(increment()); }, 1000); } } // 使用了Redux-Thunk後dispatch不僅僅可以發出plain object,還可以發出這個非同步的函式 store.dispatch(incrementAsync()); ``` 下面再來看個更實際點的例子,也是[官方文件](https://github.com/reduxjs/redux-thunk#composition)中的例子: ```javascript 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`的作用: ```javascript // 非同步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`,直接這樣寫: ```javascript setTimeout(() => { store.dispatch(increment()); }, 1000); ``` 這樣寫同樣可以在1秒後發出增加的`action`,而且程式碼還更簡單,那我們為什麼還要用`Redux-Thunk`呢,他存在的意義是什麼呢?[stackoverflow對這個問題有一個很好的回答,而且是官方推薦的解釋](https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559)。我再寫一遍也不會比他寫得更好,所以我就直接翻譯了: **----翻譯從這裡開始----** **不要覺得一個庫就應該規定了所有事情!**如果你想用JS處理一個延時任務,直接用`setTimeout`就好了,即使你使用了`Redux`也沒啥區別。`Redux`確實提供了另一種處理非同步任務的機制,但是你應該用它來解決你很多重複程式碼的問題。如果你沒有太多重複程式碼,使用語言原生方案其實是最簡單的方案。 ### 直接寫非同步程式碼 到目前為止這是最簡單的方案,`Redux`也不需要特殊的配置: ```javascript store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' }) setTimeout(() => { store.dispatch({ type: 'HIDE_NOTIFICATION' }) }, 5000) ``` (譯註:這段程式碼的功能是顯示一個通知,5秒後自動消失,也就是我們經常使用的`toast`效果,原作者一直以這個為例。) 相似的,如果你是在一個連線了`Redux`元件中使用: ```javascript 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`一個物件: ```javascript // 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`: ```javascript this.props.showNotification('You just logged in.') setTimeout(() => { this.props.hideNotification() }, 5000) ``` 到目前為止,我們沒有使用任何中介軟體或者其他高階技巧,但是我們同樣實現了非同步任務的處理。 ### 提取非同步的Action Creator 使用上面的方式在簡單場景下可以工作的很好,但是你可能已經發現了幾個問題: > 1. 每次你想顯示`toast`的時候,你都得把這一大段程式碼抄過來抄過去。 > 2. 現在的`toast`沒有`id`,這可能會導致一種競爭的情況:如果你連續快速的顯示兩次`toast`,當第一次的結束時,他會`dispatch`出`HIDE_NOTIFICATION`,這會錯誤的導致第二個也被關掉。 為了解決這兩個問題,你可能需要將`toast`的邏輯抽取出來作為一個方法,大概長這樣: ```js // 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`,再也不用抄來抄去了,也不用擔心競爭問題了: ```js // 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`: ```javascript // 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`。所以我們還是回到前面一種方案吧: ```javascript // 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`----他其實是個函式: ```javascript 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`: ```javascript // 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`作為第一個引數。 那在我們的元件中怎麼使用這個函式呢,我們當然可以這樣寫: ```javascript // component.js showNotificationWithTimeout('You just logged in.')(this.props.dispatch) ``` 這樣我們直接呼叫了非同步的`action creator`來得到內層的函式,這個函式需要`dispatch`做為引數,所以我們給了他`dispatch`引數。 然而這樣使用豈不是更尬,還不如我們之前那個版本的!我們為啥要這麼幹呢? 我之前就告訴過你:**只要使用了`Redux Thunk`,如果你想`dispatch`一個函式,而不是一個純物件,這個中介軟體會自己幫你呼叫這個函式,而且會將`dispatch`作為第一個引數傳進去。** 所以我們可以直接這樣幹: ```javascript // component.js this.props.dispatch(showNotificationWithTimeout('You just logged in.')) ``` 最後,對於元件來說,`dispatch`一個非同步的`action`(其實是一堆普通`action`)看起來和`dispatch`一個普通的同步`action`看起來並沒有啥區別。這是個好現象,因為元件就不應該關心那些動作到底是同步的還是非同步的,我們已經將它抽象出來了。 注意因為我們已經教了`Redux`怎麼區分這些特殊的`action creator`(我們稱之為`thunk action creator`),現在我們可以在任何普通的`action creator`的地方使用他們了。比如,我們可以直接在`connect()`中使用他們: ```javascript // 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`中介軟體,你需要在元件中新增這個邏輯: ```javascript // component.js if (this.props.areNotificationsEnabled) { showNotificationWithTimeout(this.props.dispatch, 'You just logged in.') } ``` 但是我們提取`action creator`的目的就是為了集中這些在各個元件中重複的邏輯。幸運的是,`Redux Thunk`提供了一個讀取當前`store state`的方法。那就是除了傳入`dispatch`引數外,他還會傳入`getState`作為第二個引數,這樣`thunk`就可以讀取`store`的當前狀態了。 ```javascript 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](https://redux.js.org/introduction/examples#async)。 你可能會發現很多例子都返回了`Promise`,這個不是必須的,但是用起來卻很方便。`Redux`並不關心你的`thunk`返回了什麼值,但是他會將這個值通過外層的`dispatch()`返回給你。這就是為什麼你可以在`thunk`中返回一個`Promise`並且等他完成: ```javascript dispatch(someThunkReturningPromise()).then(...) ``` 另外你還可以將一個複雜的`thunk action creator`拆分成幾個更小的`thunk action creator`。這是因為`thunk`提供的`dispatch`也可以接收`thunk`,所以你可以一直巢狀的`dispatch thunk`。而且結合`Promise`的話可以更好的控制非同步流程。 在一些更復雜的應用中,你可能會發現你的非同步控制流程通過`thunk`很難表達。比如,重試失敗的請求,使用`token`進行重新授權認證,或者在一步一步的引導流程中,使用這種方式可能會很繁瑣,而且容易出錯。如果你有這些需求,你可以考慮下一些更高階的非同步流程控制庫,比如[Redux Saga](https://github.com/yelouafi/redux-saga)或者[Redux Loop](https://github.com/raisemarketplace/redux-loop)。可以看看他們,評估下,哪個更適合你的需求,選一個你最喜歡的。 最後,不要使用任何庫(包括thunk)如果你沒有真實的需求。記住,我們的實現都是要看需求的,也許你的需求這個簡單的方案就能滿足: ```javascript store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' }) setTimeout(() => { store.dispatch({ type: 'HIDE_NOTIFICATION' }) }, 5000) ``` 不要跟風嘗試,除非你知道你為什麼需要這個! **----翻譯到此結束----** `StackOverflow`的大神**[Dan Abramov](https://stackoverflow.com/users/458193/dan-abramov)**對這個問題的回答實在太細緻,太到位了,以致於我看了之後都不敢再寫這個原因了,以此翻譯向大神致敬,再貼下這個回答的地址:[https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559](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`適用的場景和原理講的很清楚了,下面我們來看看他的原始碼,自己仿寫一個來替換他。照例我們先來分析下要點: > 1. `Redux-Thunk`是一個`Redux`中介軟體,所以他遵守`Redux`中介軟體的正規化。 > 2. `thunk`是一個可以`dispatch`的函式,所以我們需要改寫`dispatch`讓他接受函式引數。 ### `Redux`中介軟體正規化 [在我前面那篇講`Redux`原始碼的文章講過中介軟體的正規化以及`Redux`中這塊原始碼是怎麼實現的,沒看過或者忘了的朋友可以再去看看。](https://juejin.im/post/6845166891682512909#heading-7)我這裡再簡單提一下,一個`Redux`中介軟體結構大概是這樣: ```javascript 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`中介軟體的結構: ```javascript function thunk(store) { return function (next) { return function (action) { // 先直接返回原始結果 let result = next(action); return result } } } ``` ### 處理thunk 根據我們前面講的,`thunk`是一個函式,接收`dispatch getState`兩個引數,所以我們應該將`thunk`拿出來執行,然後給他傳入這兩個引數,再將它的返回值直接返回就行。 ```javascript 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`注入幾個自定義的引數,比如這樣: ```javascript 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`函式外面再包一層就行: ```javascript // 外面再包一層函式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`: ```javascript const thunk = createThunkMiddleware(); ``` 而暴露給外面的`withExtraArgument`函式就直接是`createThunkMiddleware`了: ```javascript thunk.withExtraArgument = createThunkMiddleware; ``` 原始碼解析到此結束。啥,這就完了?是的,這就完了!`Redux-Thunk`就是這麼簡單,雖然背後的思想比較複雜,但是程式碼真的只有14行!我當時也震驚了,來看看[官方原始碼](https://github.com/reduxjs/redux-thunk/blob/master/src/index.js)吧: ```javascript 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. `Redux`和`Redux-Thunk`讓我深深體會到什麼叫“程式設計思想”,程式設計思想可以很複雜,但是實現可能並不複雜,但是卻非常有用。 4. 在我們評估是否要引入一個庫時最好想清楚我們為什麼要引入這個庫,是否有更簡單的方案。 **本文手寫程式碼已經上傳GitHub,大家可以拿下來玩玩:[https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.js](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](https://github.com/reduxjs/redux-thunk) Redux-Thunk原始碼: [https://github.com/reduxjs/redux-thunk/blob/master/src/index.js](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](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://github.com/dennis-jiang/Front-End-Knowledges)** **作者掘金文章彙總:[https://juejin.im/post/5e3ffc85518825494e2772fd](https://juejin.im/post/5e3ffc85518825494e2772fd)** **我也搞了個公眾號[進擊的大前端],不打廣告,不寫水文,只發高質量原創,歡迎關注~** ![QRCode](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a3ecc22b508e4963adda7d69c4c01210~tplv-k3u1fbpfcp-zoom-