逐行閱讀redux原始碼(一) createStore

santree發表於2018-11-08

寫在最前面

本文面對有redux使用經驗,熟知redux用法且想了解redux到底是什麼樣的一個工具的讀者,so,希望你有一定的:

  • 工程結構基礎
  • redux(react-redux)使用基礎

這會幫助你更快的理解。

redux是什麼

Redux是一個應用狀態管理工具,其工作流程可以參照下圖:

image

從圖中可以大概瞭解,通過user觸發(dispatch)的行為(action),redux會在通過middleware以及reducer的處理後更新整個狀態樹(state),從而達到更新檢視view的目標。這就是Redux的工作流程,接下來讓我們慢慢細說這之中到底發生了什麼。

從index開始

找到根源

首先我們開啟redux的github倉庫,檢視整個專案的目錄結構:

.
+-- .github/ISSUE_TEMPLATE  // GITHUB issue 模板
|   +-- Bug_report.md  // bug 提交模板
|   +-- Custom.md    // 通用模板
+-- build
|   +-- gitbooks.css // 未知,猜測為gitbook的樣式
+-- docs             // redux的文件目錄,本文不展開詳細
+-- examples         // redux的使用樣例,本文不展開詳細
+-- logo             // redux的logo靜態資源目錄
+-- src              // redux的核心內容目錄
|   +-- utils        // redux的核心工具庫
|   |   +-- actionTypes.js // 一些預設的隨機actionTypes
|   |   +-- isPlainObject.js // 判斷是否是字面變數或者new出來的object
|   |   +-- warning.js // 列印警告的工具類
|   +-- applyMiddleware.js // 神祕的魔法
|   +-- bindActionCreator.js // 神祕的魔法
|   +-- combineReducers.js // 神祕的魔法
|   +-- compose.js // 神祕的魔法
|   +-- createStore.js // 神祕的魔法
|   +-- index.js // 神祕的魔法
+-- test            // redux 測試用例
+-- .bablerc.js // bable編譯配置
+-- .editorconfig   // 編輯器配置,方便使用者在使用不同IDE時進行統一
+-- .eslintignore   // eslint忽略的檔案目錄宣告
+-- .eslintrc.js    // eslint檢查配置
+-- .gitbook.yaml   // gitbook的生成配置
+-- .gitignore      // git提交忽略的檔案目錄宣告
+-- .prettierrc.json  // prettier程式碼自動重新格式化配置
+-- .travis.yml     // travis CI的配置工具
+-- index.d.ts       // redux的typescript變數宣告
+-- package.json     // npm 命令以及包管理
+-- rollup.config.js  // rollup打包編譯配置
複製程式碼

當然,實際上redux的工程目錄中還包括了許多的md文件,這些我們也就不一一贅述了,我們要關注的是redux的根源到底在哪,那就讓我們從package.json開始吧:

"scripts": {
    "clean": "rimraf lib dist es coverage",
    "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"",
    "lint": "eslint src test",
    "pretest": "npm run build",
    "test": "jest",
    "test:watch": "npm test -- --watch",
    "test:cov": "npm test -- --coverage",
    "build": "rollup -c",
    "prepare": "npm run clean && npm run format:check && npm run lint && npm test",
    "examples:lint": "eslint examples",
    "examples:test": "cross-env CI=true babel-node examples/testAll.js"
},
複製程式碼

package.json中我們可以找到其npm命令配置,我們可以發現reduxbuild(專案打包)命令使用了rollup進行打包編譯(不瞭解rollup的同學請看這裡),那麼我們的目光就可以轉向到rollup的配置檔案rollup.config.js中來尋找redux的根源到底在哪裡,通過閱讀config檔案,我們能找到如下程式碼:

{
    input: 'src/index.js', // 入口檔案
    output: { file: 'lib/redux.js', format: 'cjs', indent: false },
    external: [
      ...Object.keys(pkg.dependencies || {}),
      ...Object.keys(pkg.peerDependencies || {})
    ],
    plugins: [babel()]
},
複製程式碼

這裡為我們指明瞭整個專案的入口:src/index.js,根源也就在此,神祕的魔法也揭開了一點面紗,接下來,不妨讓我們更進一步:

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'
複製程式碼

