首先我們先從一個面試題入手。
面試官問: “Vue中組件通信的常用方式有哪些?”
我答:
1. props
2. 自定義事件
3. eventbus
4. vuex
5. 還有常見的邊界情況$parent、$children、$root、$refs、provide/inject
6. 此外還有一些非props特性$attrs、$listeners
面試官追問:“那你能分別說說他們的原理嗎?”
我:[一臉懵逼]😳
今天我們來看看Vuex內部的奧秘!
如果要看別的屬性原理請移步到Vue組件通信原理剖析(一)事件總線的基石 on和on和on和emit和Vue組件通信原理剖析(三)provide/inject原理分析
vuex
Vuex集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以可預測的方式發生變化。
我們先看看如何使用vuex,
-
第一步:定義一個Store
// store/index.js import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default = new Vuex.Store({state: {counter: 0},getters: {doubleCounter(state) {return state.counter * 2}},mutations: {add(state) {state.counter ++ }},actions: {add({commit}) {setTimeout(() => {commit('add')}, 1000);}} })
-
第二步,掛載app
// main.js import vue from 'vue' import App form './App.vue' import store from './store'new Vue({store,render: h => h(App) }).$mount('#app')
-
第三步:狀態調用
// test.vue <p @click="$store.commit('add')">counter: {{ $store.state.counter }}</p> <p @click="$store.dispatch('add')">async counter: {{ $store.state.counter }}</p> <p>double counter: {{ $store.getters.doubleCounter }}</p>
從上面的例子,我們可以看出,vuex需要具備這么幾個特點:
- 使用Vuex只需執行
Vue.use(Vuex)
,保證vuex是以插件的形式被vue加載。 - state的數據具有響應式,A組件中修改了,B組件中可用修改后的值。
- getters可以對state的數據做動態派生。
- mutations中的方法是同步修改。
- actions中的方法是異步修改。
那我們今天就去源碼里探索以下,vuex是怎么實現的,又是怎么解決以上的問題的!
問題1:vuex的插件加載機制
所謂插件機制,就是需要實現Install方法,并且通過mixin
形式混入到Vue的生命周期中,我們先來看看Vuex的定義
-
需要對外暴露一個對象,這樣就可以滿足
new Vuex.Store()
// src/index.js 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,并且實現vue的Install方法
// src/store.js let Vue // bind on installexport class Store {...... }// 實現的Install方法 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 = _VueapplyMixin(Vue) }
問題2:state的數據響應式
看懂了Vuex的入口定義,下面我們就針對store的定義來一探究竟,先看看state的實現
// src/store.js
export class Store {constructor(options = {}) {......// strict modethis.strict = strictconst state = this._modules.root.state// initialize the store vm, which is responsible for the reactivity// (also registers _wrappedGetters as computed properties)// 看上面的注釋可以得知,resetStoreVM就是初始化store中負責響應式的vm的方法,而且還注冊所有的gettersz作為vm的計算屬性resetStoreVM(this, state)}
}
我們來看看resetStoreVM的具體實現
// src/store.js
function resetStoreVM (store, state, hot) {const oldVm = store._vm// bind store public gettersstore.getters = {}// reset local getters cachestore._makeLocalGettersCache = Object.create(null)const wrappedGetters = store._wrappedGettersconst computed = {}// 這里是實現getters的派生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.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// 這是是通過new一個Vue實例,并將state作為實例的datas屬性,那他自然而然就具有了響應式const silent = Vue.config.silentVue.config.silent = truestore._vm = new Vue({data: {$$state: state},computed})Vue.config.silent = silent// enable strict mode for new vmif (store.strict) {enableStrictMode(store)}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())}
}
問題3:getters實現state中的數據的派生
關于getters的實現,我們在上面也做了相應的解釋,實際上就是將getters的方法包裝一層后,收集到computed對象中,并使用Object.defineProperty注冊store.getters,使得每次取值時,從store._vm中取。
關鍵的步驟就是創建一個Vue的實例
store._vm = new Vue({data: {$$state: state // 這是store中的所有state},computed // 這是store中的所有getters
})
問題4:mutations中同步commit
// src/store.js
// store的構造函數
constructor(options = {}) {// 首先在構造方法中,把store中的commit和dispatch綁定到自己的實例上,// 為什么要這么做呢?// 是因為在commit或者dispatch時,尤其是dispatch,執行function時會調用實例this,而方法體內的this是具有作用域屬性的,所以如果要保證每次this都代表store實例,就需要重新綁定一下。const store = thisconst { dispatch, commit } = thisthis.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 的實現
commit (_type, _payload, _options) {// check object-style commitconst {type,payload,options} = unifyObjectStyle(_type, _payload, _options)const mutation = { type, payload }// 通過傳入的類型,查找到mutations中的對應的入口函數const entry = this._mutations[type]......// 這里是執行的主方法,通過遍歷入口函數,并傳參執行this._withCommit(() => {entry.forEach(function commitIterator (handler) {handler(payload)})})......
}
問題5:actions中的異步dispatch
上面說了在構造store時綁定dispatch的原因,下面我們就繼續看看dispatch的具體實現。
// src/store.js
// dispatch 的實現
dispatch (_type, _payload) {// check object-style dispatchconst {type,payload} = unifyObjectStyle(_type, _payload)const action = { type, payload }// 同樣的道理,通過type獲取actions中的入口函數const entry = this._actions[type]······// 由于action是異步函數的集合,這里就用到了Promise.all,來合并多個promise方法并執行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中狀態存儲和狀態變更的流程系統的串聯了一遍,讓我們對Vuex內部的機智有個簡單的認識,最后我們根據我們對Vuex的理解來實現一個簡單的Vuex。
// store.js
let Vue// 定義store類
class Store{constructor(options = {}) {this.$options = optionsthis._mutations = options.mutationsthis._actions = options.actionsthis._wrappedGetters = options.getters// 定義computedconst computed = {}this.getters = {}const store = thisObject.keys(this._wrappedGetters).forEach(key => {// 獲取用戶定義的gettersconst fn = store._wrappedGetters[key]// 轉換為computed可以使用無參數形式computed[key] = function() {return fn(store.state)}// 為getters定義只讀屬性Object.defineProperty(store.getters, key {get:() => store._vm[key]})})// state的響應式實現this._vm = new Vue({data: {// 加兩個$,Vue不做代理$$state: options.state},computed // 添加計算屬性})this.commit = this.commit.bind(this)this.dispatch = this.dispatch.bind(this)}// 存取器,獲取store.state ,只通過get形式獲取,而不是直接this.xxx, 達到對stateget state() {return this._vm._data.$$state}set state(v) {// 如果用戶不通過commit方式來改變state,就可以在這里做一控制}// commit的實現commit(type, payload) {const entry = this._mutations[type]if (entry) {entry(this.state, payload)}}// dispatch的實現dispatch(type, payload) {const entry = this._actions[type]if (entry) {entry(this, payload)}}
}// 實現install
function install(_Vue) {Vue = _VueVue.mixin({beforeCreate() {if (this.$options.store) {Vue.prototype.$Store = this.$options.store // 這樣就可以使用 this.$store}}})
}// 導出Vuex對象
export default {Store,install
}
全部文章鏈接
Vue組件通信原理剖析(一)事件總線的基石 on和on和on和emit
Vue組件通信原理剖析(二)全局狀態管理Vuex
Vue組件通信原理剖析(三)provide/inject原理分析
最后喜歡我的小伙伴也可以通過關注公眾號“劍指大前端”,或者掃描下方二維碼聯系到我,進行經驗交流和分享,同時我也會定期分享一些大前端干貨,讓我們的開發從此不迷路。