前言
這是學習源碼整體架構第五篇。整體架構這詞語好像有點大,姑且就算是源碼整體結構吧,主要就是學習是代碼整體結構,不深究其他不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。
其余四篇分別是:
學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫
學習underscore源碼整體架構,打造屬于自己的函數式編程類庫
學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫
學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK
感興趣的讀者可以點擊閱讀。下一篇可能是學習?axios
?源碼。
導讀
文章比較詳細的介紹了vuex
、vue
源碼調試方法和?Vuex
?原理。并且詳細介紹了?Vuex.use
?安裝和?new Vuex.Store
?初始化、Vuex.Store
?的全部API
(如dispatch
、commit
等)的實現和輔助函數?mapState
、mapGetters
、?mapActions
、mapMutations
?createNamespacedHelpers
。
chrome 瀏覽器調試 vuex 源碼方法
Vue文檔:在 VS Code 中調試 Vue 項目
從上文中同理可得調試?vuex
?方法,這里詳細說下,便于幫助到可能不知道如何調試源碼的讀者。
可以把筆者的這個?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配置,便于調試
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 在左側找到 webapck// . src 目錄 store 文件 根據自己需求斷點調試即可。
本文主要就是通過Shopping Cart
,(路徑vuex/examples/shopping-cart
)例子調試代碼的。
順便提一下調試 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 在左側找到 src 目錄 即vue.js源碼文件 根據自己需求斷點調試即可。
本小節大篇幅介紹調試方法。是因為真的很重要。會調試代碼,看源碼就比較簡單了。關注主線調試代碼,很容易看懂。
強烈建議克隆筆者的這個倉庫,自己調試代碼,對著注釋看,不調試代碼,只看文章不容易吸收消化。
筆者也看了文章末尾筆者推薦閱讀的文章,但還是需要自己看源代碼,才知道這些文章哪里寫到了,哪里沒有細寫。?
正文開始~
vuex 原理
簡單說明下?vuex
?原理
<template>
<div>count {{$store.state.count}}
</div>
</template>
每個組件(也就是Vue實例
)在beforeCreate
的生命周期中都混入(Vue.mixin)同一個Store實例
?作為屬性?$store
, 也就是為啥可以通過?this.$store.dispatch
?等調用方法的原因。
最后顯示在模板里的?$store.state.count
?源碼是這樣的。
class Store{get state () {return this._vm._data.$$state}
}
其實就是:?vm.$store._vm._data.$$state.count
?其中vm.$store._vm._data.$$state
?是 響應式的。怎么實現響應式的?其實就是new Vue()
function resetStoreVM (store, state, hot) {// 省略若干代碼store._vm = new Vue({data: {$$state: state},computed})// 省略若干代碼
}
這里的?state
?就是 用戶定義的?state
。這里的?computed
?就是處理后的用戶定義的?getters
。而?class Store
上的一些函數(API)主要都是圍繞修改vm.$store._vm._data.$$state
和computed(getter)
服務的。
Vue.use 安裝
筆者畫了一張圖表示下Vuex
對象,是Vue
的一個插件。
看到這里,恭喜你已經了解了
Vuex
原理。文章比較長,如果暫時不想關注源碼細節,可以克隆一下本倉庫代碼git clone https://github.com/lxchuan12/vuex-analysis.git
,后續調試代碼,點贊收藏到時想看了再看。
文檔 Vue.use?Vue.use(Vuex)
參數:{Object | Function} plugin 用法:
安裝 Vue.js 插件。如果插件是一個對象,必須提供?install
?方法。如果插件是一個函數,它會被作為?install
?方法。install
?方法調用時,會將 Vue 作為參數傳入。
該方法需要在調用?new Vue()
?之前被調用。
當?install
?方法被同一個插件多次調用,插件將只會被安裝一次。
根據斷點調試,來看下Vue.use
的源碼。
function initUse (Vue) {Vue.use = function (plugin) {var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));// 如果已經存在,則直接返回this也就是Vueif (installedPlugins.indexOf(plugin) > -1) {return this}// additional parametersvar args = toArray(arguments, 1);// 把 this(也就是Vue)作為數組的第一項args.unshift(this);// 如果插件的install屬性是函數,調用它if (typeof plugin.install === 'function') {plugin.install.apply(plugin, args);} else if (typeof plugin === 'function') {// 如果插件是函數,則調用它// apply(null) 嚴格模式下 plugin 插件函數的 this 就是 nullplugin.apply(null, args);}// 添加到已安裝的插件installedPlugins.push(plugin);return this};
}
install 函數
vuex/src/store.js
export function install (_Vue) {// Vue 已經存在并且相等,說明已經Vuex.use過if (Vue && _Vue === Vue) {// 省略代碼:非生產環境報錯,vuex已經安裝return}Vue = _VueapplyMixin(Vue)
}
接下來看?applyMixin
?函數
applyMixin 函數
vuex/src/mixin.js
export default function (Vue) {// Vue 版本號const version = Number(Vue.version.split('.')[0])if (version >= 2) {// 合并選項后 beforeCreate 是數組里函數的形式 [?, ?]// 最后調用循環遍歷這個數組,調用這些函數,這是一種函數與函數合并的解決方案。// 假設是我們自己來設計,會是什么方案呢。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 構造函數
先看最終?new Vuex.Store
?之后的?Store
?實例對象關系圖:先大致有個印象。?
export class Store {constructor (options = {}) {// 這個構造函數比較長,這里省略,后文分開細述}
}
if (!Vue && typeof window !== 'undefined' && window.Vue) {install(window.Vue)
}
如果是?cdn script
?方式引入vuex
插件,則自動安裝vuex
插件,不需要用Vue.use(Vuex)
來安裝。
// asset 函數實現
export function assert (condition, msg) {if (!condition) throw new Error(`[vuex] ${msg}`)
}
if (process.env.NODE_ENV !== 'production') {// 可能有讀者會問:為啥不用 console.assert,console.assert 函數報錯不會阻止后續代碼執行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)
?創建?store
?實例。
2.當前環境不支持Promise
,報錯:vuex
?需要?Promise polyfill
。
3.Store
?函數必須使用?new
?操作符調用。
const {// 插件默認是空數組plugins = [],// 嚴格模式默認是falsestrict = false
} = options
從用戶定義的new Vuex.Store(options)
?取出plugins
和strict
參數。
// store internal state
// store 實例對象 內部的 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)
// 模塊收集器,構造模塊樹形結構
this._modules = new ModuleCollection(options)
// 用于存儲模塊命名空間的關系
this._modulesNamespaceMap = Object.create(null)
// 訂閱
this._subscribers = []
// 用于使用 $watch 觀測 getters
this._watcherVM = new Vue()
// 用來存放生成的本地 getters 的緩存
this._makeLocalGettersCache = Object.create(null)
聲明Store
實例對象一些內部變量。用于存放處理后用戶自定義的actions
、mutations
、getters
等變量。
提一下?
Object.create(null)
?和?{}
?的區別。前者沒有原型鏈,后者有。即?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
為何要這樣綁定 ?
說明調用?commit
?和?dispach
?的?this
?不一定是?store
?實例
這是確保這兩個函數里的?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
?傳給插件函數,執行所有插件。
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {devtoolPlugin(this)
}
初始化?vue-devtool
?開發工具。
參數?devtools
?傳遞了取?devtools
?否則取Vue.config.devtools
?配置。
初讀這個構造函數的全部源代碼。會發現有三個地方需要重點看。分別是:
this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
閱讀時可以斷點調試,賦值語句this._modules = new ModuleCollection(options)
,如果暫時不想看,可以直接看返回結果。installModule
,resetStoreVM
函數則可以斷點調試。
class ModuleCollection
收集模塊,構造模塊樹結構。
注冊根模塊 參數?
rawRootModule
?也就是?Vuex.Store
?的?options
?參數
未加工過的模塊(用戶自定義的),根模塊
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) {// 非生產環境 斷言判斷用戶自定義的模塊是否符合要求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 的模塊 基礎數據結構,包括一些屬性和方法
export default class Module {constructor (rawModule, runtime) {// 接收參數 runtimethis.runtime = runtime// Store some children item// 存儲子模塊this._children = Object.create(null)// Store the origin module object which passed by programmer// 存儲原始未加工的模塊this._rawModule = rawModule// 模塊 stateconst rawState = rawModule.state// Store the origin module's state// 原始Store 可能是函數,也可能是是對象,是假值,則賦值空對象。this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}}
}
經過一系列的注冊后,最后?this._modules = new ModuleCollection(options)
?this._modules
?的值是這樣的。筆者畫了一張圖表示:
installModule 函數
function installModule (store, rootState, path, module, hot) {// 是根模塊const isRoot = !path.length// 命名空間 字符串const namespace = store._modules.getNamespace(path)if (module.namespaced) {// 省略代碼:模塊命名空間map對象中已經有了,開發環境報錯提示重復// module 賦值給 _modulesNamespaceMap[namespace]store._modulesNamespaceMap[namespace] = module}// ... 后續代碼 移出來 待讀解釋
}
注冊 state
// set state
// 不是根模塊且不是熱重載
if (!isRoot && !hot) {// 獲取父級的stateconst parentState = getNestedState(rootState, path.slice(0, -1))// 模塊名稱// 比如 cartconst moduleName = path[path.length - 1]// state 注冊store._withCommit(() => {// 省略代碼:非生產環境 報錯 模塊 state 重復設置Vue.set(parentState, moduleName, module.state)})
}
最后得到的是類似這樣的結構且是響應式的數據 實例 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
四個輔助函數使用的。
生成本地的dispatch、commit、getters和state。
主要作用就是抹平差異化,不需要用戶再傳模塊參數。
遍歷注冊 mutation
module.forEachMutation((mutation, key) => {const namespacedType = namespace + keyregisterMutation(store, namespacedType, mutation, local)
})
/*** 注冊 mutation* @param {Object} store 對象* @param {String} type 類型* @param {Function} handler 用戶自定義的函數* @param {Object} local local 對象*/
function registerMutation (store, type, handler, local) {// 收集的所有的mutations找對應的mutation函數,沒有就賦值空數組const entry = store._mutations[type] || (store._mutations[type] = [])// 最后 mutationentry.push(function wrappedMutationHandler (payload) {/*** mutations: {* pushProductToCart (state, { id }) {* console.log(state);* }* }* 也就是為什么用戶定義的 mutation 第一個參數是state的原因,第二個參數是payload參數*/handler.call(store, local.state, payload)})
}
遍歷注冊 action
module.forEachAction((action, key) => {const type = action.root ? key : namespace + keyconst handler = action.handler || actionregisterAction(store, type, handler, local)
})
/**
* 注冊 mutation
* @param {Object} store 對象
* @param {String} type 類型
* @param {Function} handler 用戶自定義的函數
* @param {Object} local local 對象
*/
function registerAction (store, type, handler, local) {const entry = store._actions[type] || (store._actions[type] = [])// payload 是actions函數的第二個參數entry.push(function wrappedActionHandler (payload) {/*** 也就是為什么用戶定義的actions中的函數第一個參數有* { 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 中處理異步函數也就是為什么構造函數中斷言不支持promise報錯的原因vuex需要Promise polyfillassert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)*/if (!isPromise(res)) {res = Promise.resolve(res)}// devtool 工具觸發 vuex:errorif (store._devtoolHook) {// catch 捕獲錯誤return res.catch(err => {store._devtoolHook.emit('vuex:error', err)// 拋出錯誤throw err})} else {// 然后函數執行結果return res}})
}
遍歷注冊 getter
module.forEachGetter((getter, key) => {const namespacedType = namespace + keyregisterGetter(store, namespacedType, getter, local)
})
/*** 注冊 getter* @param {Object} store Store實例* @param {String} type 類型* @param {Object} rawGetter 原始未加工的 getter 也就是用戶定義的 getter 函數* @examples 比如 cartProducts: (state, getters, rootState, rootGetters) => {}* @param {Object} local 本地 local 對象*/
function registerGetter (store, type, rawGetter, local) {// 類型如果已經存在,報錯:已經存在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 statelocal.getters, // local gettersstore.state, // root statestore.getters // root getters)}
}
遍歷注冊 子模塊
module.forEachChild((child, key) => {installModule(store, rootState, path.concat(key), child, hot)
})
resetStoreVM 函數
resetStoreVM(this, state, hot)
初始化?
store._vm
?響應式的
并且注冊?_wrappedGetters
?作為?computed
?的屬性
function resetStoreVM (store, state, hot) {// 存儲一份老的Vue實例對象 _vmconst oldVm = store._vm// bind store public getters// 綁定 store.getterstore.getters = {}// reset local getters cache// 重置 本地getters的緩存store._makeLocalGettersCache = Object.create(null)// 注冊時收集的處理后的用戶自定義的 wrappedGettersconst 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 函數* 執行函數 返回一個新函數export function partial (fn, arg) {return function () {return fn(arg)}}*/computed[key] = partial(fn, store)// getter 賦值 keysObject.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 存儲用戶設置的靜默模式配置const silent = Vue.config.silent// 靜默模式開啟Vue.config.silent = truestore._vm = new Vue({data: {$$state: state},computed})// 把存儲的靜默模式配置賦值回來Vue.config.silent = silent// enable strict mode for new vm// 開啟嚴格模式 執行這句// 用 $watch 觀測 state,只能使用 mutation 修改 也就是 _withCommit 函數if (store.strict) {enableStrictMode(store)}// 如果存在老的 _vm 實例if (oldVm) {// 熱加載為 trueif (hot) {// dispatch changes in all subscribed watchers// to force getter re-evaluation for hot reloading.// 設置 oldVm._data.$$state = nullstore._withCommit(() => {oldVm._data.$$state = null})}// 實例銷毀Vue.nextTick(() => oldVm.$destroy())}
}
到此,構造函數源代碼看完了,接下來看?Vuex.Store
?的 一些?API
?實現。
Vuex.Store 實例方法
Vuex API 文檔
commit
提交?mutation
。
commit (_type, _payload, _options) {// check object-style commit// 統一成對象風格const {type,payload,options} = unifyObjectStyle(_type, _payload, _options)const mutation = { type, payload }// 取出處理后的用戶定義 mutationconst entry = this._mutations[type]// 省略 非生產環境的警告代碼 ...this._withCommit(() => {// 遍歷執行entry.forEach(function commitIterator (handler) {handler(payload)})})// 訂閱 mutation 執行this._subscribers.forEach(sub => sub(mutation, this.state))// 省略 非生產環境的警告代碼 ...
}
commit
?支持多種方式。比如:
store.commit('increment', {count: 10
})
// 對象提交方式
store.commit({type: 'increment',count: 10
})
unifyObjectStyle
函數將參數統一,返回?{ type, payload, options }
。
dispatch
分發?action
。
dispatch (_type, _payload) {// check object-style dispatch// 獲取到type和payload參數const {type,payload} = unifyObjectStyle(_type, _payload)// 聲明 action 變量 等于 type和payload參數const action = { type, payload }// 入口,也就是 _actions 集合const entry = this._actions[type]// 省略 非生產環境的警告代碼 ...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
?的根狀態,僅用狀態合并或時光旅行調試。
replaceState (state) {this._withCommit(() => {this._vm._data.$$state = state})
}
watch
響應式地偵聽 fn 的返回值,當值改變時調用回調函數。
/*** 觀測某個值* @param {Function} getter 函數* @param {Function} cb 回調* @param {Object} options 參數對象*/
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 } : fnreturn genericSubscribe(subs, this._actionSubscribers)
}
registerModule
注冊一個動態模塊。
/*** 動態注冊模塊* @param {Array|String} path 路徑* @param {Object} rawModule 原始未加工的模塊* @param {Object} options 參數選項*/
registerModule (path, rawModule, options = {}) {// 如果 path 是字符串,轉成數組if (typeof path === 'string') path = [path]// 省略 非生產環境 報錯代碼// 手動調用 模塊注冊的方法this._modules.register(path, rawModule)// 安裝模塊installModule(this, this.state, path, this._modules.get(path), options.preserveState)// reset store to update getters...// 設置 resetStoreVMresetStoreVM(this, this.state)
}
unregisterModule
卸載一個動態模塊。
/*** 注銷模塊* @param {Array|String} path 路徑*/
unregisterModule (path) {// 如果 path 是字符串,轉成數組if (typeof path === 'string') path = [path]// 省略 非生產環境 報錯代碼 ...// 手動調用模塊注銷this._modules.unregister(path)this._withCommit(() => {// 注銷這個模塊const parentState = getNestedState(this.state, path.slice(0, -1))Vue.delete(parentState, path[path.length - 1])})// 重置 StoreresetStore(this)
}
hotUpdate
熱替換新的?action
?和?mutation
。
// 熱加載
hotUpdate (newOptions) {// 調用的是 ModuleCollection 的 update 方法,最終調用對應的是每個 Module 的 updatethis._modules.update(newOptions)// 重置 StoreresetStore(this, true)
}
組件綁定的輔助函數
文件路徑:vuex/src/helpers.js
mapState
為組件創建計算屬性以返回?Vuex store
?中的狀態。
export const mapState = normalizeNamespace((namespace, states) => {const res = {}// 非生產環境 判斷參數 states 必須是數組或者是對象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.statelet getters = this.$store.getters// 傳了參數 namespaceif (namespace) {// 用 namespace 從 store 中找一個模塊。const module = getModuleByNamespace(this.$store, 'mapState', namespace)if (!module) {return}state = module.context.stategetters = module.context.getters}return typeof val === 'function'? val.call(this, state, getters): state[val]}// 標記為 vuex 方便在 devtools 顯示// mark vuex getter for devtoolsres[key].vuex = true})return res
})
normalizeNamespace 標準化統一命名空間
function normalizeNamespace (fn) {return (namespace, map) => {// 命名空間沒傳,交換參數,namespace 為空字符串if (typeof namespace !== 'string') {map = namespacenamespace = ''} else if (namespace.charAt(namespace.length - 1) !== '/') {// 如果是字符串,最后一個字符不是 / 添加 /// 因為 _modulesNamespaceMap 存儲的是這樣的結構。/*** _modulesNamespaceMap:cart/: {}products/: {}}* */namespace += '/'}return fn(namespace, map)}
}
// 校驗是否是map 是數組或者是對象。
function isValidMap (map) {return Array.isArray(map) || isObject(map)
}
/*** Normalize the map* 標準化統一 map,最終返回的是數組* 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
四個輔助函數使用的。
// 在構造函數中 installModule 中
const local = module.context = makeLocalContext(store, namespace, path)
這里就是抹平差異,不用用戶傳遞命名空間,獲取到對應的 commit、dispatch、state、和 getters
getModuleByNamespace
function getModuleByNamespace (store, helper, namespace) {// _modulesNamespaceMap 這個變量在 class Store installModule 函數中賦值的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}),
}
沒有命名空間的情況下,最終會轉換成這樣
computed: {checkoutStatus: this.$store.state.checkoutStatus
}
假設有命名空間'ruochuan',
computed: {...mapState('ruochuan', {checkoutStatus: state => state.cart.checkoutStatus}),
}
則會轉換成:
computed: {checkoutStatus: this.$store._modulesNamespaceMap.['ruochuan/'].context.checkoutStatus
}
mapGetters
為組件創建計算屬性以返回?getter
?的返回值。
export const mapGetters = normalizeNamespace((namespace, getters) => {const res = {}// 省略代碼:非生產環境 判斷參數 getters 必須是數組或者是對象normalizeMap(getters).forEach(({ key, val }) => {// The namespace has been mutated by normalizeNamespaceval = namespace + valres[key] = function mappedGetter () {if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {return}// 省略代碼:匹配不到 getterreturn this.$store.getters[val]}// mark vuex getter for devtoolsres[key].vuex = true})return res
})
舉例:
computed: {...mapGetters('cart', {products: 'cartProducts',total: 'cartTotalPrice'})
},
最終轉換成:
computed: {products: this.$store.getters['cart/cartProducts'],total: this.$store.getters['cart/cartTotalPrice'],
}
mapActions
創建組件方法分發?action
。
export const mapActions = normalizeNamespace((namespace, actions) => {const res = {}// 省略代碼:非生產環境 判斷參數 actions 必須是數組或者是對象normalizeMap(actions).forEach(({ key, val }) => {res[key] = function mappedAction (...args) {// get dispatch function from storelet dispatch = this.$store.dispatchif (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
創建組件方法提交?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'])}
}
最終轉換成
{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))}}
}
由此可見:這些輔助函數極大地方便了開發者。
createNamespacedHelpers
創建基于命名空間的組件綁定輔助函數。
export const createNamespacedHelpers = (namespace) => ({// bind(null) 嚴格模式下,napState等的函數 this 指向就是 nullmapState: mapState.bind(null, namespace),mapGetters: mapGetters.bind(null, namespace),mapMutations: mapMutations.bind(null, namespace),mapActions: mapActions.bind(null, namespace)
})
就是把這些輔助函數放在一個對象中。
插件
插件部分文件路徑是:vuex/src/plugins/devtool
vuex/src/plugins/logger
文章比較長了,這部分就不再敘述。具體可以看筆者的倉庫?vuex-analysis?vuex/src/plugins/
?的源碼注釋。
總結
文章比較詳細的介紹了vuex
、vue
源碼調試方法和?Vuex
?原理。并且詳細介紹了?Vuex.use
?安裝和?new Vuex.Store
?初始化、Vuex.Store
?的全部API
(如dispatch
、commit
等)的實現和輔助函數?mapState
、mapGetters
、?mapActions
、mapMutations
?createNamespacedHelpers
。
文章注釋,在vuex-analysis源碼倉庫里基本都有注釋分析,求個star
。再次強烈建議要克隆代碼下來。
git clone https://github.com/lxchuan12/vuex-analysis.git
先把?Store
?實例打印出來,看具體結構,再結合實例斷點調試,事半功倍。
Vuex
?源碼相對不多,打包后一千多行,非常值得學習,也比較容易看完。
如果讀者發現有不妥或可改善之處,再或者哪里沒寫明白的地方,歡迎評論指出。另外覺得寫得不錯,對您有些許幫助,可以點贊、評論、轉發分享,也是對筆者的一種支持,萬分感謝。
推薦閱讀
vuex 官方文檔
vuex github 倉庫
美團明裔:Vuex框架原理與源碼分析這篇文章強烈推薦,流程圖畫的很好
知乎黃軼:Vuex 2.0 源碼分析這篇文章也強烈推薦,講述的比較全面
小蟲巨蟹:Vuex 源碼解析(如何閱讀源代碼實踐篇)這篇文章也強烈推薦,主要講如何閱讀源代碼
染陌:Vuex 源碼解析
網易考拉前端團隊:Vuex 源碼分析
yck:Vuex 源碼深度解析
小生方勤:【前端詞典】從源碼解讀 Vuex 注入 Vue 生命周期的過程
筆者精選文章
工作一年后,我有些感悟(寫于2017年)
高考七年后、工作三年后的感悟
面試官問:JS的繼承
前端使用puppeteer 爬蟲生成《React.js 小書》PDF并合并
學習 jQuery 源碼整體架構,打造屬于自己的 js 類庫
學習underscore源碼整體架構,打造屬于自己的函數式編程類庫
學習 lodash 源碼整體架構,打造屬于自己的函數式編程類庫
學習 sentry 源碼整體架構,打造屬于自己的前端異常監控SDK
關于
作者:常以若川為名混跡于江湖。前端路上 | PPT愛好者 | 所知甚少,唯善學。
個人博客 https://lxchuan12.cn/posts?使用?vuepress
重構了,閱讀體驗可能更好些
https://github.com/lxchuan12/blog,相關源碼和資源都放在這里,求個 star
^_^~
歡迎加微信交流 微信公眾號
可能比較有趣的微信公眾號,長按掃碼關注。也可以加微信?lxchuan12
,注明來源,拉您進【前端視野交流群】。
左邊是個人微信號? lxchuan12,右邊是公眾號【若川視野】
由于公眾號限制外鏈,點擊閱讀原文,或許閱讀體驗更佳,覺得文章不錯,可以點個在看呀^_^