Redux:自問自答

請叫我王磊同學發表於2017-07-19

  前段時間看了Redux的原始碼,寫了一篇關於Redux的原始碼分析: Redux:百行程式碼千行文件,沒有看的小夥伴可以看一下,整篇文章主要是對Redux執行的原理進行了大致的解析,但是其實有很多內容並沒有明確地深究為什麼要這麼做?本篇文章的內容主要就是我自己提出一些問題,然後試著去回答這個問題,再次做個廣告,歡迎大家關注我的掘金賬號和我的部落格。   

為什麼createStore中既存在currentListeners也存在nextListeners?

  看過原始碼的同學應該瞭解,createStore函式為了儲存store的訂閱者,不僅儲存了當前的訂閱者currentListeners而且也儲存了nextListenerscreateStore中有一個內部函式ensureCanMutateNextListeners:   

function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
}複製程式碼

  這個函式實質的作用是確保可以改變nextListeners,如果nextListenerscurrentListeners一致的話,將currentListeners做一個拷貝賦值給nextListeners,然後所有的操作都會集中在nextListeners,比如我們看訂閱的函式subscribe:

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都會做如下的操作:

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,相當於更新了當前currentListenersnextListeners,然後通知訂閱者,到這裡我們不禁要問為什麼要存在這個nextListeners?
  
  其實程式碼中的註釋也是做了相關的解釋:

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 the dispatch() that is currently in progress.However, the next dispatch() call, whether nested or not, will use a more recent snapshot of the subscription list. 

  來讓我這個六級沒過的渣渣翻譯一下: 訂閱者(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是單執行緒語言,不可能出現出現想上述所說的多執行緒場景,但是我忽略了一點,執行訂閱者函式時,在這個回撥函式中可以執行退訂或者訂閱事件。例如:

const store = createStore(reducers.todos)
const unsubscribe1 = store.subscribe(() => {
    const unsubscribe2 = store.subscribe(()=>{})
})複製程式碼

  這不就實現了在通知listener的過程中混入訂閱subscribe與退訂unsubscribe嗎?   

為什麼Reducer中不能進行dispatch操作?

  我們知道在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

為什麼applyMiddleware中middlewareAPI中的dispathc要用閉包包裹?

  關於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要用閉包包裹,而不是直接傳入呢?首先用一幅圖來解釋一下中介軟體:
  
  


  
  如上圖所示,中介軟體的執行過程非常類似於洋蔥圈(Onion Rings),假設我們在函式applyMiddleware中傳入中介軟體的順序分別是mid1、mid2、mid3。而中介軟體函式的結構類似於:

export default function createMiddleware({ getState }) {
    return (next) => 
        (action) => {
            //before
            //......
            next(action)
            //after
            //......
        };
}複製程式碼

  那麼中介軟體函式內部程式碼執行次序分別是:
  

  但是如果在中介軟體函式中呼叫了dispatch(用mid3-before中為例),執行的次序就變成了:

  

  所以給中介軟體函式傳入的middlewareAPIdispatch函式是經過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原始碼時還有別的疑惑和感受,歡迎大家在評論區相互交流,討論和學習。

相關文章