Redux原始碼分析–資料中心篇

lanzhiheng發表於2019-03-01

在如今的前端浪潮中,React和Redux有著舉足輕重的地位。React,Redux再加上用於連結他們的程式碼庫就足矣讓一些沒有足夠經驗的開發者迷失到程式碼的海洋裡,很容易讓程式設計師們培養成一種別人怎麼寫我就怎麼寫的編碼習慣,難怪許多大神會說這是最好的時代但也是最壞的時代。

Redux

今天我想脫離整體來看區域性,從原始碼的角度上來剖析Redux到底是個什麼玩意,瞭解了它的原理才不至於在如今的浪潮中顯得手忙腳亂。

前言

拋開React不談,Redux其實就只是一個管理狀態的資料中心,然而作為一個資料中心它的特色在於我們不能夠直接修改資料中心裡面的資料,我們需要自行定義操作邏輯reducer,以及操作型別action,通過分發不同的action來匹配reducer裡面對應的操作,才能達到修改資料的目的。

一般來說我們會通過以下方式來建立一個資料中心

import { createStore } from `redux`
const store = createStore(...blablabla)
複製程式碼

這裡最為關鍵的就是createStore這個函式,接下來我想詳細地對它做個分析。

createStore方法剖析

createStore.js這個檔案純程式碼的部分大概有100多行,如果把他們全部貼出來再一一分析並非明智之舉,我認為只對關鍵的部分進行分析是更恰當的做法。要分析一個方法我覺得比較有意義的是看它接收了什麼,以及返回了什麼。

1) 接收的引數

export default function createStore(reducer, preloadedState, enhancer) {
  ...
}
複製程式碼

這個方法接受三個引數,分別是reducer, preloadedState, enhancer。以上都分別可以由開發者進行定義,reducer就是由開發者定義的一個操作方法,它會以舊的狀態作為引數,處理過後返回一個新的狀態。preloadedState則可以理解成資料中心的初始狀態,它是個可選值。

最後的enhancer又是什麼呢?從字面上理解它是一個增強器,用於增強createStore。從原始碼看它的工作方式

export default function createStore(reducer, preloadedState, enhancer) {
  .....
  if (typeof preloadedState === `function` && typeof enhancer === `undefined`) { // 引數歸一
    enhancer = preloadedState
    preloadedState = undefined
  }

  if (typeof enhancer !== `undefined`) {
    if (typeof enhancer !== `function`) {
      throw new Error(`Expected the enhancer to be a function.`)
    }

    return enhancer(createStore)(reducer, preloadedState) // 直接返回一個增強後的`createStore
  }
  .....
}
複製程式碼

可見,它接收了原來的createStore作為引數,並且返回了一個增強了的方法,最後用增強過的方法來呼叫原來傳入的引數。瞭解原理之後我們可以很容易地寫出一個狀態列印增強器,用於列印dispatch前後的狀態資訊。

.....
function enhancer(createStore) {
  return (reducer, initialState, enhancer) => {
    const store = createStore(reducer, initialState, enhancer)

    function dispatch(action) {
      console.log(`old`, store.getState())
      const res = store.dispatch(action);
      console.log(`new`, store.getState())
      return res
    }

    // 用心的dispatch方法來替換原有的dispatch方法
    return {
      ...store,
      dispatch
    }
  }
}

const store = createStore(reducers, undefined, enhancer)
複製程式碼

另外,從Redux的原始碼可以看到createStore做了一種叫做引數歸一的處理,在許多JS庫中都會採用這種方式相容不同情況下的引數傳入。當我們不需要傳入初始狀態,而只需要使用enhancer增強器的時候,我們還可以把程式碼寫成這樣

const store = createStore(reducers, enhancer)
複製程式碼

2) 返回值

接下來我們看看返回值。createStore最終會返回一個物件,包含的東西如下

import $$observable from `symbol-observable`

