學習 vuex 源碼整體架構(gòu),打造屬于自己的狀態(tài)管理庫
前言
這是學習源碼整體架構(gòu)第五篇。整體架構(gòu)這詞語好像有點大,姑且就算是源碼整體結(jié)構(gòu)吧,主要就是學習是代碼整體結(jié)構(gòu),不深究其他不是主線的具體函數(shù)的實現(xiàn)。本篇文章學習的是實際倉庫的代碼。
其余四篇分別是:
感興趣的讀者可以點擊閱讀。下一篇可能是學習 axios 源碼。
導讀
文章比較詳細的介紹了vuex、vue源碼調(diào)試方法和 Vuex 原理。并且詳細介紹了 Vuex.use 安裝和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatch、commit等)的實現(xiàn)和輔助函數(shù) mapState、mapGetters、 mapActions、mapMutations createNamespacedHelpers。
chrome 瀏覽器調(diào)試 vuex 源碼方法
Vue文檔:在 VS Code 中調(diào)試 Vue 項目
從上文中同理可得調(diào)試 vuex 方法,這里詳細說下,便于幫助到可能不知道如何調(diào)試源碼的讀者。
可以把筆者的這個 vuex-analysis 源碼分析倉庫fork一份或者直接克隆下來, git clone https://github.com/lxchuan12/vuex-analysis.git
其中文件夾
vuex,是克隆官方的vuex倉庫dev分支。
截至目前(2019年11月),版本是v3.1.2,最后一次commit是ba2ff3a3,2019-11-11 11:51 Ben Hutton。
包含筆者的注釋,便于理解。
克隆完成后, 在vuex/examples/webpack.config.js 中添加devtool配置。
// 新增devtool配置,便于調(diào)試
devtool: 'source-map',
output: {}
git clone https://github.com/lxchuan12/vuex-analysis.git
cd vuex
npm i
npm run dev
打開 http://localhost:8080/
點擊你想打開的例子,例如:Shopping Cart => http://localhost:8080/shopping-cart/
打開控制面板 source 在左側(cè)找到 webapck// . src 目錄 store 文件 根據(jù)自己需求斷點調(diào)試即可。
本文主要就是通過Shopping Cart,(路徑vuex/examples/shopping-cart)例子調(diào)試代碼的。
順便提一下調(diào)試 vue 源碼(v2.6.10)的方法
git clone https://github.com/vuejs/vue.git
克隆下來后將package.json 文件中的script dev命令后面添加這個 --sourcemap。
{
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap"
}
git clone https://github.com/vuejs/vue.git
cd vue
npm i
# 在 dist/vue.js 最后一行追加一行 //# sourceMappingURL=vue.js.map
npm run dev
# 新終端窗口
# 根目錄下 全局安裝http-server(一行命令啟動服務的工具)
npm i -g http-server
hs -p 8100
# 在examples 文件夾中把引用的vuejs的index.html 文件 vue.min.js 改為 vue.js
# 或者把dist文件夾的 vue.min.js ,替換成npm run dev編譯后的dist/vue.js
# 瀏覽器打開 open http://localhost:8100/examples/
# 打開控制面板 source 在左側(cè)找到 src 目錄 即vue.js源碼文件 根據(jù)自己需求斷點調(diào)試即可。
本小節(jié)大篇幅介紹調(diào)試方法。是因為真的很重要。會調(diào)試代碼,看源碼就比較簡單了。關(guān)注主線調(diào)試代碼,很容易看懂。
強烈建議克隆筆者的這個倉庫,自己調(diào)試代碼,對著注釋看,不調(diào)試代碼,只看文章不容易吸收消化。
筆者也看了文章末尾筆者推薦閱讀的文章,但還是需要自己看源代碼,才知道這些文章哪里寫到了,哪里沒有細寫。
正文開始~
vuex 原理
簡單說明下 vuex 原理
<template>
<div>
count {{$store.state.count}}
</div>
</template>
每個組件(也就是Vue實例)在beforeCreate的生命周期中都混入(Vue.mixin)同一個Store實例 作為屬性 $store, 也就是為啥可以通過 this.$store.dispatch 等調(diào)用方法的原因。
最后顯示在模板里的 $store.state.count 源碼是這樣的。
class Store{
get state () {
return this._vm._data.$$state
}
}
其實就是: vm.$store._vm._data.$$state.count 其中vm.$store._vm._data.$$state 是 響應式的。怎么實現(xiàn)響應式的?其實就是new Vue()
function resetStoreVM (store, state, hot) {
// 省略若干代碼
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 省略若干代碼
}
這里的 state 就是 用戶定義的 state。這里的 computed 就是處理后的用戶定義的 getters。而 class Store上的一些函數(shù)(API)主要都是圍繞修改vm.$store._vm._data.$$state和computed(getter)服務的。
Vue.use 安裝
筆者畫了一張圖表示下Vuex對象,是Vue的一個插件。