首先是index的依賴部分,我們可以看到其使用了同目錄下的createStore、combineReducers、bindActionCreators、applyMiddleware、compose這幾個模組,同時引入了utils資料夾下的工具模組warning、__DO_NOT_USE__ActionTypes,這兩個工具類顯而易見一個是用來進行列印警告,另一個是用來宣告不能夠使用的預設actionTypes的,接下來看看我們的index到底做了什麼:

function isCrushed() {}

if (
  process.env.NODE_ENV !== 'production' &&
  typeof isCrushed.name === 'string' &&
  isCrushed.name !== 'isCrushed'
) {
  warning(
    ...
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}
複製程式碼

首先讓我們注意到這個宣告的空函式isCrushed,這其實是一個斷言函式,因為在進行產品級(production)構建的時候,這種函式名都會被混淆,反言之如果這個函式被混淆了,其name已經不是isCrushed,但是你的環境卻不是production,也就是說你在dev環境下跑的卻是生產環境下的redux,如果出現這種情況,redux會進行提示。接下來便是 export 的時間,我們會看到,這裡把之前引入了的createStore、combineReducers、bindActionCreators、applyMiddleware、compose以及__DO_NOT_USE__ActionTypes。這些就是我們在使用redux的時候,經常會用的一些API和常量。接下來讓我們繼續追根溯源,一個一個慢慢詳談。

createStore

首先,讓我們看看我們宣告 redux store 的方法createStore,正如大家所知,我們每次去初始化redux的store時,都會這樣使用:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

// 在reducers中,我們使用了combinedReducer將多個reducer合併成了一個並export
// 使用 thunk 中介軟體讓dispatch接受函式,方便非同步操作,在此文不過於贅述

export default createStore(rootReducer, applyMiddleware(thunk));
複製程式碼

那麼createStore到底是怎麼去實現的呢?讓我們先找到createStore函式

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

接受引數

首先從其接受引數談起吧:

  • reducer 一個函式,可以通過接受一個state tree然後返回一個新的state tree
  • preloadedState 初始化的時候生成的state tree
  • enhancer 一個為redux提供增強功能的函式

createStore之前

在函式的頂部,會有一大段的判斷:

if (
    (typeof preloadedState === 'function' && typeof enhancer === 'function') ||
    (typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
    throw new Error(
      '...'
    )
}

if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState
    preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('...')
    }

    return enhancer(createStore)(reducer, preloadedState)
}

if (typeof reducer !== 'function') {
    throw new Error('...')
}
複製程式碼

通過這些判斷,我們能發現createStore的一些小規則:

  • 第二個引數preloadedState和第三個引數enhancer不能同時為函式型別
  • 不能存在第四個引數,且該引數為函式型別
  • 在不宣告preloadedState的狀態下可以直接用enhancer代替preloadedState,該情況下preloadedState預設為undefined
  • 如果存在enhancer,且其為函式的情況下,會呼叫使用createStore作為引數的enhancer高階函式對原有createState進行處理,並終止之後的createStore流程
  • reducer必須為函式。

當滿足這些規則之後,我們方才正式進入createStore的流程。

開始createStore

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
複製程式碼

接下來便是對函式類的初始變數的宣告,我們可以清楚的看見,reducerpreloadedState都被儲存到了當前函式中的變數裡,此外還宣告瞭當前的監聽事件的佇列,和一個用來標識當前正在dispatch的狀態值isDispatching

然後在接下來,我們先跳過在原始碼中作為工具使用的函式,直接進入正題:

在首當其衝的subscribe方法之前,我們不妨先瞧瞧用來在觸發subscribe(訂閱)的監聽事件listenerdispatch

function dispatch(action) {
    // action必須是一個物件
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    
    // action必須擁有一個type
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    
    // 如果正在dispatching,那麼不執行dispatch操作。
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    
    // 設定dispatching狀態為true,並使用reducer生成新的狀態樹。
    try {
      isDispatching = true
      currentState = currentReducer(currentState, action)
    } finally {
      // 當獲取新的狀態樹完成後,設定狀態為false.
      isDispatching = false
    }
    
    // 將目前最新的監聽方法放置到即將執行的佇列中遍歷並且執行
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }
    
    // 將觸發的action返回 
    return action
}
複製程式碼

