Vuex 源碼學(xué)習(xí)
源碼目錄
src:.
│ helpers.js
│ index.esm.js
│ index.js
│ mixin.js
│ store.js
│ util.js
│
├─module
│ module-collection.js
│ module.js
│
└─plugins
devtool.js
logger.js
Vuex 核心 API:
// 初始化實例
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state,
mutations,
actions,
getters,
modules,
plugins
})
const vm = new Vue({
store, // inject store to all children
el: '#app',
render: h => h(App)
})
// 實例方法
commit dispatch
// 輔助函數(shù)
mapState mapGetters mapActions mapMutations
插件安裝
import Vuex from 'vuex'
Vue.use(Vuex)
引入了 src/index.js
暴露的對象:
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
其中包含一個 install
方法,這也是 Vue 官方開發(fā)插件的方式。 install
方法位于 src/store.js
中:
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
這里將傳入的 Vue
(_Vue) 賦值給 Vue
崖面,便于后續(xù)的使用券膀。然后調(diào)用 src/mixin.js
中暴露的方法 applyMinxin(Vue)
浆竭,主要作用就是混入 beforeCreate
鉤子函數(shù)入蛆,保證每個組件的 this.$store
都是初始化實例的 store
。
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
// Vue 2.x版本直接通過Vue.mixin混淆執(zhí)行vuexInit方法
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 1.x版本覆寫原_init 方法
// 加入vuexInit方法
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex 初始化鉤子
*/
function vuexInit () {
const options = this.$options
// store injection
// options.store 代表根組件
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
// 在每個子組件上面掛載store的引用
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
Store 構(gòu)造函數(shù)
const store = new Vuex.Store({
state,
mutations,
actions,
getters,
modules,
plugins
})
const vm = new Vue({
store, // inject store to all children
el: '#app',
render: h => h(App)
})
在上邊代碼中烁设,調(diào)用Store
構(gòu)造函數(shù)創(chuàng)建store
實例。 這里主要是創(chuàng)建一些 store
實例內(nèi)部的屬性钓试,module
注冊以及 mutations
, actions
, getters
的注冊和通過 store._vm
觀測 state, getters
的變化装黑。下邊分析一下store.js
中相對核心的代碼:
this._modules
如果我們在實例化store
對象時,添加了 mod1
模塊
modules: {
mod1: {}
}
this._modules = new ModuleCollection(options)
現(xiàn)在根據(jù)生成的屬性對象弓熏,來進(jìn)行代碼學(xué)習(xí)
src/module-collection.js
:
關(guān)鍵代碼:
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
// 實例化一個module
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// 如果是根module就綁定到root屬性
this.root = newModule
} else {
// 子module添加到父module的 _children屬性上
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
// 注冊嵌套模塊(modules屬性存在)
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
第一次調(diào)用恋谭,path = []
, 進(jìn)入path.length === 0
的邏輯中,實例化 Module
賦值給 this.root
(_modules.root)挽鞠。先不分析 else
的邏輯疚颊,先看下 Module
構(gòu)造函數(shù)做了什么?
constructor (rawModule, runtime) {
// 初始化時runtime為false
this.runtime = runtime
// Store some children item
// _children:保存子模塊
this._children = Object.create(null)
// Store the origin module object which passed by programmer
// 保存原始對象,傳入的信认,也就是父級Module
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
// 保存state 材义,是函數(shù)就執(zhí)行
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
下邊回到剛才src/module-collection.js
, 執(zhí)行到options
含有 modules 屬性時,執(zhí)行以下操作來遞歸注冊模塊
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
這時就不符合path.length === 0
嫁赏,進(jìn)入 else 邏輯:
// 子module添加到父module的 _children屬性上
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
installModule()
installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
// 是否為根Module
const isRoot = !path.length
// 獲取module的完整Namespace (傳入完整的路徑) ["cart", "cart_child"] --> 獲得 cart/cart_child/
const namespace = store._modules.getNamespace(path)
// 如果namespaced為true則在_modulesNamespaceMap中注冊
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
// set state
// 非根設(shè)置state
if (!isRoot && !hot) {
// 根據(jù)path獲取父state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 當(dāng)前module
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 通過Vue.set將state設(shè)置為響應(yīng)式
Vue.set(parentState, moduleName, module.state)
})
}
// 設(shè)置module上下文
// store cart/ ["cart"]
const local = module.context = makeLocalContext(store, namespace, path)
// 遍歷注冊mutation
module.forEachMutation((mutation, key) => {
// cart/pushProductToCart
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 遍歷注冊action
module.forEachAction((action, key) => {
// {root: true} -> 在帶命名空間的模塊注冊全局 action
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 遍歷注冊getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 注冊子module
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
這里主要就是注冊mutation action getter
, 根據(jù)_modules
生成namespace
其掂,分別注冊state mutation action getter
,最后遞歸注冊子模塊潦蝇。
先看 makeLocalContext
清寇,它根據(jù) namespace
創(chuàng)建局部 context
喘漏,分別注冊state mutation action getter
。其實這里namespace: true
會讓state mutation action getter
都擁有自己的全名华烟。這樣可以減少命名沖突配名。
注意:
module.context
屬性,輔助函數(shù)方法中會使用到
在進(jìn)行子module
的注冊時,是遍歷module._children
屬性丁屎。會執(zhí)行
// 非根設(shè)置state
if (!isRoot && !hot) {
// 根據(jù)path獲取父state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 當(dāng)前module
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 通過Vue.set將state設(shè)置為響應(yīng)式
Vue.set(parentState, moduleName, module.state)
})
}
再看下
installModule
過程中的其它 3 個重要方法:registerMutation瘫筐、registerAction 和 registerGetter
:
registerMutation
// 處理mutation === handler
function registerMutation (store, type, handler, local) {
// store._mutations[type]判斷,不存在就賦值空數(shù)組
const entry = store._mutations[type] || (store._mutations[type] = [])
// 將mutation的包裝函數(shù)push到對應(yīng)的mutation對象數(shù)組
entry.push(function wrappedMutationHandler (payload) {
// 調(diào)用我們設(shè)置的mutation的回調(diào)函數(shù) --> commit觸發(fā)
handler.call(store, local.state, payload)
})
}
mutation
的回調(diào)函數(shù)的調(diào)用是通過commit
觸發(fā)的喂链。這里需要通過commit
函數(shù)進(jìn)行了解:
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) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 遍歷這個 type 對應(yīng)的 mutation 對象數(shù)組返十,執(zhí)行 handler(payload)
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 通知所有訂閱者 (_subscribers: 訂閱(注冊監(jiān)聽) store 的 mutation)
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
commit 支持 3 個參數(shù),type 表示 mutation 的類型椭微,payload 表示額外的參數(shù)洞坑,options 表示一些配置。commit 根據(jù) type 去查找對應(yīng)的 mutation蝇率,如果找不到迟杂,則輸出一條錯誤信息,否則遍歷這個 type 對應(yīng)的 mutation 對象數(shù)組本慕,執(zhí)行 handler(payload) 方法排拷,這個方法就是之前定義的 wrappedMutationHandler
,執(zhí)行它就相當(dāng)于執(zhí)行了 registerMutation 注冊的回調(diào)函數(shù)锅尘。注意這里我們依然使用了 this._withCommit 的方法提交 mutation监氢。
registerAction
// 處理action
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
// 返回值如果不是Promise對象就包裝成一個Promise對象
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
}
})
}
和mutation
類似,action
的注冊比mutation
多了一步藤违,將函數(shù)進(jìn)行了Promise
包裝浪腐,這也是為什么action
可以異步的原因。action
是通過dispatch
觸發(fā)的顿乒。
registerGetter
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) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
這里保存 getter 到store._wrappedGetters
上牛欢。
resetStoreVM
// 設(shè)置一個新的vue實例,用來保存state和getter
function resetStoreVM (store, state, hot) {
// 保存之前的vm對象
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
// this.$store.getters.xxxgetters -> store._vm[xxxgetters]
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 enviroment.
computed[key] = partial(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
// 在new一個Vue實例的過程中不會報出一切警告
Vue.config.silent = true
// new一個vue實例, 響應(yīng)式 state->state, computed->getter
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
// 保證修改store只能通過mutation
if (store.strict) {
enableStrictMode(store)
}
// 函數(shù)每次都會創(chuàng)建新的 Vue 實例并賦值到 store._vm
// 這里將舊的 _vm 對象的狀態(tài)設(shè)置為 null淆游,并調(diào)用 $destroy 方法銷毀這個舊的 _vm 對象
if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
利用 store._vm 保存了一個 Vue 實例傍睹,通過 Vue 實例來保留 state 樹,以及用計算屬性的方式存儲了 store 的 getters犹菱。
輔助函數(shù)
mapState
官方示例:
computed: mapState({
count: state => state.count,
countAlias: 'count',
countPlusLocalState (state) {
return state.count + this.localCount
}
})
// 當(dāng)映射的計算屬性的名稱與 state 的子節(jié)點(diǎn)名稱相同時,可以傳遞數(shù)組
computed: mapState(['count'])
// 命名空間
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
normalizeNamespace
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
mapState
首先通過normalizeNamespace
對傳入的參數(shù)進(jìn)行有沒有 namespace 的處理拾稳,而后執(zhí)行 fn(namespace, map)。
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
這里通過normalizeMap
將傳入的數(shù)組或者對象這兩種方式進(jìn)行處理:
/**
* Normalize the 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}
*/
// map 處理
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
mapState 的作用是把全局的 state 和 getters 映射到當(dāng)前組件的 computed 計算屬性中腊脱,Vue 中 每個計算屬性都是一個函數(shù)访得, mapState 函數(shù)的返回值是這樣一個對象:
computed: {
count() {
return this.$store.state.count
}
}
其他就不再一一分析。