這是學習源碼總體架構第五篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。php
其他四篇分別是:html
學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫前端
學習underscore源碼總體架構,打造屬於本身的函數式編程類庫vue
學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫react
學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDKwebpack
感興趣的讀者能夠點擊閱讀。下一篇多是學習 axios 源碼。ios
導讀
文章比較詳細的介紹了vuex、vue源碼調試方法和 Vuex 原理。而且詳細介紹了 Vuex.use 安裝和 new Vuex.Store 初始化、Vuex.Store 的所有API(如dispatch、commit等)的實現和輔助函數 mapState、mapGetters、 mapActions、mapMutations createNamespacedHelpers。git
Vue文檔:在 VS Code 中調試 Vue 項目
從上文中同理可得調試 vuex 方法,這裏詳細說下,便於幫助到可能不知道如何調試源碼的讀者。
能夠把筆者的這個 vuex-analysis 源碼分析倉庫fork一份或者直接克隆下來, git clone https://github.com/lxchuan12/vuex-analysis.gitgithub
其中文件夾vuex,是克隆官方的vuex倉庫 dev分支。
截至目前(2019年11月),版本是v3.1.2,最後一次commit是ba2ff3a3,2019-11-11 11:51 Ben Hutton。
包含筆者的註釋,便於理解。web
克隆完成後, 在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)例子調試代碼的。
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 原理
<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)服務的。
筆者畫了一張圖表示下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也就是Vue if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters var 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 就是 null plugin.apply(null, args); } // 添加到已安裝的插件 installedPlugins.push(plugin); return this }; }
vuex/src/store.js
export function install (_Vue) { // Vue 已經存在而且相等,說明已經Vuex.use過 if (Vue && _Vue === Vue) { // 省略代碼:非生產環境報錯,vuex已經安裝 return } Vue = _Vue applyMixin(Vue) }
接下來看 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
先看最終 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 = [], // 嚴格模式默認是false strict = 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函數則能夠斷點調試。
收集模塊,構造模塊樹結構。
註冊根模塊 參數 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) }) } }
// Base data struct for store's module, package with some attribute and method // store 的模塊 基礎數據結構,包括一些屬性和方法 export default class Module { constructor (rawModule, runtime) { // 接收參數 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 多是函數,也多是是對象,是假值,則賦值空對象。 this.state = (typeof rawState === 'function' ? rawState() : rawState) || {} } }
通過一系列的註冊後,最後 this._modules = new ModuleCollection(options) this._modules 的值是這樣的。筆者畫了一張圖表示:
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 } // ... 後續代碼 移出來 待讀解釋 }
// set state // 不是根模塊且不是熱重載 if (!isRoot && !hot) { // 獲取父級的state const parentState = getNestedState(rootState, path.slice(0, -1)) // 模塊名稱 // 好比 cart const 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。
主要做用就是抹平差別化,不須要用戶再傳模塊參數。
module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(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] = []) // 最後 mutation entry.push(function wrappedMutationHandler (payload) { /** * mutations: { * pushProductToCart (state, { id }) { * console.log(state); * } * } * 也就是爲何用戶定義的 mutation 第一個參數是state的緣由,第二個參數是payload參數 */ handler.call(store, local.state, payload) }) }
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 用戶自定義的函數 * @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 polyfill assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`) */ if (!isPromise(res)) { res = Promise.resolve(res) } // devtool 工具觸發 vuex:error if (store._devtoolHook) { // catch 捕獲錯誤 return res.catch(err => { store._devtoolHook.emit('vuex:error', err) // 拋出錯誤 throw err }) } else { // 而後函數執行結果 return res } }) }
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 函數 * @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 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(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 函數 * 執行函數 返回一個新函數 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 存儲用戶設置的靜默模式配置 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 // 開啓嚴格模式 執行這句 // 用 $watch 觀測 state,只能使用 mutation 修改 也就是 _withCommit 函數 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. // 設置 oldVm._data.$$state = null store._withCommit(() => { oldVm._data.$$state = null }) } // 實例銷燬 Vue.nextTick(() => oldVm.$destroy()) } }
到此,構造函數源代碼看完了,接下來看 Vuex.Store 的 一些 API 實現。
Vuex API 文檔
提交 mutation。
commit (_type, _payload, _options) { // check object-style commit // 統一成對象風格 const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } // 取出處理後的用戶定義 mutation const 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 }。
分發 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 }) }
替換 store 的根狀態,僅用狀態合併或時光旅行調試。
replaceState (state) { this._withCommit(() => { this._vm._data.$$state = state }) }
響應式地偵聽 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) }
訂閱 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) } } }
訂閱 store 的 action。
subscribeAction (fn) { const subs = typeof fn === 'function' ? { before: fn } : fn return genericSubscribe(subs, this._actionSubscribers) }
註冊一個動態模塊。
/** * 動態註冊模塊 * @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... // 設置 resetStoreVM resetStoreVM(this, this.state) }
卸載一個動態模塊。
/** * 註銷模塊 * @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]) }) // 重置 Store resetStore(this) }
熱替換新的 action 和 mutation。
// 熱加載 hotUpdate (newOptions) { // 調用的是 ModuleCollection 的 update 方法,最終調用對應的是每一個 Module 的 update this._modules.update(newOptions) // 重置 Store resetStore(this, true) }
文件路徑:vuex/src/helpers.js
爲組件建立計算屬性以返回 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.state let getters = this.$store.getters // 傳了參數 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 標準化統一命名空間
function normalizeNamespace (fn) { return (namespace, map) => { // 命名空間沒傳,交換參數,namespace 爲空字符串 if (typeof namespace !== 'string') { map = namespace namespace = '' } 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 }
爲組件建立計算屬性以返回 getter 的返回值。
export const mapGetters = normalizeNamespace((namespace, getters) => { const res = {} // 省略代碼:非生產環境 判斷參數 getters 必須是數組或者是對象 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' }) },
最終轉換成:
computed: { products: this.$store.getters['cart/cartProducts'], total: this.$store.getters['cart/cartTotalPrice'], }
建立組件方法分發 action。
export const mapActions = normalizeNamespace((namespace, actions) => { const res = {} // 省略代碼:非生產環境 判斷參數 actions 必須是數組或者是對象 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 })
建立組件方法提交 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)) } } }
因而可知:這些輔助函數極大地方便了開發者。
建立基於命名空間的組件綁定輔助函數。
export const createNamespacedHelpers = (namespace) => ({ // bind(null) 嚴格模式下,napState等的函數 this 指向就是 null mapState: 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 源碼相對很少,打包後一千多行,很是值得學習,也比較容易看完。