不少時候咱們在開發一個Vue項目的時候,用一個Vue實例封裝的EventBus來處理事件的傳遞從而達到組件間狀態的共享。可是隨着業務的複雜度提高,組件間共享的狀態變得難以追溯和維護。所以咱們須要將這些共享的狀態經過一個全局的單例對象保存下來,在經過指定的方法去更新狀態更新組件。javascript
既然都說vuex是解決組件間數據通訊的一種方式,那咱們先來回顧下組件間通訊的幾種方法:前端
這種方法咱們能夠直接將父組件的值傳遞給子組件,並在子組件中調用。很明顯,props是一種單向的數據綁定,而且子組件不能去修改props的值。在vue1.x中能夠經過.async來實現雙向綁定,可是這種雙向的綁定很難去定位數據錯誤的來源,在vue2.3.0版本又加回了.async。vue
// 父組件
<Child name="hahaha" />
// 子組件
<div>{{name}}</div>
// ...
props: ['name']
// ...
複製代碼
$on $emit
若是子組件向父組件傳遞數據,咱們能夠經過$emit
和$on
,在子組件註冊事件,在父組件監聽事件並做回調。java
// 父組件
<Child @getName="getNameCb" />
// ...
getNameCb(name) {
console.log('name');
}
// 子組件
someFunc() {
this.$emit('getName', 'hahahah');
}
複製代碼
前面兩種方式很容易就解決了父子組件的通訊問題,可是很難受的是,處理兄弟組件或者是祖孫組件的通訊時你須要一層一層的去傳遞props,一層一層的去$emit
。那麼其實就可使用EventBus了,EventBus其實是一個Vue的實例,咱們經過Vue實例的$emit
和$on
來進行事件的發佈訂閱。可是問題也很明顯,過多的使用EventBus也會形成數據源難以追溯的問題,而且不及時經過$off
註銷事件的化,也會發生不少奇妙的事情。vue-router
import EventBus from '...';
// 某一個組件
// ...
mounted() {
EventBus.$on('someevent', (data) => {
// ...
})
}
// ...
// 某另外一個組件
// ...
someFunc() {
EventBus.$emit('someevent', 'hahahah');
}
// ...
複製代碼
接下來就是咱們要講的Vuex了,以上這些問題Vuex均可以解決,Vuex也是Vue官方團隊維護的Vue全家桶中的一員,做爲Vue的親兒子,Vuex毫無疑問是很是適合Vue項目的了。可是Vuex也不是完美的,毫無疑問在應用中加一層Store或多或少的都會增長學習和維護的成本,而且說白了一個小項目沒幾個組件,Vuex只會增長你的代碼量,酌情使用吧。下面就進入到咱們Vuex源碼學習的正文了。vuex
回顧一下Vuex的設計原理。咱們把組件間共享的狀態存儲到Vuex的state中,而且組件會根據這個state的值去渲染。當須要更新state的時候,咱們在組件中調用Vuex提供的dispatch方法去觸發action,而在action中去經過commit方法去提交一個mutation,最後經過mutation去直接修改state,組件監聽到state的更新最後更新組件。須要注意的有,mutaion不能執行異步操做,異步操做須要放到action中去完成;直接修改state的有且僅有mutation。(具體的使用方法筆者就不去囉嗦了,官方文檔寫的很詳細,還有中文版,爲啥不看...)數據庫
在筆者看來,Vuex的做用是用來解決組件間狀態的共享,使項目更加利於維護,一樣也是貫徹單向數據流這個理念。但其實從功能上講,Vuex也像是一個前端的「數據庫」,咱們在使用Vuex時很像是後端同窗對庫的增刪改查。redux
在Vue的項目中,咱們也能夠去使用Redux等來處理共享的狀態,甚至是能夠本身簡單封裝一個工具來處理狀態,畢竟引入Vuex對開發同窗來講也是有必定成本的。可是歸根到底都是單向數據流的思想,一通則百通。後端
插個題外話,筆者在研究Vue ssr的時候不想去用Vuex作先後端狀態共享,因而基於EventBus的思想對Vue實例進行了封裝也一樣實現了Vuex的功能,有興趣的同窗能夠看下。戳這裏。數組
首先咱們將掛載完Vuex實例的Vue實例打印出來看看掛載完增長了哪些東西。
拿到一個項目的源碼咱們要先去瀏覽他它的目錄結構:
一般在構建包含Vuex的程序的時候會這麼寫:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex({
state: {...},
mutations: {...},
actions: {...},
});
new Vue({
store,
template,
}).$mount('#app')
複製代碼
用過redux的小夥伴能夠發現Vuex採用的是面向對象化的配置方式,不一樣於redux那種「偏函數式的初始化」,能更容易的讓開發者理解。而且Vuex是以插件的形式安裝在Vue實例上。
在store.js中定義了一個符合Vue插件機制的導出函數install,而且封裝了一個beforeCreate的mixin。
源碼位置:/src/store.js /src/mixin.js
// store.js
// ...
// 綁定一個Vue實例;
// 不用將Vue打包進項目即可以使用Vue的提供的一些靜態方法;
let Vue
// ...
// Vue 插件機制
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
// 封裝mixin掛載$store
applyMixin(Vue)
}
// mixin.js
export default function (Vue) {
// 獲取版本號
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 兼容低版本的Vue
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
// 封裝mixin;
// 綁定$store實例;
// 子組件的$store也始終指向根組件掛載的store實例;
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
// store多是一個工廠函數,vue ssr中避免狀態交叉污染一般會用工廠函數封裝store;
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 子組件從其父組件引用$store屬性,嵌套設置
this.$store = options.parent.$store
}
}
}
複製代碼
這裏其實作的很簡單就是在beforeCreate鉤子中爲Vue實例綁定了一個$store屬性指向咱們定義的Store實例上。此外也能夠看到Vuex也採用了很常見的導出一個Vue實例,從而不將Vue打包進項目就能使用Vue提供的一些方法。
實例化Store類,咱們先來看Store類的構造函數:
源碼位置:/src/store.js
constructor (options = {}) {
// 若是window上有Vue實例,直接安裝插件;
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.`)
}
// 實例化store時傳入的配置項;
const {
plugins = [],
strict = false
} = options
// store internal state
// 收集commit
this._committing = false
// 收集action
this._actions = Object.create(null)
// action訂閱者
this._actionSubscribers = []
// 收集mutation
this._mutations = Object.create(null)
// 收集getter
this._wrappedGetters = Object.create(null)
// 收集module
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
// 用以處理狀態變化的Vue實例
this._watcherVM = new Vue()
// 將dispatch和commit調用的this指向Store實例;
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)
}
// strict mode
this.strict = strict
// 獲取state
const state = this._modules.root.state
// 主要做用就是生成namespace的map,掛載action、mutation、getter;
installModule(this, state, [], this._modules.root)
// 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed
resetStoreVM(this, state)
// 使用插件
plugins.forEach(plugin => plugin(this))
if (Vue.config.devtools) {
devtoolPlugin(this)
}
複製代碼
能夠看出整個構造函數中,主要就是聲明一些基礎的變量,而後最主要的就是執行了intsllModule函數來註冊Module和resetStoreVM來使Store具備「響應式」。 至於ModuleCollection相關的代碼咱們暫且不去深究,知道他就是一個Module的收集器,而且提供了一些方法便可。
接下來看這兩個主要的方法,首先是installModule,在這個方法中回去生成命名空間,而後掛載mutation、action、getter:
源碼位置:/src/store.js
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// 生成name 和 Module 的 Map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
// 爲module註冊響應式;
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
// 掛載mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 掛載action
module.forEachAction((action, key) => {
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
function registerMutation (store, type, handler, local) {
// 在_mutations中找到對應type的mutation數組
// 若是是第一次建立,就初始化爲一個空數組
const entry = store._mutations[type] || (store._mutations[type] = [])
// push一個帶有payload參數的包裝過的函數
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
// 註冊action
function registerAction (store, type, handler, local) {
// 根據type找到對應的action;
const entry = store._actions[type] || (store._actions[type] = [])
// push一個帶有payload參數的包裝過的函數
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)
// 若是 res 不是 promise 對象 ,將其轉化爲promise對象
// 這是由於store.dispatch 方法裏的 Promise.all()方法。
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
}
})
}
// 註冊getter
function registerGetter (store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// 將定義的getter所有存儲到_wrappedGetters中;
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
複製代碼
在Vuex的module中,咱們是能夠拆分不少個module出來的,每個拆分出來的module又能夠看成一個全新的module掛載在父級module上,所以這時候就須要一個path變量來區分層級關係了,咱們能夠根據這個path來去拿到每一次module下的state、mutation、action等。
接下來是resetStoreVM這個方法,在這個方法中,爲store綁定了一個指向新的Vue實例的_vm屬性,同時傳入了state和computed,computed就是咱們在store中設置的getter。
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
// 爲每個getter設置get;
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
Vue.config.silent = true
// 爲store綁定Vue實例並註冊state和computed
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
// 去除綁定舊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())
}
}
複製代碼
在Vuex中有兩個重要的操做,一個是dispatch,一個是commit,咱們經過dispatch去觸發一個action,而後在action中咱們經過提交commit去達到更新state的目的。下面就來看看這兩部門的源碼。
源碼位置:/src/store.js
commit (_type, _payload, _options) {
// check object-style commit
// 檢驗類型;
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 找到type對應的mutation方法;
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 執行mutation;
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 (_type, _payload) {
// check object-style dispatch
// 檢驗值;
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
// 獲取type對應的action;
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
// 通知action訂閱者;
this._actionSubscribers.forEach(sub => sub(action, this.state))
// 返回action
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
複製代碼
Vuex爲咱們提供了一些靜態方法,都是經過調用綁定在Vue實例上的Store實例來操做咱們的state、mutation、action和getter等。
源碼位置:/src/helpers.js
//返回一個對象
//對象的屬性名對應於傳入的 states 的屬性名或者數組元素
//執行這個函數的返回值根據 val 的不一樣而不一樣
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
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]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
// 返回一個對象
// 執行這個函數後將觸發指定的 mutation
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
})
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
// thie namespace has been mutate by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
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
})
// 接受一個對象或者數組,最後都轉化成一個數組形式,數組元素是包含key和value兩個屬性的對象
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
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)
}
}
複製代碼
筆者沒有將所有的源碼貼出來逐行分析,只是簡單的分析了核心邏輯的源碼。總的來講Vuex源碼很少,寫的很精練也很易懂,但願你們都能抽時間親自看看源碼學習學習。