看到這里,恭喜你已經(jīng)了解了
Vuex原理。文章比較長,如果暫時不想關(guān)注源碼細節(jié),可以克隆一下本倉庫代碼git clone https://github.com/lxchuan12/vuex-analysis.git,后續(xù)調(diào)試代碼,點贊收藏到時想看了再看。
文檔 Vue.use Vue.use(Vuex)
參數(shù):{Object | Function} plugin 用法:
安裝 Vue.js 插件。如果插件是一個對象,必須提供install方法。如果插件是一個函數(shù),它會被作為install方法。install方法調(diào)用時,會將 Vue 作為參數(shù)傳入。
該方法需要在調(diào)用new Vue()之前被調(diào)用。
當install方法被同一個插件多次調(diào)用,插件將只會被安裝一次。
根據(jù)斷點調(diào)試,來看下Vue.use的源碼。
function initUse (Vue) {
Vue.use = function (plugin) {
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
// 如果已經(jīng)存在,則直接返回this也就是Vue
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
var args = toArray(arguments, 1);
// 把 this(也就是Vue)作為數(shù)組的第一項
args.unshift(this);
// 如果插件的install屬性是函數(shù),調(diào)用它
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
// 如果插件是函數(shù),則調(diào)用它
// apply(null) 嚴格模式下 plugin 插件函數(shù)的 this 就是 null
plugin.apply(null, args);
}
// 添加到已安裝的插件
installedPlugins.push(plugin);
return this
};
}
install 函數(shù)
vuex/src/store.js
export function install (_Vue) {
// Vue 已經(jīng)存在并且相等,說明已經(jīng)Vuex.use過
if (Vue && _Vue === Vue) {
// 省略代碼:非生產(chǎn)環(huán)境報錯,vuex已經(jīng)安裝
return
}
Vue = _Vue
applyMixin(Vue)
}
接下來看 applyMixin 函數(shù)
applyMixin 函數(shù)
vuex/src/mixin.js
export default function (Vue) {
// Vue 版本號
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 合并選項后 beforeCreate 是數(shù)組里函數(shù)的形式 [?, ?]
// 最后調(diào)用循環(huán)遍歷這個數(shù)組,調(diào)用這些函數(shù),這是一種函數(shù)與函數(shù)合并的解決方案。
// 假設(shè)是我們自己來設(shè)計,會是什么方案呢。
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 省略1.x的版本代碼 ...
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
// store 注入到每一個Vue的實例中
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
最終每個Vue的實例對象,都有一個$store屬性。且是同一個Store實例。
用購物車的例子來舉例就是:
const vm = new Vue({
el: '#app',
store,
render: h => h(App)
})
console.log('vm.$store === vm.$children[0].$store', vm.$store === vm.$children[0].$store)
// true
console.log('vm.$store === vm.$children[0].$children[0].$store', vm.$store === vm.$children[0].$children[0].$store)
// true
console.log('vm.$store === vm.$children[0].$children[1].$store', vm.$store === vm.$children[0].$children[1].$store)
// true
Vuex.Store 構(gòu)造函數(shù)
先看最終 new Vuex.Store 之后的 Store 實例對象關(guān)系圖:先大致有個印象。

export class Store {
constructor (options = {}) {
// 這個構(gòu)造函數(shù)比較長,這里省略,后文分開細述
}
}
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
如果是 cdn script 方式引入vuex插件,則自動安裝vuex插件,不需要用Vue.use(Vuex)來安裝。
// asset 函數(shù)實現(xiàn)
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
if (process.env.NODE_ENV !== 'production') {
// 可能有讀者會問:為啥不用 console.assert,console.assert 函數(shù)報錯不會阻止后續(xù)代碼執(zhí)行
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `store must be called with the new operator.`)
}
條件斷言:不滿足直接拋出錯誤
1.必須使用
Vue.use(Vuex)創(chuàng)建store實例。
2.當前環(huán)境不支持Promise,報錯:vuex需要Promise polyfill。
3.Store函數(shù)必須使用new操作符調(diào)用。
const {
// 插件默認是空數(shù)組
plugins = [],
// 嚴格模式默認是false
strict = false
} = options
從用戶定義的new Vuex.Store(options) 取出plugins和strict參數(shù)。
// store internal state
// store 實例對象 內(nèi)部的 state
this._committing = false
// 用來存放處理后的用戶自定義的actoins
this._actions = Object.create(null)
// 用來存放 actions 訂閱
this._actionSubscribers = []
// 用來存放處理后的用戶自定義的mutations
this._mutations = Object.create(null)
// 用來存放處理后的用戶自定義的 getters
this._wrappedGetters = Object.create(null)
// 模塊收集器,構(gòu)造模塊樹形結(jié)構(gòu)
this._modules = new ModuleCollection(options)
// 用于存儲模塊命名空間的關(guān)系
this._modulesNamespaceMap = Object.create(null)
// 訂閱
this._subscribers = []
// 用于使用 $watch 觀測 getters
this._watcherVM = new Vue()
// 用來存放生成的本地 getters 的緩存
this._makeLocalGettersCache = Object.create(null)
聲明Store實例對象一些內(nèi)部變量。用于存放處理后用戶自定義的actions、mutations、getters等變量。
提一下
Object.create(null)和{}的區(qū)別。前者沒有原型鏈,后者有。即Object.create(null).__proto__是undefined({}).__proto__是Object.prototype
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
給自己 綁定 commit 和 dispatch
為何要這樣綁定 ?
說明調(diào)用commit和dispach的this不一定是store實例
這是確保這兩個函數(shù)里的this是store實例
// 嚴格模式,默認是false
this.strict = strict
// 根模塊的state
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
上述這段代碼 installModule(this, state, [], this._modules.root)
初始化 根模塊。
并且也遞歸的注冊所有子模塊。
并且收集所有模塊的getters放在this._wrappedGetters里面。
resetStoreVM(this, state)
初始化
store._vm響應式的
并且注冊_wrappedGetters作為computed的屬性
plugins.forEach(plugin => plugin(this))
插件:把實例對象 store 傳給插件函數(shù),執(zhí)行所有插件。
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
初始化 vue-devtool 開發(fā)工具。
參數(shù) devtools 傳遞了取 devtools 否則取Vue.config.devtools 配置。
初讀這個構(gòu)造函數(shù)的全部源代碼。會發(fā)現(xiàn)有三個地方需要重點看。分別是:
this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
閱讀時可以斷點調(diào)試,賦值語句this._modules = new ModuleCollection(options),如果暫時不想看,可以直接看返回結(jié)果。installModule,resetStoreVM函數(shù)則可以斷點調(diào)試。
class ModuleCollection
收集模塊,構(gòu)造模塊樹結(jié)構(gòu)。
注冊根模塊 參數(shù)
rawRootModule也就是Vuex.Store的options參數(shù)
未加工過的模塊(用戶自定義的),根模塊
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
}
/**
* 注冊模塊
* @param {Array} path 路徑
* @param {Object} rawModule 原始未加工的模塊
* @param {Boolean} runtime runtime 默認是 true
*/
register (path, rawModule, runtime = true) {
// 非生產(chǎn)環(huán)境 斷言判斷用戶自定義的模塊是否符合要求
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
// 遞歸注冊子模塊
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
class Module
// Base data struct for store's module, package with some attribute and method
// store 的模塊 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),包括一些屬性和方法
export default class Module {
constructor (rawModule, runtime) {
// 接收參數(shù) runtime
this.runtime = runtime
// Store some children item
// 存儲子模塊
this._children = Object.create(null)
// Store the origin module object which passed by programmer
// 存儲原始未加工的模塊
this._rawModule = rawModule
// 模塊 state
const rawState = rawModule.state
// Store the origin module's state
// 原始Store 可能是函數(shù),也可能是是對象,是假值,則賦值空對象。
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
經(jīng)過一系列的注冊后,最后 this._modules = new ModuleCollection(options) this._modules 的值是這樣的。筆者畫了一張圖表示:

installModule 函數(shù)
function installModule (store, rootState, path, module, hot) {
// 是根模塊
const isRoot = !path.length
// 命名空間 字符串
const namespace = store._modules.getNamespace(path)
if (module.namespaced) {
// 省略代碼:模塊命名空間map對象中已經(jīng)有了,開發(fā)環(huán)境報錯提示重復
// module 賦值給 _modulesNamespaceMap[namespace]
store._modulesNamespaceMap[namespace] = module
}
// ... 后續(xù)代碼 移出來 待讀解釋
}
注冊 state
// set state
// 不是根模塊且不是熱重載
if (!isRoot && !hot) {
// 獲取父級的state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 模塊名稱
// 比如 cart
const moduleName = path[path.length - 1]
// state 注冊
store._withCommit(() => {
// 省略代碼:非生產(chǎn)環(huán)境 報錯 模塊 state 重復設(shè)置
Vue.set(parentState, moduleName, module.state)
})
}
最后得到的是類似這樣的結(jié)構(gòu)且是響應式的數(shù)據(jù) 實例 Store.state 比如:
{
// 省略若干屬性和方法
// 這里的 state 是只讀屬性 可搜索 get state 查看,上文寫過
state: {
cart: {
checkoutStatus: null,
items: []
}
}
}
const local = module.context = makeLocalContext(store, namespace, path)
module.context這個賦值主要是給helpers中mapState、mapGetters、mapMutations、mapActions四個輔助函數(shù)使用的。
生成本地的dispatch、commit、getters和state。
主要作用就是抹平差異化,不需要用戶再傳模塊參數(shù)。
遍歷注冊 mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
/**
* 注冊 mutation
* @param {Object} store 對象
* @param {String} type 類型
* @param {Function} handler 用戶自定義的函數(shù)
* @param {Object} local local 對象
*/
function registerMutation (store, type, handler, local) {
// 收集的所有的mutations找對應的mutation函數(shù),沒有就賦值空數(shù)組
const entry = store._mutations[type] || (store._mutations[type] = [])
// 最后 mutation
entry.push(function wrappedMutationHandler (payload) {
/**
* mutations: {
* pushProductToCart (state, { id }) {
* console.log(state);
* }
* }
* 也就是為什么用戶定義的 mutation 第一個參數(shù)是state的原因,第二個參數(shù)是payload參數(shù)
*/
handler.call(store, local.state, payload)
})
}
遍歷注冊 action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
/**
* 注冊 mutation
* @param {Object} store 對象
* @param {String} type 類型
* @param {Function} handler 用戶自定義的函數(shù)
* @param {Object} local local 對象
*/
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
// payload 是actions函數(shù)的第二個參數(shù)
entry.push(function wrappedActionHandler (payload) {
/**
* 也就是為什么用戶定義的actions中的函數(shù)第一個參數(shù)有
* { dispatch, commit, getters, state, rootGetters, rootState } 的原因
* actions: {
* checkout ({ commit, state }, products) {
* console.log(commit, state);
* }
* }
*/
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
/**
* export function isPromise (val) {
return val && typeof val.then === 'function'
}
* 判斷如果不是Promise Promise 化,也就是為啥 actions 中處理異步函數(shù)
也就是為什么構(gòu)造函數(shù)中斷言不支持promise報錯的原因
vuex需要Promise polyfill
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
*/
if (!isPromise(res)) {
res = Promise.resolve(res)
}
// devtool 工具觸發(fā) vuex:error
if (store._devtoolHook) {
// catch 捕獲錯誤
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
// 拋出錯誤
throw err
})
} else {
// 然后函數(shù)執(zhí)行結(jié)果
return res
}
})
}
遍歷注冊 getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
/**
* 注冊 getter
* @param {Object} store Store實例
* @param {String} type 類型
* @param {Object} rawGetter 原始未加工的 getter 也就是用戶定義的 getter 函數(shù)
* @examples 比如 cartProducts: (state, getters, rootState, rootGetters) => {}
* @param {Object} local 本地 local 對象
*/
function registerGetter (store, type, rawGetter, local) {
// 類型如果已經(jīng)存在,報錯:已經(jīng)存在
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// 否則:賦值
store._wrappedGetters[type] = function wrappedGetter (store) {
/**
* 這也就是為啥 getters 中能獲取到 (state, getters, rootState, rootGetters) 這些值的原因
* getters = {
* cartProducts: (state, getters, rootState, rootGetters) => {
* console.log(state, getters, rootState, rootGetters);
* }
* }
*/
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
遍歷注冊 子模塊
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
resetStoreVM 函數(shù)
resetStoreVM(this, state, hot)
初始化
store._vm響應式的
并且注冊_wrappedGetters作為computed的屬性
function resetStoreVM (store, state, hot) {
// 存儲一份老的Vue實例對象 _vm
const oldVm = store._vm
// bind store public getters
// 綁定 store.getter
store.getters = {}
// reset local getters cache
// 重置 本地getters的緩存
store._makeLocalGettersCache = Object.create(null)
// 注冊時收集的處理后的用戶自定義的 wrappedGetters
const wrappedGetters = store._wrappedGetters
// 聲明 計算屬性 computed 對象
const computed = {}
// 遍歷 wrappedGetters 賦值到 computed 上
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
/**
* partial 函數(shù)
* 執(zhí)行函數(shù) 返回一個新函數(shù)
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
*/
computed[key] = partial(fn, store)
// getter 賦值 keys
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
// 可以枚舉
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
// 使用一個 Vue 實例對象存儲 state 樹
// 阻止警告 用戶添加的一些全局mixins
// 聲明變量 silent 存儲用戶設(shè)置的靜默模式配置
const silent = Vue.config.silent
// 靜默模式開啟
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 把存儲的靜默模式配置賦值回來
Vue.config.silent = silent
// enable strict mode for new vm
// 開啟嚴格模式 執(zhí)行這句
// 用 $watch 觀測 state,只能使用 mutation 修改 也就是 _withCommit 函數(shù)
if (store.strict) {
enableStrictMode(store)
}
// 如果存在老的 _vm 實例
if (oldVm) {
// 熱加載為 true
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
// 設(shè)置 oldVm._data.$$state = null
store._withCommit(() => {
oldVm._data.$$state = null
})
}
// 實例銷毀
Vue.nextTick(() => oldVm.$destroy())
}
}
到此,構(gòu)造函數(shù)源代碼看完了,接下來看 Vuex.Store 的 一些 API 實現(xiàn)。
Vuex.Store 實例方法
Vuex API 文檔
commit
提交 mutation。
commit (_type, _payload, _options) {
// check object-style commit
// 統(tǒng)一成對象風格
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 取出處理后的用戶定義 mutation
const entry = this._mutations[type]
// 省略 非生產(chǎn)環(huán)境的警告代碼 ...
this._withCommit(() => {
// 遍歷執(zhí)行
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 訂閱 mutation 執(zhí)行
this._subscribers.forEach(sub => sub(mutation, this.state))
// 省略 非生產(chǎn)環(huán)境的警告代碼 ...
}
commit 支持多種方式。比如:
store.commit('increment', {
count: 10
})
// 對象提交方式
store.commit({
type: 'increment',
count: 10
})
unifyObjectStyle函數(shù)將參數(shù)統(tǒng)一,返回 { type, payload, options }。
dispatch
分發(fā) action。
dispatch (_type, _payload) {
// check object-style dispatch
// 獲取到type和payload參數(shù)
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
// 聲明 action 變量 等于 type和payload參數(shù)
const action = { type, payload }
// 入口,也就是 _actions 集合
const entry = this._actions[type]
// 省略 非生產(chǎn)環(huán)境的警告代碼 ...
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
return res
})
}
replaceState
替換 store 的根狀態(tài),僅用狀態(tài)合并或時光旅行調(diào)試。
replaceState (state) {
this._withCommit(() => {
this._vm._data.$$state = state
})
}
watch
響應式地偵聽 fn 的返回值,當值改變時調(diào)用回調(diào)函數(shù)。
/**
* 觀測某個值
* @param {Function} getter 函數(shù)
* @param {Function} cb 回調(diào)
* @param {Object} options 參數(shù)對象
*/
watch (getter, cb, options) {
if (process.env.NODE_ENV !== 'production') {
assert(typeof getter === 'function', `store.watch only accepts a function.`)
}
return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}
subscribe
訂閱 store 的 mutation。
subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
}
// 收集訂閱者
function genericSubscribe (fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
subscribeAction
訂閱 store 的 action。
subscribeAction (fn) {
const subs = typeof fn === 'function' ? { before: fn } : fn
return genericSubscribe(subs, this._actionSubscribers)
}
registerModule
注冊一個動態(tài)模塊。
/**
* 動態(tài)注冊模塊
* @param {Array|String} path 路徑
* @param {Object} rawModule 原始未加工的模塊
* @param {Object} options 參數(shù)選項
*/
registerModule (path, rawModule, options = {}) {
// 如果 path 是字符串,轉(zhuǎn)成數(shù)組
if (typeof path === 'string') path = [path]
// 省略 非生產(chǎn)環(huán)境 報錯代碼
// 手動調(diào)用 模塊注冊的方法
this._modules.register(path, rawModule)
// 安裝模塊
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
// 設(shè)置 resetStoreVM
resetStoreVM(this, this.state)
}
unregisterModule
卸載一個動態(tài)模塊。
/**
* 注銷模塊
* @param {Array|String} path 路徑
*/
unregisterModule (path) {
// 如果 path 是字符串,轉(zhuǎn)成數(shù)組
if (typeof path === 'string') path = [path]
// 省略 非生產(chǎn)環(huán)境 報錯代碼 ...
// 手動調(diào)用模塊注銷
this._modules.unregister(path)
this._withCommit(() => {
// 注銷這個模塊
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
// 重置 Store
resetStore(this)
}
hotUpdate
熱替換新的 action 和 mutation。
// 熱加載
hotUpdate (newOptions) {
// 調(diào)用的是 ModuleCollection 的 update 方法,最終調(diào)用對應的是每個 Module 的 update
this._modules.update(newOptions)
// 重置 Store
resetStore(this, true)
}
組件綁定的輔助函數(shù)
文件路徑:vuex/src/helpers.js
mapState
為組件創(chuàng)建計算屬性以返回 Vuex store 中的狀態(tài)。
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
// 非生產(chǎn)環(huán)境 判斷參數(shù) states 必須是數(shù)組或者是對象
if (process.env.NODE_ENV !== 'production' && !isValidMap(states)) {
console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
// 傳了參數(shù) namespace
if (namespace) {
// 用 namespace 從 store 中找一個模塊。
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// 標記為 vuex 方便在 devtools 顯示
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
normalizeNamespace 標準化統(tǒng)一命名空間
function normalizeNamespace (fn) {
return (namespace, map) => {
// 命名空間沒傳,交換參數(shù),namespace 為空字符串
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
// 如果是字符串,最后一個字符不是 / 添加 /
// 因為 _modulesNamespaceMap 存儲的是這樣的結(jié)構(gòu)。
/**
* _modulesNamespaceMap:
cart/: {}
products/: {}
}
* */
namespace += '/'
}
return fn(namespace, map)
}
}
// 校驗是否是map 是數(shù)組或者是對象。
function isValidMap (map) {
return Array.isArray(map) || isObject(map)
}
/**
* Normalize the map
* 標準化統(tǒng)一 map,最終返回的是數(shù)組
* normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
* normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
* @param {Array|Object} map
* @return {Object}
*/
function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
module.context 這個賦值主要是給 helpers 中 mapState、mapGetters、mapMutations、mapActions四個輔助函數(shù)使用的。
// 在構(gòu)造函數(shù)中 installModule 中
const local = module.context = makeLocalContext(store, namespace, path)
這里就是抹平差異,不用用戶傳遞命名空間,獲取到對應的 commit、dispatch、state、和 getters
getModuleByNamespace
function getModuleByNamespace (store, helper, namespace) {
// _modulesNamespaceMap 這個變量在 class Store installModule 函數(shù)中賦值的
const module = store._modulesNamespaceMap[namespace]
if (process.env.NODE_ENV !== 'production' && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
看完這些,最后舉個例子: vuex/examples/shopping-cart/components/ShoppingCart.vue
computed: {
...mapState({
checkoutStatus: state => state.cart.checkoutStatus
}),
}
沒有命名空間的情況下,最終會轉(zhuǎn)換成這樣
computed: {
checkoutStatus: this.$store.state.checkoutStatus
}
假設(shè)有命名空間'ruochuan',
computed: {
...mapState('ruochuan', {
checkoutStatus: state => state.cart.checkoutStatus
}),
}
則會轉(zhuǎn)換成:
computed: {
checkoutStatus: this.$store._modulesNamespaceMap.['ruochuan/'].context.checkoutStatus
}
mapGetters
為組件創(chuàng)建計算屬性以返回 getter 的返回值。
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
// 省略代碼:非生產(chǎn)環(huán)境 判斷參數(shù) getters 必須是數(shù)組或者是對象
normalizeMap(getters).forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
// 省略代碼:匹配不到 getter
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
舉例:
computed: {
...mapGetters('cart', {
products: 'cartProducts',
total: 'cartTotalPrice'
})
},
最終轉(zhuǎn)換成:
computed: {
products: this.$store.getters['cart/cartProducts'],
total: this.$store.getters['cart/cartTotalPrice'],
}
mapActions
創(chuàng)建組件方法分發(fā) action。
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
// 省略代碼:非生產(chǎn)環(huán)境 判斷參數(shù) actions 必須是數(shù)組或者是對象
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
mapMutations
創(chuàng)建組件方法提交 mutation。mapMutations 和 mapActions 類似,只是 dispatch 換成了 commit。
let commit = this.$store.commit
commit = module.context.commit
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
vuex/src/helpers
mapMutations、mapActions 舉例:
{
methods: {
...mapMutations(['inc']),
...mapMutations('ruochuan', ['dec']),
...mapActions(['actionA'])
...mapActions('ruochuan', ['actionB'])
}
}
最終轉(zhuǎn)換成
{
methods: {
inc(...args){
return this.$store.dispatch.apply(this.$store, ['inc'].concat(args))
},
dec(...args){
return this.$store._modulesNamespaceMap.['ruochuan/'].context.dispatch.apply(this.$store, ['dec'].concat(args))
},
actionA(...args){
return this.$store.commit.apply(this.$store, ['actionA'].concat(args))
}
actionB(...args){
return this.$store._modulesNamespaceMap.['ruochuan/'].context.commit.apply(this.$store, ['actionB'].concat(args))
}
}
}
由此可見:這些輔助函數(shù)極大地方便了開發(fā)者。
createNamespacedHelpers
創(chuàng)建基于命名空間的組件綁定輔助函數(shù)。
export const createNamespacedHelpers = (namespace) => ({
// bind(null) 嚴格模式下,napState等的函數(shù) this 指向就是 null
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
就是把這些輔助函數(shù)放在一個對象中。
插件
插件部分文件路徑是:vuex/src/plugins/devtoolvuex/src/plugins/logger
文章比較長了,這部分就不再敘述。具體可以看筆者的倉庫 vuex-analysis vuex/src/plugins/ 的源碼注釋。
總結(jié)
文章比較詳細的介紹了vuex、vue源碼調(diào)試方法和 Vuex 原理。并且詳細介紹了 Vuex.use 安裝和 new Vuex.Store 初始化、Vuex.Store 的全部API(如dispatch、commit等)的實現(xiàn)和輔助函數(shù) mapState、mapGetters、 mapActions、mapMutations createNamespacedHelpers。
文章注釋,在vuex-analysis源碼倉庫里基本都有注釋分析,求個star。再次強烈建議要克隆代碼下來。
git clone https://github.com/lxchuan12/vuex-analysis.git
先把 Store 實例打印出來,看具體結(jié)構(gòu),再結(jié)合實例斷點調(diào)試,事半功倍。
Vuex 源碼相對不多,打包后一千多行,非常值得學習,也比較容易看完。
如果讀者發(fā)現(xiàn)有不妥或可改善之處,再或者哪里沒寫明白的地方,歡迎評論指出。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉(zhuǎn)發(fā)分享,也是對筆者的一種支持,萬分感謝。
推薦閱讀
vuex 官方文檔
vuex github 倉庫
美團明裔:Vuex框架原理與源碼分析這篇文章強烈推薦,流程圖畫的很好
知乎黃軼:Vuex 2.0 源碼分析這篇文章也強烈推薦,講述的比較全面
小蟲巨蟹:Vuex 源碼解析(如何閱讀源代碼實踐篇)這篇文章也強烈推薦,主要講如何閱讀源代碼
染陌:Vuex 源碼解析
網(wǎng)易考拉前端團隊:Vuex 源碼分析
yck:Vuex 源碼深度解析
小生方勤:【前端詞典】從源碼解讀 Vuex 注入 Vue 生命周期的過程
筆者精選文章
前端使用puppeteer 爬蟲生成《React.js 小書》PDF并合并
學習 jQuery 源碼整體架構(gòu),打造屬于自己的 js 類庫
學習underscore源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫
學習 lodash 源碼整體架構(gòu),打造屬于自己的函數(shù)式編程類庫
學習 sentry 源碼整體架構(gòu),打造屬于自己的前端異常監(jiān)控SDK
關(guān)于
作者:常以若川為名混跡于江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人博客 https://lxchuan12.cn/posts 使用 vuepress重構(gòu)了,閱讀體驗可能更好些
https://github.com/lxchuan12/blog,相關(guān)源碼和資源都放在這里,求個 star^_^~
歡迎加微信交流 微信公眾號
可能比較有趣的微信公眾號,長按掃碼關(guān)注。也可以加微信 lxchuan12,注明來源,拉您進【前端視野交流群】。
左邊是個人微信號 lxchuan12,右邊是公眾號【若川視野】
由于公眾號限制外鏈,點擊閱讀原文,或許閱讀體驗更佳,覺得文章不錯,可以點個在看呀^_^
?? 看完兩件事
如果你覺得這篇內(nèi)容對你挺有益,我想邀請你幫我兩個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容
關(guān)注公眾號「全棧大佬的修煉之路」,每周學習一個新技術(shù)。
