上一章總結(jié)了 Vuex 的框架原理,這一章我們將從 Vuex 的入口文件開(kāi)始,分步驟閱讀和解析源碼。由于 Vuex 的源碼是基于 Vue 和 ES6 的,因此這兩部分不熟悉的小伙伴可以先去熟悉一下它們的基礎(chǔ)知識(shí),這對(duì)理解 Vuex 源碼的實(shí)現(xiàn)原理幫助是很大的。
在閱讀源碼之前,我們需要到 Vuex 的 Github 地址下載源碼及示例代碼:
git clone https://github.com/vuejs/vuex.git
1. 目錄結(jié)構(gòu)介紹
雖然下載下來(lái)的代碼有很多個(gè)文件夾,但是源碼只被存在./src
目錄下,具體的源碼目錄結(jié)構(gòu)如下圖:
Vuex提供了非常強(qiáng)大的狀態(tài)管理功能,源碼代碼量卻不多,目錄結(jié)構(gòu)劃分也很清晰。先大體介紹下各個(gè)目錄文件的功能:
- module:提供 module 對(duì)象與 module 對(duì)象樹(shù)的創(chuàng)建功能;
- plugins:提供開(kāi)發(fā)輔助插件,如 “時(shí)光穿梭” 功能,state 修改的日志記錄功能等;
- helpers.js:提供 action、mutations 以及 getters 的查找 API;
- index.js:是源碼主入口文件,提供 store 的各 module 構(gòu)建安裝;
- mixin.js:提供了 store 在 Vue 實(shí)例上的裝載注入;
- util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法。
2. 初始化裝載與注入
在基本了解了源碼的目錄結(jié)構(gòu)之后,就可以開(kāi)始源碼的閱讀了。我們先從入口文件index.js
開(kāi)始入手,index.js
中包含了所有的核心代碼的引用。
2.1 裝載實(shí)例
在將 Vuex 注入到一個(gè) Vue 項(xiàng)目時(shí),我們經(jīng)常會(huì)在項(xiàng)目的store.js
文件中加載 Vuex 框架,并且創(chuàng)建出一個(gè) store 對(duì)象實(shí)例:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store()
然后在index.js
中,正常初始化一個(gè)頁(yè)面根級(jí)別的 Vue 組件,傳入這個(gè)自定義的 store 對(duì)象:
import Vue from 'vue'
import App from './../pages/app.vue'
import store from './store.js'
new Vue({
el: '#root',
router,
store,
render: h => h(App)
})
那么問(wèn)題來(lái)了:使用 Vuex 只需執(zhí)行Vue.use(Vuex)
,并在Vue的配置中傳入一個(gè) store 對(duì)象的示例,store 是如何實(shí)現(xiàn)注入的 ?
2.2 裝載分析
在store.js
文件的代碼開(kāi)頭中,let Vue
語(yǔ)句定義了局部 Vue 變量,用于判斷是否已經(jīng)裝載和減少全局作用域查找。
接著判斷是否已經(jīng)加載過(guò) Vue 對(duì)象,如果處于瀏覽器環(huán)境下且加載過(guò) Vue,則執(zhí)行 install 方法。install 方法將 Vuex 裝載到 Vue 對(duì)象上,Vue.use(Vuex)
也是通過(guò)它執(zhí)行:
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
若是首次加載,將局部 Vue 變量賦值為全局的 Vue 對(duì)象,并執(zhí)行mixin.js
文件中定義的 applyMixin 方法:
function install (_Vue) {
if (Vue) { //保證install方法只執(zhí)行一次
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue) // => ./minxin.js
}
點(diǎn)開(kāi)mixin.js
文件,看一下 applyMixin 方法內(nèi)部代碼,由于 Vue 2.0 以后引入了一些生命鉤子,因此這里也對(duì) Vue 的版本進(jìn)行了判斷:如果是 2.x.x 以上版本,可以使用 hook 的形式進(jìn)行注入,或使用封裝并替換 Vue 對(duì)象原型的 _init 方法,實(shí)現(xiàn)注入:
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
我們?cè)倏匆幌戮唧w的注入方法:
// 初始化鉤子前插入一段 Vuex 初始化代碼 => 給 Vue 實(shí)例注入一個(gè) $store 屬性 => this.$store.xxx
function vuexInit () {
const options = this.$options
if (options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
從代碼中可以看出來(lái),vuexInit()
方法將初始化 Vue 根組件時(shí)傳入的 store 設(shè)置到 this 對(duì)象的 $store 屬性上,子組件從其父組件引用 $store 屬性,層層嵌套進(jìn)行設(shè)置。在任意組件中執(zhí)行 this.$store 都能找到裝載的那個(gè) store 對(duì)象。
如果覺(jué)得這么說(shuō)看起來(lái)有些抽象,那就畫個(gè)對(duì)應(yīng) store 的流向圖吧:
3. store 對(duì)象構(gòu)造
上面對(duì)Vuex框架的裝載以及注入自定義store對(duì)象進(jìn)行分析,接下來(lái)詳細(xì)分析store對(duì)象的內(nèi)部功能和具體實(shí)現(xiàn)。
3.1 環(huán)境判斷
開(kāi)始分析store的構(gòu)造函數(shù),分小節(jié)逐函數(shù)逐行的分析其功能。
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
通過(guò)這兩行的判斷語(yǔ)句,我們可以看出在 store 構(gòu)造函數(shù)中執(zhí)行環(huán)境判斷,以下都是Vuex工作的必要條件:
- 已經(jīng)執(zhí)行安裝函數(shù)進(jìn)行裝載;
- 支持Promise語(yǔ)法。
assert 函數(shù)是一個(gè)簡(jiǎn)單的斷言函數(shù)的實(shí)現(xiàn),類似于console.assert()
,它的具體實(shí)現(xiàn)在util.js
文件中:
function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
3.2 數(shù)據(jù)初始化、module 樹(shù)的構(gòu)造
環(huán)境判斷后,根據(jù)new構(gòu)造傳入的options或默認(rèn)值,初始化內(nèi)部數(shù)據(jù)。
const {
state = {},
plugins = [],
strict = false
} = options
// store internal state
this._committing = false // 是否在進(jìn)行提交狀態(tài)標(biāo)識(shí)
this._actions = Object.create(null) // acitons操作對(duì)象
this._mutations = Object.create(null) // mutations操作對(duì)象
this._wrappedGetters = Object.create(null) // 封裝后的getters集合對(duì)象
this._modules = new ModuleCollection(options) // Vuex支持store分模塊傳入,存儲(chǔ)分析后的modules
this._modulesNamespaceMap = Object.create(null) // 模塊命名空間map
this._subscribers = [] // 訂閱函數(shù)集合,Vuex提供了subscribe功能
this._watcherVM = new Vue() // Vue組件用于watch監(jiān)視變化
調(diào)用new Vuex.store(options)
時(shí)傳入的 options 對(duì)象,用于構(gòu)造 ModuleCollection 類,下面看看其功能:
this.root = new Module(rawRootModule, false)
if (rawRootModule.modules) {
forEachValue(rawRootModule.modules, (rawModule, key) => {
this.register([key], rawModule, false)
})
}
ModuleCollection 主要將傳入的 options 對(duì)象整個(gè)構(gòu)造為一個(gè)module對(duì)象,并循環(huán)調(diào)用this.register([key], rawModule, false)
為其中的 modules 屬性進(jìn)行模塊注冊(cè),使其都成為 module 對(duì)象,最后 options 對(duì)象被構(gòu)造成一個(gè)完整的組件樹(shù)。ModuleCollection 類還提供了modules的更替功能,詳細(xì)實(shí)現(xiàn)可以查看源文件module-collection.js
。
3.3 dispatch與commit設(shè)置
在 store 的構(gòu)造函數(shù)中:
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)
}
封裝替換原型中的 dispatch 和 commit 方法,將 this 指向當(dāng)前 store 對(duì)象。dispatch 和 commit 方法具體實(shí)現(xiàn)如下:
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload) // 配置參數(shù)處理
// 當(dāng)前type下所有action處理函數(shù)集合
const entry = this._actions[type]
if (!entry) {
console.error(`[vuex] unknown action type: ${type}`)
return
}
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
前面提到,dispatch 的功能是觸發(fā)并傳遞一些參數(shù)(payload)給對(duì)應(yīng) type 的 action。因?yàn)槠渲С?2 種調(diào)用方法,所以在 dispatch 中,先進(jìn)行參數(shù)的適配處理,然后判斷 action type 是否存在,若存在就逐個(gè)執(zhí)行(注:上面代碼中的this._actions[type]
以及 下面的this._mutations[type]
均是處理過(guò)的函數(shù)集合,具體內(nèi)容留到后面進(jìn)行分析)。
commit方法和dispatch相比雖然都是觸發(fā)type,但是對(duì)應(yīng)的處理卻相對(duì)復(fù)雜。
commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
console.error(`[vuex] unknown mutation type: ${type}`)
return
}
// 專用修改state方法,其他修改state方法均是非法修改
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 訂閱者函數(shù)遍歷執(zhí)行,傳入當(dāng)前的mutation對(duì)象和當(dāng)前的state
this._subscribers.forEach(sub => sub(mutation, this.state))
if (options && options.silent) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
該方法同樣支持 2 種調(diào)用方法。先進(jìn)行參數(shù)適配,判斷觸發(fā) mutation type,利用 _withCommit 方法執(zhí)行本次批量觸發(fā) mutation 處理函數(shù),并傳入 payload 參數(shù)。執(zhí)行完成后,通知所有 _subscribers(訂閱函數(shù))本次操作的 mutation 對(duì)象以及當(dāng)前的 state 狀態(tài),如果傳入了已經(jīng)移除的 silent 選項(xiàng)則進(jìn)行提示警告。
3.4 state 修改方法
_withCommit是一個(gè)代理方法,所有觸發(fā)mutation的進(jìn)行state修改的操作都經(jīng)過(guò)它,由此來(lái)統(tǒng)一管理監(jiān)控state狀態(tài)的修改。
_withCommit (fn) {
// 保存之前的提交狀態(tài)
const committing = this._committing
// 進(jìn)行本次提交,若不設(shè)置為true,直接修改state,strict模式下,Vuex將會(huì)產(chǎn)生非法修改state的警告
this._committing = true
// 執(zhí)行state的修改操作
fn()
// 修改完成,還原本次修改之前的狀態(tài)
this._committing = committing
}
緩存執(zhí)行時(shí)的 committing 狀態(tài)將當(dāng)前狀態(tài)設(shè)置為 true 后進(jìn)行本次提交操作,待操作完畢后,將 committing 狀態(tài)還原為之前的狀態(tài)。
3.5 module 安裝
綁定 dispatch 和 commit 方法之后,進(jìn)行嚴(yán)格模式的設(shè)置,以及模塊的安裝(installModule)。由于占用資源較多影響頁(yè)面性能,嚴(yán)格模式建議只在開(kāi)發(fā)模式開(kāi)啟,上線后需要關(guān)閉。
// strict mode
this.strict = strict
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
3.5.1 初始化 rootState
上述代碼的備注中,提到installModule方法初始化組件樹(shù)根組件、注冊(cè)所有子組件,并將其中所有的getters存儲(chǔ)到this._wrappedGetters屬性中。
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (namespace) {
store._modulesNamespaceMap[namespace] = module
}
// 非根組件設(shè)置 state 方法
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
...
}
判斷是否是根目錄,以及是否設(shè)置了命名空間,若存在則在 namespace 中進(jìn)行 module 的存儲(chǔ),在不是根組件且不是 hot 條件的情況下,通過(guò) getNestedState 方法拿到該 module 父級(jí)的 state,拿到其所在的 moduleName ,調(diào)用Vue.set(parentState, moduleName, module.state)
方法將其state設(shè)置到父級(jí) state 對(duì)象的 moduleName 屬性中,由此實(shí)現(xiàn)該模塊的 state 注冊(cè)(首次執(zhí)行這里,因?yàn)槭歉夸涀?cè),所以并不會(huì)執(zhí)行該條件中的方法)。getNestedState 方法代碼很簡(jiǎn)單,分析 path 拿到 state。
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
3.5.2 module 上下文環(huán)境設(shè)置
const local = module.context = makeLocalContext(store, namespace, path)
命名空間和根目錄條件判斷完畢后,接下來(lái)定義 local 變量和 module.context 的值,執(zhí)行 makeLocalContext 方法,為該 module 設(shè)置局部的 dispatch、commit 方法以及 getters 和 state(由于 namespace 的存在需要做兼容處理)。
3.5.3 mutations、actions 以及 getters 注冊(cè)
定義local環(huán)境后,循環(huán)注冊(cè)我們?cè)趏ptions中配置的action以及mutation等。逐個(gè)分析各注冊(cè)函數(shù)之前,先看下模塊間的邏輯關(guān)系流程圖:
下面分析代碼邏輯:
// 注冊(cè)對(duì)應(yīng)模塊的mutation,供state修改使用
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 注冊(cè)對(duì)應(yīng)模塊的action,供數(shù)據(jù)操作、提交mutation等異步操作使用
module.forEachAction((action, key) => {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})
// 注冊(cè)對(duì)應(yīng)模塊的getters,供state讀取使用
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
registerMutation 方法中,獲取 store 中的對(duì)應(yīng) mutation type 的處理函數(shù)集合,將新的處理函數(shù) push 進(jìn)去。這里將我們?cè)O(shè)置在 mutations type 上對(duì)應(yīng)的 handler 進(jìn)行了封裝,給原函數(shù)傳入了state。在執(zhí)行commit('xxx', payload)
的時(shí)候,type 為 xxx 的 mutation 的所有 handler 都會(huì)接收到 state 以及 payload,這就是在 handler 里面拿到 state 的原因。
function registerMutation (store, type, handler, local) {
// 取出對(duì)應(yīng)type的mutations-handler集合
const entry = store._mutations[type] || (store._mutations[type] = [])
// commit實(shí)際調(diào)用的不是我們傳入的handler,而是經(jīng)過(guò)封裝的
entry.push(function wrappedMutationHandler (payload) {
// 調(diào)用handler并將state傳入
handler(local.state, payload)
})
}
action 和 getter 的注冊(cè)也是同理的,看一下代碼(注:前面提到的this.actions
以及this.mutations
在此處進(jìn)行設(shè)置)。
function registerAction (store, type, handler, local) {
// 取出對(duì)應(yīng)type的actions-handler集合
const entry = store._actions[type] || (store._actions[type] = [])
// 存儲(chǔ)新的封裝過(guò)的action-handler
entry.push(function wrappedActionHandler (payload, cb) {
// 傳入 state 等對(duì)象供我們?cè)璦ction-handler使用
let res = handler({
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
// action需要支持promise進(jìn)行鏈?zhǔn)秸{(diào)用,這里進(jìn)行兼容處理
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
function registerGetter (store, type, rawGetter, local) {
// getters只允許存在一個(gè)處理函數(shù),若重復(fù)需要報(bào)錯(cuò)
if (store._wrappedGetters[type]) {
console.error(`[vuex] duplicate getter key: ${type}`)
return
}
// 存儲(chǔ)封裝過(guò)的getters處理函數(shù)
store._wrappedGetters[type] = function wrappedGetter (store) {
// 為原getters傳入對(duì)應(yīng)狀態(tài)
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
action handler 比 mutation handler 以及 getter wrapper 多拿到 dispatch 和 commit 操作方法,因此 action 可以進(jìn)行 dispatch action 和 commit mutation 操作。
3.5.4 子 module 安裝
注冊(cè)完了根組件的 actions、mutations 以及 getters 后,遞歸調(diào)用自身,為子組件注冊(cè)其 state,actions、mutations 以及 getters 等。
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
3.5.5 實(shí)例
前面介紹了 dispatch 和 commit 方法以及 actions 等的實(shí)現(xiàn),下面結(jié)合一個(gè)官方的購(gòu)物車實(shí)例中的部分代碼來(lái)加深理解。
Vuex 配置代碼:
/
* store-index.js store配置文件
*
/
import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import cart from './modules/cart'
import products from './modules/products'
import createLogger from '../../../src/plugins/logger'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
actions,
getters,
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})
Vuex 組件 module 中各模塊 state 配置代碼部分:
/**
* cart.js
*
**/
const state = {
added: [],
checkoutStatus: null
}
/**
* products.js
*
**/
const state = {
all: []
}
加載上述配置后,頁(yè)面 state 結(jié)構(gòu)如下圖:
state 中的屬性配置都是按照 option 配置中 module path 的規(guī)則來(lái)進(jìn)行的,下面看 action 的操作實(shí)例:
Vuecart 組件代碼部分:
/**
* Cart.vue 省略template代碼,只看script部分
*
**/
export default {
methods: {
// 購(gòu)物車中的購(gòu)買按鈕,點(diǎn)擊后會(huì)觸發(fā)結(jié)算。源碼中會(huì)調(diào)用 dispatch方法
checkout (products) {
this.$store.dispatch('checkout', products)
}
}
}
Vuexcart.js 組件 action 配置代碼部分:
const actions = {
checkout ({ commit, state }, products) {
const savedCartItems = [...state.added] // 存儲(chǔ)添加到購(gòu)物車的商品
commit(types.CHECKOUT_REQUEST) // 設(shè)置提交結(jié)算狀態(tài)
shop.buyProducts( // 提交api請(qǐng)求,并傳入成功與失敗的cb-func
products,
() => commit(types.CHECKOUT_SUCCESS), // 請(qǐng)求返回成功則設(shè)置提交成功狀態(tài)
() => commit(types.CHECKOUT_FAILURE, { savedCartItems }) // 請(qǐng)求返回失敗則設(shè)置提交失敗狀態(tài)
)
}
}
Vue 組件中點(diǎn)擊購(gòu)買執(zhí)行當(dāng)前 module 的 dispatch 方法,傳入 type 值為 'checkout',payload 值為 'products',在源碼中 dispatch 方法在所有注冊(cè)過(guò)的 actions 中查找 'checkout' 的對(duì)應(yīng)執(zhí)行數(shù)組,取出循環(huán)執(zhí)行。執(zhí)行的是被封裝過(guò)的被命名為 wrappedActionHandler 的方法,真正傳入的 checkout 的執(zhí)行函數(shù)在 wrappedActionHandler 這個(gè)方法中被執(zhí)行,源碼如下(注:前面貼過(guò),這里再看一次):
function wrappedActionHandler (payload, cb) {
let res = handler({
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
}
...
}
handler 在這里就是傳入的 checkout 函數(shù),其執(zhí)行需要的 commit 以及state 就是在這里被傳入,payload 也傳入了,在實(shí)例中對(duì)應(yīng)接收的參數(shù)名為 products。commit 的執(zhí)行也是同理的,實(shí)例中 checkout 還進(jìn)行了一次 commit 操作,提交一次 type 值為 types.CHECKOUT_REQUEST 的修改,因?yàn)?mutation 名字是唯一的,這里進(jìn)行了常量形式的調(diào)用,防止命名重復(fù),執(zhí)行跟源碼分析中一致,調(diào)用function wrappedMutationHandler (payload) { handler(local.state, payload) }
封裝函數(shù)來(lái)實(shí)際調(diào)用配置的 mutation 方法。
看到完源碼分析和上面的小實(shí)例,應(yīng)該能理解 dispatch action 和 commit mutation 的工作原理了。接著看源碼,看看 getters 是如何實(shí)現(xiàn) state 實(shí)時(shí)訪問(wèn)的。
3.6 store._vm 組件設(shè)置
執(zhí)行完各 module 的 install 后,執(zhí)行 resetStoreVM 方法,進(jìn)行 store 組件的初始化。
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
綜合前面的分析可以了解到,Vuex 其實(shí)構(gòu)建的就是一個(gè)名為 store 的 vm 組件,所有配置的 state、actions、mutations 以及 getters 都是其組件的屬性,所有的操作都是對(duì)這個(gè) vm 組件進(jìn)行的。
一起看下resetStoreVM方法的內(nèi)部實(shí)現(xiàn)。
function resetStoreVM (store, state) {
const oldVm = store._vm // 緩存前vm組件
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
// 循環(huán)所有處理過(guò)的getters,并新建computed對(duì)象進(jìn)行存儲(chǔ),通過(guò)Object.defineProperty方法為getters對(duì)象建立屬性,使得我們通過(guò)this.$store.getters.xxxgetter能夠訪問(wèn)到該getters
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
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
const silent = Vue.config.silent
// 暫時(shí)將Vue設(shè)為靜默模式,避免報(bào)出用戶加載的某些插件觸發(fā)的警告
Vue.config.silent = true
// 設(shè)置新的storeVm,將當(dāng)前初始化的state以及getters作為computed屬性(剛剛遍歷生成的)
store._vm = new Vue({
data: { state },
computed
})
// 恢復(fù)Vue的模式
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
// 該方法對(duì)state執(zhí)行$watch以禁止從mutation外部修改state
enableStrictMode(store)
}
// 若不是初始化過(guò)程執(zhí)行的該方法,將舊的組件state設(shè)置為null,強(qiáng)制更新所有監(jiān)聽(tīng)者(watchers),待更新生效,DOM更新完成后,執(zhí)行vm組件的destroy方法進(jìn)行銷毀,減少內(nèi)存的占用
if (oldVm) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation.
store._withCommit(() => {
oldVm.state = null
})
Vue.nextTick(() => oldVm.$destroy())
}
}
resetStoreVm 方法創(chuàng)建了當(dāng)前 store 實(shí)例的 _vm 組件,至此 store 就創(chuàng)建完畢了。上面代碼涉及到了嚴(yán)格模式的判斷,看一下嚴(yán)格模式如何實(shí)現(xiàn)的。
function enableStrictMode (store) {
store._vm.$watch('state', () => {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}, { deep: true, sync: true })
}
很簡(jiǎn)單的應(yīng)用,監(jiān)視 state 的變化,如果沒(méi)有通過(guò)this._withCommit()
方法進(jìn)行 state 修改,則報(bào)錯(cuò)。
3.7 plugin注入
最后執(zhí)行plugin的植入:
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
devtoolPlugin 提供的功能有 3 個(gè):
// 1. 觸發(fā)Vuex組件初始化的hook
devtoolHook.emit('vuex:init', store)
// 2. 提供“時(shí)空穿梭”功能,即state操作的前進(jìn)和倒退
devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
// 3. mutation被執(zhí)行時(shí),觸發(fā)hook,并提供被觸發(fā)的mutation函數(shù)和當(dāng)前的state狀態(tài)
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})
源碼分析到這里,Vuex 框架的實(shí)現(xiàn)原理基本都已經(jīng)分析完畢。
總結(jié)
講道理,讀源碼是個(gè)比較痛苦的過(guò)程,不過(guò)在通讀源碼之后,也會(huì)有豁然開(kāi)朗的感覺(jué)。我在這里放五個(gè)美團(tuán)點(diǎn)評(píng)團(tuán)隊(duì)關(guān)于 Vuex 的問(wèn)題思考,希望能起到一個(gè)總結(jié)的作用吧。
-
問(wèn):使用 Vuex 只需執(zhí)行
Vue.use(Vuex)
,并在Vue的配置中傳入一個(gè) store 對(duì)象的示例,store 是如何實(shí)現(xiàn)注入的?
答:
Vue.use(Vuex)
方法執(zhí)行的是 install 方法,它實(shí)現(xiàn)了 Vue 實(shí)例對(duì)象的 init 方法封裝和注入,使傳入的 store 對(duì)象被設(shè)置到 Vue 上下文環(huán)境的 $store 中。因此在 Vue Component 任意地方都能夠通過(guò)this.$store
訪問(wèn)到該 store。
- 問(wèn):state 內(nèi)部支持模塊配置和模塊嵌套,如何實(shí)現(xiàn)的?
答:在 store 構(gòu)造方法中有 makeLocalContext 方法,所有 module 都會(huì)有一個(gè) local context,根據(jù)配置時(shí)的 path 進(jìn)行匹配。所以執(zhí)行如
dispatch('submitOrder', payload)
這類 action 時(shí),默認(rèn)的拿到都是 module的 local state,如果要訪問(wèn)最外層或者是其他 module 的 state,只能從 rootState 按照 path 路徑逐步進(jìn)行訪問(wèn)。
- 問(wèn):在執(zhí)行 dispatch 觸發(fā) action (commit 同理)的時(shí)候,只需傳入 (type, payload),action 執(zhí)行函數(shù)中第一個(gè)參數(shù) store 從哪里獲取的?
答:store 初始化時(shí),所有配置的 action 和 mutation 以及 getters 均被封裝過(guò)。在執(zhí)行如
dispatch('submitOrder', payload)
的時(shí)候, actions 中 type 為 submitOrder 的所有處理方法都是被封裝后的,其第一個(gè)參數(shù)為當(dāng)前的 store 對(duì)象,所以能夠獲取到{ dispatch, commit, state, rootState }
等數(shù)據(jù)。
- 問(wèn):Vuex 如何區(qū)分 state 是外部直接修改,還是通過(guò) mutation 方法修改的?
答:Vuex 中修改 state 的唯一渠道就是執(zhí)行
commit('xx', payload)
方法,其底層通過(guò)執(zhí)行this._withCommit(fn)
設(shè)置 _committing 標(biāo)志變量為 true,然后才能修改 state,修改完畢還需要還原 _committing 變量。外部修改雖然能夠直接修改 state,但是并沒(méi)有修改 _committing 標(biāo)志位,所以只要 watch 一下 state,state change 時(shí)判斷是否 _committing 值為 true,即可判斷修改的合法性。
- 問(wèn):調(diào)試時(shí)的 "時(shí)空穿梭" 功能是如何實(shí)現(xiàn)的?
答:devtoolPlugin 中提供了此功能。因?yàn)?dev 模式下所有的 state change 都會(huì)被記錄下來(lái),'時(shí)空穿梭' 功能其實(shí)就是將當(dāng)前的 state 替換為記錄中某個(gè)時(shí)刻的 state 狀態(tài),利用
store.replaceState(targetState)
方法將執(zhí)行this._vm.state = state
實(shí)現(xiàn)。
源碼中還有一些工具函數(shù)類似 registerModule、unregisterModule、hotUpdate、watch 以及 subscribe 等,如有興趣可以打開(kāi)源碼看看,這里不再細(xì)述。