逐行閱讀redux原始碼(一) createStore
寫在最前面
本文面對有redux使用經驗,熟知redux用法且想了解redux到底是什麼樣的一個工具的讀者,so,希望你有一定的:
- 工程結構基礎
- redux(react-redux)使用基礎
這會幫助你更快的理解。
redux是什麼
Redux是一個應用狀態管理工具,其工作流程可以參照下圖:
從圖中可以大概瞭解,通過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 // 神祕的魔法
| +-- bind ActionCreator.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命令配置,我們可以發現redux
的build(專案打包)
命令使用了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
複製程式碼
接下來便是對函式類的初始變數的宣告,我們可以清楚的看見,reducer
和preloadedState
都被儲存到了當前函式中的變數裡,此外還聲明瞭當前的監聽事件的佇列,和一個用來標識當前正在dispatch
的狀態值isDispatching
。
然後在接下來,我們先跳過在原始碼中作為工具使用的函式,直接進入正題:
在首當其衝的subscribe
方法之前,我們不妨先瞧瞧用來在觸發subscribe(訂閱)
的監聽事件listener
的dispatch
:
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
執行監聽佇列的函式使用的佇列都是執行dispatch
時nextListeners
的快照,你在函式裡更新的佇列要下次dispatch
才會執行,所以儘量保證unsubscribe
和subscribe
在dispatch
之前執行,這樣才能保證每次使用的監聽佇列都是最新的。 - 在
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
.
通過以上程式碼,我相信大家已經對subscribe
和dispatch
以及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
(通過其觀察者物件上的next
和getState
方法),同時其也將類庫獲取最新的state的方法放入了redux
的監聽佇列nextListeners
中,以期每次發生dispatch
操作的時候,都會去通知該觀察者狀態樹的更新,最後又返回了取消該訂閱的方法(subscribe
方法的返回值就是取消當前訂閱的方法)。
至此,createStore的面紗終於完全被揭開,我們現在終於認識了所有createStore
的方法:
dispatch
用於觸發action,通過reducer
將state
更新subscribe
用於訂閱dispatch
,當使用dispatch
時,會通知所有的訂閱者,並執行其內部的listener
getState
用於獲取當前redux
中最新的狀態樹replaceReducer
用於將當前redux
中的reducer
進行替換,並且其會觸發預設的內建REPLACE
action.[$$observable]([Symbol.observable])
(不瞭解Symbol.observable的同學可以看這裡),其可以提供observable/reactive(觀察者模式/響應式程式設計)
類庫以訂閱redux
中dispatch
方法的途徑,每當dispatch
時都會將最新的state
傳遞給訂閱的observer(觀察者)
。
結語
在工作之餘斷斷續續的書寫中通讀redux原始碼的第一篇終於完成,通過一個方法一個方法的分析,雖然有諸多缺漏,但是筆者也算是從其中加深了對redux
的理解,希望本文也能給諸位也帶來一些讀原始碼的思路和對redux
的認識。
非常感謝你的閱讀~