根據上面的程式碼,我們會發現我們註冊的監聽事件會在狀態樹更新之後進行遍歷呼叫,這個時候我們再來繼續看subscribe函式:

function subscribe(listener) {
    // listener必須為函式
    if (typeof listener !== 'function') {
      throw new Error(...)
    }
    
    // 如果正在dispatch中則拋錯
    if (isDispatching) {
      throw new Error(
        ...
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

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

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }
複製程式碼

在這裡我們就會用到一個方法ensureCanMutateNextListeners,這個方法是用來做什麼的呢?讓我們看看程式碼:

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

在定義變數的階段,我們發現我們將currentListeners定義為了[],並將nextLiteners指向了這個currentListeners的引用(如果不清楚引用賦值和傳值賦值的區別的同學請看這裡),也就是說如果我改變nextListeners,那麼也會同步改變currentListeners,這樣會造成我們完全無法區分當前正在執行的監聽佇列和上一次的監聽佇列,而ensureCanMutateNextListeners正是為了將其區分開來的一步處理。

再經過這樣的處理之後,每次執行監聽佇列裡的函式之前,currentListeners始終是上一次的執行dispatch時的nextListeners

// 將目前最新的監聽方法放置到即將執行的佇列中遍歷並且執行
const listeners = (currentListeners = nextListeners)
複製程式碼

只有當再次執行subscribe去更新nextListeners和後,再次執行dispatch這個currentListeners才會被更新。因此,我們需要注意:

  • listener中執行unsubscribe是不會立即生效的,因為每次dispatch執行監聽佇列的函式使用的佇列都是執行dispatchnextListeners的快照,你在函式裡更新的佇列要下次dispatch才會執行,所以儘量保證unsubscribesubscribedispatch之前執行,這樣才能保證每次使用的監聽佇列都是最新的。
  • listener執行時,直接取到的狀態樹可能並非最新的狀態樹,因為你的listener並不能清楚在其執行的過程中是否又執行了dispatch(),所以我們需要一個方法:
function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }
    
    return currentState
}
複製程式碼

來獲取當前真實完整的state.

通過以上程式碼,我相信大家已經對subscribedispatch以及listener已經有一定的認識,那麼讓我們繼續往下看:

function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('...')
    }

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

這是redux丟擲的一個方法,其作用是替換當前整個redux中正在執行的reducer為新傳入的reducer,同時其會預設觸發一次內建的replace事件。

接下來便是最後的波紋(霧,在這個方法裡,其提供了一個預留給遵循observable/reactive(觀察者模式/響應式程式設計)的類庫用於互動的api,我們可以看看這個api程式碼的核心部分:

const outerSubscribe = subscribe
return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

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

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

這裡的outerSubscribe就是之前redux暴露的的subscribe方法,當外部的類庫使用暴露物件中的subscribe方法進行訂閱時,其始終能通過其傳入的觀察者物件,獲取當前最新的state(通過其觀察者物件上的nextgetState方法),同時其也將類庫獲取最新的state的方法放入了redux的監聽佇列nextListeners中,以期每次發生dispatch操作的時候,都會去通知該觀察者狀態樹的更新,最後又返回了取消該訂閱的方法(subscribe方法的返回值就是取消當前訂閱的方法)。

至此,createStore的面紗終於完全被揭開,我們現在終於認識了所有createStore的方法:

  • dispatch用於觸發action,通過reducerstate更新
  • subscribe用於訂閱dispatch,當使用dispatch時,會通知所有的訂閱者,並執行其內部的listener
  • getState用於獲取當前redux中最新的狀態樹
  • replaceReducer用於將當前redux中的reducer進行替換,並且其會觸發預設的內建REPLACE action.
  • [$$observable]([Symbol.observable])(不瞭解Symbol.observable的同學可以看這裡),其可以提供observable/reactive(觀察者模式/響應式程式設計)類庫以訂閱reduxdispatch方法的途徑,每當dispatch時都會將最新的state傳遞給訂閱的observer(觀察者)

結語

在工作之餘斷斷續續的書寫中通讀redux原始碼的第一篇終於完成,通過一個方法一個方法的分析,雖然有諸多缺漏,但是筆者也算是從其中加深了對redux的理解,希望本文也能給諸位也帶來一些讀原始碼的思路和對redux的認識。

非常感謝你的閱讀~

相關文章