本文章主要是拆解vuex v2.x的源碼,快速看懂vuex1.0與vuex2.0的實現原理。css
vuex的源碼是基於es6語法,因此在閱讀文章以前,你須要先掌握這些基礎:html
打開vuex gitHub 由於到我寫這篇文章爲止,vuex已經發展到了3.0, 3.*是使用ts語法編寫,因此相對來講閱讀成本又要進階一層,我本身也還沒徹底閱讀完,後期將會出vuex3.x的源碼解析,還有typescript的相關文章。前端
github上選擇 tag 咱們選擇一個最高版本的v2.5.0,clone到本地 vue
完成後打開文件,能夠看到: 其中src目錄下就是vuex的源碼實現,層級最多就2層,閱讀的起點,當前要從index.js開始。 先看index.js主要輸出了哪些東西:git
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
複製代碼
讓咱們來大體認識下它們: store vuex 對外提供的狀態存儲類,用在項目中,它就是一個狀態管理器,集中管理狀態
install vuex提供的在項目中安裝自身的方法
version vue版本號
mapState 爲組件建立計算屬性以返回 Vuex store 中的狀態
mapMutations 建立組件方法提交 mutation
mapGetters 爲組件建立計算屬性以返回 getter 的返回值
mapActions 建立組件方法分發 action
createNamespacedHelpers 建立基於命名空間的組件綁定輔助函數
接下來,我會以vue項目中使用vuex的順序來解析下vuex是如何發揮做用的,後面再展開vuex的其餘屬性與方法es6
install 爲何不是從store開始說,由於這個纔是vuex插入到vue項目中的第一步哈,install與store都來自於store.js 文件github
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)
}
複製代碼
代碼看起來很簡單吧,install 裏面主要作了什麼: 1.判斷是否有全局的Vue(即window.Vue), 若是沒有,就賦值,確保咱們的vuex只被install一次 Vue.use(vuex)就是執行了vuex的install方法;而後把vue掛載到全局對象上,這樣子就能夠在其餘地方訪問vue拉 最後一行的applyMixin 來看下具體代碼:vuex
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 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)
}
}
function vuexInit () {
const options = this.$options
// store injection
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版本號,在vue的生命週期初始化鉤子(vue1.0的版本是init, vue2.*的是beforeCreated)前執行一斷代碼,具體執行代碼在vuexInit中。給vue實例添加一個store,判斷判斷vue實例的初始化選項this.options裏面有沒有store,有就直接賦值到實例屬性store,這樣子咱們就能夠在vue實例裏面經過this.store,而不用辛苦地this.options.store或者this.options.parent.$store去訪問了。typescript
實例化store,咱們來看Store的構造函數裏面作了哪些事情:api
export class Store {
constructor (options = {}) {
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
if (process.env.NODE_ENV !== 'production') {
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.`)
}
...
}
複製代碼
確保install, 接下來是一些斷言函數,不過js裏面是沒有這個斷言函數,就是模擬斷言函數。 接下來是一些屬性賦值,以及把類方法dispatch, commit的this指向當前的store實例,代碼就不貼了
再繼續看:
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
複製代碼
這三行代碼作了很重要的事情。installModule安裝options(建立store實例的時候,傳入的模塊) resetStoreVM 方法是初始化 store._vm,觀測 state 和 getters 的變化; plugins 使用傳入的插件
一個一個方法來具體查看
#### installModule
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// set 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)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
複製代碼
最多接收5個傳入參數
store 表示當前 Store 實例
rootState 表示根 state
path 表示當前嵌套模塊的路徑數組
module 表示當前安裝的模塊
hot 當動態改變 modules 或者熱更新的時候爲 true
接下來就是對初始化傳入的options進行註冊安裝。具體的什麼實現,將會在後面的文章具體說明這邊咱們先了解它是什麼動做,其中對module的註冊值得關注下:
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
複製代碼
由於store是單一的狀態樹,若是當須要管理的數據愈來愈多,這棵樹就變得難以維護,因此引入module使得其結構更規範易維護。
執行完installModule,就執行resetStoreVM
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
複製代碼
給store設置一個私有變量_vm,它是vue實例,這個 _vm 對象會保留咱們的 state 樹,以及用計算屬性的方式存儲了 store 的 getters。
Vuex 的初始化主要核心就是 installModule 和 resetStoreVM 函數。經過對 mutations 、actions 和 getters 的註冊,state 的是按模塊劃分的,按模塊的嵌套造成一顆狀態樹。而 actions、mutations 和 getters 的全局的。 對vuex的初始化有一個大體的瞭解以後,讓咱們來對咱們在項目中用到的api來具體說明下是怎麼實現的,也會將vuex初始化的時候一些未說明清楚的東西說清楚。
先看下commit的函數定義:
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
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
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'
)
}
}
複製代碼
先看下dispatch的函數定義:
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
this._actionSubscribers.forEach(sub => sub(action, this.state))
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
傳入的參數:
_type action 類型
_payload 咱們要更新的值
複製代碼
先來看下subscribe的函數定義
subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
}
複製代碼
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)
}
複製代碼
getter 監聽屬性名 cb 對應的回調 options 可選,如{deep: true}之類的配置 響應式的接收一個getter返回值,當值改變時調用回調函數。getter接收state狀態爲惟一參數,而且options做爲_vm.watch的參數。