本文面對有redux使用經驗,熟知redux用法且想了解redux究竟是什麼樣的一個工具的讀者,so,但願你有必定的:css
這會幫助你更快的理解。html
Redux是一個應用狀態管理工具,其工做流程能夠參照下圖: node
從圖中能夠大概瞭解,經過user觸發(dispatch
)的行爲(action
),redux會在經過middleware
以及reducer
的處理後更新整個狀態樹(state
),從而達到更新視圖view
的目標。這就是Redux的工做流程,接下來讓咱們慢慢細說這之中到底發生了什麼。react
首先咱們打開redux的github倉庫,查看整個項目的目錄結構:git
.
+-- .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
開始吧:github
"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文件,咱們能找到以下代碼:typescript
{
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
,根源也就在此,神祕的魔法也揭開了一點面紗,接下來,不妨讓咱們更進一步:npm
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和常量。接下來讓咱們繼續追根溯源,一個一個慢慢詳談。json
首先,讓咱們看看咱們聲明 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
提供加強功能的函數
在函數的頂部,會有一大段的判斷:
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的流程。
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
的認識。
很是感謝你的閱讀~