這是學習源碼總體架構第五篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。html
其他四篇分別是:前端
1.學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
2.學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
3.學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
4.學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
vue
感興趣的讀者能夠點擊閱讀。下一篇多是學習 axios
源碼。react
導讀
文章比較詳細的介紹了vuex
、vue
源碼調試方法和 Vuex
原理。而且詳細介紹了 Vuex.use
安裝和 new Vuex.Store
初始化、Vuex.Store
的所有API
(如dispatch
、commit
等)的實現和輔助函數 mapState
、mapGetters
、 mapActions
、mapMutations
createNamespacedHelpers
。webpack
Vue文檔:在 VS Code 中調試 Vue 項目
從上文中同理可得調試 vuex
方法,這裏詳細說下,便於幫助到可能不知道如何調試源碼的讀者。
能夠把筆者的這個 vuex-analysis 源碼分析倉庫fork
一份或者直接克隆下來, git clone https://github.com/lxchuan12/vuex-analysis.git
ios
其中文件夾
vuex
,是克隆官方的vuex
倉庫dev
分支。
截至目前(2019年11月),版本是v3.1.2
,最後一次commit
是ba2ff3a3
,2019-11-11 11:51 Ben Hutton
。
包含筆者的註釋,便於理解。
git
克隆完成後, 在vuex/examples/webpack.config.js
中添加devtool
配置。github
// 新增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 文件 根據本身需求斷點調試便可。
web
本文主要就是經過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
實現。
提交 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))
複製代碼
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
源碼相對很少,打包後一千多行,很是值得學習,也比較容易看完。
若是讀者發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎評論指出。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對筆者的一種支持,萬分感謝。
vuex 官方文檔
vuex github 倉庫
美團明裔:Vuex框架原理與源碼分析這篇文章強烈推薦,流程圖畫的很好
知乎黃軼:Vuex 2.0 源碼分析這篇文章也強烈推薦,講述的比較全面
小蟲巨蟹:Vuex 源碼解析(如何閱讀源代碼實踐篇)這篇文章也強烈推薦,主要講如何閱讀源代碼
染陌:Vuex 源碼解析
網易考拉前端團隊:Vuex 源碼分析
yck:Vuex 源碼深度解析
小生方勤:【前端詞典】從源碼解讀 Vuex 注入 Vue 生命週期的過程
學習 sentry 源碼總體架構,打造屬於本身的前端異常監控SDK
學習 lodash 源碼總體架構,打造屬於本身的函數式編程類庫
學習 underscore 源碼總體架構,打造屬於本身的函數式編程類庫
學習 jQuery 源碼總體架構,打造屬於本身的 js 類庫
面試官問:JS的繼承
面試官問:JS的this指向
面試官問:可否模擬實現JS的call和apply方法
面試官問:可否模擬實現JS的bind方法
面試官問:可否模擬實現JS的new操做符
做者:常以若川爲名混跡於江湖。前端路上 | PPT愛好者 | 所知甚少,惟善學。
我的博客-若川,使用vuepress
重構了,閱讀體驗可能更好些
掘金專欄,歡迎關注~
segmentfault
前端視野專欄,歡迎關注~
知乎前端視野專欄,歡迎關注~
github blog,相關源碼和資源都放在這裏,求個star
^_^~
可能比較有趣的微信公衆號,長按掃碼關注。也能夠加微信 lxchuan12
,註明來源,拉您進【前端視野交流羣】。