export default function createStore(reducer, preloadedState, enhancer) {
  .....
  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
複製程式碼

這些便是我們資料中心為外部提供的全部介面了。最後一個看起來有點奇怪,其他的從字面上應該都比較容易理解,容許許我一一分析。

a. getState–返回當前狀態

Redux的核心理念之一就是不支援直接修改狀態,它是通過閉包來實現這一點。

export default function createStore(reducer, preloadedState, enhancer) {
  let currentState = preloadedState

  function getState() {
    .....
    return currentState
  }
}

複製程式碼

它先是定義了一個內部的變數currentState,然後通過一個名為getState的方法來返回它的值。這就造成了currentState這個狀態對我們而言是隻讀的,我們沒辦法直接修改它的值。在程式碼裡面我們可以通過getState這個方法來返回當前狀態

console.log(store.getState())
複製程式碼

b. subscribe–構造監聽者佇列

每個store本身會維護一個監聽者佇列,我們可以把它想象成一個方法的佇列,在每次分發action的時候都會依次呼叫監聽者佇列中所有方法。通過這個subscribe方法可以手動地把一些回撥函式新增到監聽者佇列中

export default function createStore(reducer, preloadedState, enhancer) {
  ....

  let currentListeners = []
  let nextListeners = currentListeners

  ...

  function ensureCanMutateNextListeners() {
    if (nextListeners === currentListeners) {
      nextListeners = currentListeners.slice()
    }
  }

  ...

  function subscribe(listener) {
    .....

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      ....

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

}
複製程式碼

邏輯其實很簡單,為了減少篇幅我把一些型別檢查的程式碼去掉了。每次呼叫subscribe的時候傳入一個回撥函式,subscribe會把它放到一個監聽者佇列中去,並返回一個unsubscribe的方法。這個unsubscribe方法是讓開發者可以方便地從列表中刪除對應的回撥函式,此外該方法還維護著一個isSubscribed標識訂閱狀態。

這裡面有一個比較有意思的ensureCanMutateNextListeners的方法,按照程式碼的邏輯,它是要保證監聽者的新增與刪除並不在currentListeners這個原始的佇列裡面進行直接操作,我們操作的只是它的一個副本。直到我們呼叫dispatch方法進行分發的時候,currentListenersnextListeners才會再一次指向同一個物件,這個在後面的程式碼裡面會看到。

c. dispatch–低調的action分發者

dispatch方法是用來分發action的,可以把它理解成用於觸發資料更新的方法。它的核心實現也比較簡單

export default function createStore(reducer, preloadedState, enhancer) {
  ...
  function dispatch(action) {
    ....

    // 呼叫reducer
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    // 呼叫監聽者
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }
}
複製程式碼

我依舊把一些型別檢查的程式碼去掉,首先dispatch方法會以當前的狀態currentState以及我們定義的動作action作為引數來呼叫當前的reducer方法。另外它使用isDispatching變數來記錄分發的狀態,正在分發則設定為true。這裡需要注意的是我們的reducer方法將會被設定成一個純函式–它不會產生副作用,並且對於同樣的輸入它會返回同樣的輸出。換句話說它不會直接在原來狀態的基礎上進行修改,而是會直接返回一個新的狀態,並對原有狀態進行替換。

完成了上面這些之後我們會依次遍歷所有的監聽者,並且手動呼叫所有的回撥函式。這裡需要注意的是之前有講過的,訂閱/取消訂閱的時候我們會生成一個currentLIsteners的副本nextListeners並在它上面新增/刪除回撥函式。然而到了dispatch這一步他們會做一次同步,這樣他們就又會指向同一個物件了。

d. replaceReducer–替換當前的reducer

replaceReducer這個方法做的事情其實很簡單,它可以用新的reducer替換掉當前的reducer,並且分發一個替換的action,下面是原始碼

export default function createStore(reducer, preloadedState, enhancer) {
  .....
  function replaceReducer(nextReducer) {
    .....

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }
}
複製程式碼

據說這種方式在除錯環境下會用得比較多。在正式環境下一般都不會在中途更替reducer,以免得增加維護成本。

e. observable–觀察者

這個是比較讓我費解的一個功能了,然而Redux的資料中心居然把它作為api開放出來,我們門先貼原始碼

export default function createStore(reducer, preloadedState, enhancer) {
  ....
  function observable() {
    const outerSubscribe = subscribe
    return {
      ...
      subscribe(observer) {
        ....
        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }
}
複製程式碼

如果直接呼叫這個介面,它會返回一個物件,而物件裡面包含了subscribe方法,並且我們可以把一個包含next欄位(它是一個函式)的物件作為subscribe方法的引數,就可以在每次資料變動的時候以**當前狀態getState()**作為引數呼叫next所攜帶的函式。

這麼說有點拗口,可能給個例子會比較直觀

import $$observable from `symbol-observable`

......
const store = createStore(reducer)

const subObject = store[$$observable]()
subObject.subscribe({
  next: (a) => {
    console.log(a)
  }
})
複製程式碼

這樣就可以做到每次動作被分發的時候都會呼叫next所攜帶的方法,並列印出getState()的值。這種觀察者模式的寫法有什麼特殊的意義我也還沒有時間去深究,似乎是草案的一部分,估計目前用的也不多,先不深入探究了。

尾聲

這篇文章的原標題是Redux原始碼分析,但由於本人概括能力有限,感覺只用一篇文章要分析完整個Redux的原始碼有點艱難,所以最後還是決定拆分,這篇文章主要講解Redux的資料中心store到底是什麼玩意,分別對store開放的api進行原始碼分析,簡單瞭解了一下它的工作原理。

Happy Coding and Writing!!

相關文章