前段時間看了Redux的源碼,寫了一篇關於Redux的源碼分析: Redux:百行代碼千行文檔,沒有看的小夥伴能夠看一下,整篇文章主要是對Redux運行的原理進行了大體的解析,可是其實有不少內容並無明確地深究爲何要這麼作?本篇文章的內容主要就是我本身提出一些問題,而後試着去回答這個問題,再次作個廣告,歡迎你們關注個人掘金帳號和個人博客。 javascript
看過源碼的同窗應該瞭解,createStore
函數爲了保存store
的訂閱者,不只保存了當前的訂閱者currentListeners
並且也保存了nextListeners
。createStore
中有一個內部函數ensureCanMutateNextListeners
: java
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}複製代碼
這個函數實質的做用是確保能夠改變nextListeners
,若是nextListeners
與currentListeners
一致的話,將currentListeners
作一個拷貝賦值給nextListeners
,而後全部的操做都會集中在nextListeners
,好比咱們看訂閱的函數subscribe
:git
function subscribe(listener) {
// ......
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
// ......
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}複製代碼
咱們發現訂閱和解除訂閱都是在nextListeners
作的操做,而後每次dispatch
一個action
都會作以下的操做:github
function dispatch(action) {
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 至關於currentListeners = nextListeners const listeners = currentListeners
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}複製代碼
咱們發如今dispatch
中作了const listeners = currentListeners = nextListeners
,至關於更新了當前currentListeners
爲nextListeners
,而後通知訂閱者,到這裏咱們不由要問爲何要存在這個nextListeners
?
其實代碼中的註釋也是作了相關的解釋:redux
The subscriptions are snapshotted just before every
dispatch()
call.If you subscribe or unsubscribe while the listeners are being invoked, this will not have any effect on thedispatch()
that is currently in progress.However, the nextdispatch()
call, whether nested or not, will use a more recent snapshot of the subscription list. api
來讓我這個六級沒過的渣渣翻譯一下: 訂閱者(subscriptions)在每次dispatch()
調用以前都是一份快照(snapshotted)。若是你在listener
被調用期間,進行訂閱或者退訂,在本次的dispatch()
過程當中是不會生效的,然而在下一次的dispatch()
調用中,不管dispatch
是不是嵌套調用的,都將使用最近一次的快照訂閱者列表。用圖表示的效果以下:
多線程
nextListeners
這份快照的話,由於
dispatch
致使的
store
的改變,從而進一步通知訂閱者,若是在通知訂閱者的過程當中發生了其餘的訂閱(subscribe)和退訂(unsubscribe),那確定會發生
錯誤或者
不肯定性。例如:好比在通知訂閱的過程當中,若是發生了退訂,那就既有可能成功退訂(在通知以前就執行了
nextListeners.splice(index, 1)
)或者沒有成功退訂(在已經通知了以後才執行了
nextListeners.splice(index, 1)
),這固然是不行的。由於
nextListeners
的存在因此通知訂閱者的行爲是明確的,訂閱和退訂是不會影響到本次訂閱者通知的過程。
這都沒有問題,但是存在一個問題,JavaScript不是單線程的嗎?怎麼會出現上述所說的場景呢?百思不得其解的狀況下,去Redux項目下開了一個issue,獲得了維護者的回答:閉包
得了,咱們再來看看測試相關的代碼吧。看完以後我瞭解到了。的確,由於JavaScript是單線程語言,不可能出現出現想上述所說的多線程場景,可是我忽略了一點,執行訂閱者函數時,在這個回調函數中能夠執行退訂或者訂閱事件。例如:app
const store = createStore(reducers.todos)
const unsubscribe1 = store.subscribe(() => {
const unsubscribe2 = store.subscribe(()=>{})
})複製代碼
這不就實現了在通知listener的過程當中混入訂閱subscribe
與退訂unsubscribe
嗎? 函數
咱們知道在reducer
函數中是不能執行dispatch
操做的。一方面,reducer
做爲計算下一次state
的純函數是不該該承擔執行dispatch
這樣的操做。另外一方面,即便你嘗試着在reducer
中執行dispatch
,也並不會成功,而且會獲得"Reducers may not dispatch actions."的提示。由於在dispatch
函數就作了相關的限制:
function dispatch(action) {
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
//...notice listener
}複製代碼
在執行dispatch
時就會將標誌位isDispatching
置爲true
。而後若是在currentReducer(currentState, action)
執行的過程當中由執行了dispatch
,那麼就會拋出錯誤('Reducers may not dispatch actions.')。之因此作如此的限制,是由於在dispatch
中會引發reducer
的執行,若是此時reducer
中又執行了dispatch
,這樣就落入了一個死循環,因此就要避免reducer
中執行dispatch
。
關於Redux的中間件以前我寫過一篇相關的文章Redux:Middleware你咋就這麼難,沒有看過的同窗能夠了解一下,其實文章中也有一個地方沒有明確的解釋,當時初學不是很理解,如今來解釋一下:
export default function applyMiddleware(...middlewares) {
return (next) =>
(reducer, initialState) => {
var store = next(reducer, initialState);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware =>
middleware(middlewareAPI));
dispatch = compose(...chain, store.dispatch);
return {
...store,
dispatch
};
};
}複製代碼
這個問題的就是爲何middlewareAPI中的dispathc要用閉包包裹,而不是直接傳入呢?首先用一幅圖來解釋一下中間件:
applyMiddleware
中傳入中間件的順序分別是mid一、mid二、mid3。而中間件函數的結構相似於:
export default function createMiddleware({ getState }) {
return (next) =>
(action) => {
//before
//......
next(action)
//after
//......
};
}複製代碼
那麼中間件函數內部代碼執行次序分別是:
可是若是在中間件函數中調用了dispatch
(用mid3-before
中爲例),執行的次序就變成了:
因此給中間件函數傳入的middlewareAPI
中dispatch
函數是通過applyMiddleware
改造過的dispatch
,而不是redux
原生的store.dispatch
。因此咱們經過一個閉包包裹dispatch
:
(action) => dispatch(action)複製代碼
這樣咱們在後面給dispatch
賦值爲dispatch = compose(...chain, store.dispatch);
,這樣只要 dispatch 更新了,middlewareAPI 中的 dispatch 應用也會發生變化。若是咱們寫成:
var middlewareAPI = {
getState: store.getState,
dispatch: dispatch
};複製代碼
那中間件函數中接受到的dispatch
永遠只能是最開始的redux
中的dispatch
。
最後,若是你們在閱讀Redux源碼時還有別的疑惑和感覺,歡迎你們在評論區相互交流,討論和學習。