【前端詞典】從源碼解讀Vuex注入Vue生命週期的過程

NO IMAGE

前言

這篇文章是【前端詞典】系列文章的第 13 篇文章,接下的 9 篇我會圍繞著 Vue 展開,希望這 9 篇文章可以使大家加深對 Vue 的瞭解。當然這些文章的前提是默認你對 Vue 有一定的基礎。如果一點基礎都沒有,建議先看官方文檔。

第一篇文章我會結合 Vue 和 Vuex 的部分源碼,來說明 Vuex 注入 Vue 生命週期的過程。

說到源碼,其實沒有想象的那麼難。也和我們平時寫業務代碼差不多,都是方法的調用。但是源碼的調用樹會複雜很多。

為何使用 Vuex

使用 Vue 我們就不可避免的會遇到組件間共享的數據或狀態。應用的業務代碼逐漸複雜,props、事件、事件總線等通信的方式的弊端就會愈發明顯。這個時候我們就需要 Vuex 。Vuex 是一個專門為 Vue 設計的狀態管理工具。

狀態管理是 Vue 組件解耦的重要手段。

它借鑑了 Flux、redux 的基本思想,將狀態抽離到全局,形成一個 Store。

【前端詞典】從源碼解讀Vuex注入Vue生命週期的過程

Vuex 不限制你的代碼結構,但需要遵守一些規則:

  1. 應用層級的狀態應該集中到單個 store 對象中
  2. 提交 mutation 是更改狀態的唯一方法,並且這個過程是同步的
  3. 異步邏輯都應該封裝到 action 裡面

Vuex 注入 Vue 生命週期的過程

我們在安裝插件的時候,總會像下面一樣用 Vue.use() 來載入插件,可是 Vue.use() 做了什麼呢?

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);

Vue.use() 做了什麼

安裝 Vue.js 插件。如果插件是一個對象,必須提供 install 方法。如果插件是一個函數,它會被作為 install 方法。install 方法調用時,會將 Vue 作為參數傳入。

以上是 官方文檔 的解釋。

接下來我們從源碼部分來看看 Vue.use() 都做了什麼。

Vue 源碼在 initGlobalAPI 入口方法中調用了 initUse (Vue) 方法,這個方法定義了 Vue.use() 需要做的內容。

function initGlobalAPI (Vue) {
......
initUse(Vue);
initMixin$1(Vue); // 下面講 Vue.mixin 會提到
......
}
function initUse (Vue) {
Vue.use = function (plugin) {
var installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
/* 判斷過這個插件是否已經安裝 */
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
var args = toArray(arguments, 1);
args.unshift(this);
/* 判斷插件是否有 install 方法 */
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
plugin.apply(null, args);
}
installedPlugins.push(plugin);
return this
};
}

這段代碼主要做了兩件事情:

  1. 一件是防止重複安裝相同的 plugin
  2. 另一件是初始化 plugin

插件的 install 方法

看完以上源碼,我們知道插件(Vuex)需要提供一個 install 方法。那麼我們看看 Vuex 源碼中是否有這個方法。結果當然是有的:

/* 暴露給外部的 install 方法 */
function install (_Vue) {
/* 避免重複安裝(Vue.use 內部也會檢測一次是否重複安裝同一個插件)*/
if (Vue && _Vue === Vue) {
{
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
);
}
return
}
Vue = _Vue;
/* 將 vuexInit 混淆進 Vue 的 beforeCreate(Vue2.0) 或 _init 方法(Vue1.0) */
applyMixin(Vue);
}

這段代碼主要做了兩件事情:

  1. 一件是防止 Vuex 被重複安裝
  2. 另一件是執行 applyMixin,目的是執行 vuexInit 方法初始化 Vuex

接下來 我們看看 applyMixin(Vue) 源碼:

/* 將 vuexInit 混淆進 Vue 的 beforeCreate */
function applyMixin (Vue) {
var version = Number(Vue.version.split('.')[0]);
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit });
} else {
/* Vue1.0 的處理邏輯,此處省略 */
......
}
function vuexInit () {
......
}
}

從上面的源碼,可以看出 Vue.mixin 方法將 vuexInit 方法混淆進 beforeCreate 鉤子中,也是因為這個操作,所以每一個 vm 實例都會調用 vuexInit 方法。那麼 vuexInit 又做了什麼呢?

vuexInit()

我們在使用 Vuex 的時候,需要將 store 傳入到 Vue 實例中去。

new Vue({
el: '#app',
store
});

但是我們卻在每一個 vm 中都可以訪問該 store,這個就需要靠 vuexInit 了。

  function vuexInit () {
const options = this.$options
if (options.store) {
/* 根節點存在 stroe 時 */
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
/* 子組件直接從父組件中獲取 $store,這樣就保證了所有組件都公用了全局的同一份 store*/
this.$store = options.parent.$store
}
}

根節點存在 stroe 時,則直接將 options.store 賦值給 this.$store。否則則說明不是根節點,從父節點的 $store 中獲取。

通過這步的操作,我們就以在任意一個 vm 中通過 this.$store 來訪問 Store 的實例。接下來我們反過來說說 Vue.mixin()。

Vue.mixin()

全局註冊一個混入,影響註冊之後所有創建的每個 Vue 實例。插件作者可以使用混入,向組件注入自定義的行為。不推薦在應用代碼中使用。

在 vue 的 initGlobalAPI 入口方法中調用了 initMixin$1(Vue) 方法:

function initMixin$1 (Vue) {
Vue.mixin = function (mixin) {
this.options = mergeOptions(this.options, mixin);
return this
};
}

Vuex 注入 Vue 生命週期的過程大概就是這樣,如果你感興趣的話,你可以直接看看 Vuex 的源碼,接下來我們說說 Store。

Store

上面我們講到了 vuexInit 會從 options 中獲取 Store。所以接下來會講到 Store 是怎麼來的呢?

我們使用 Vuex 的時候都會定義一個和下面類似的 Store 實例。

import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
Vue.use(Vuex)
const state = {
showState: 0,                             
}
export default new Vuex.Store({
strict: true,
state,
getters,
})

不要在發佈環境下啟用嚴格模式。嚴格模式會深度監測狀態樹來檢測不合規的狀態變更 —— 請確保在發佈環境下關閉嚴格模式,以避免性能損失。

state 的響應式

你是否關心 state 是如何能夠響應式呢?這個主要是通過 Store 的構造函數中調用的 resetStoreVM(this, state) 方法來實現的。

這個方法主要是重置一個私有的 _vm(一個 Vue 的實例) 對象。這個 _vm 對象會保留我們的 state 樹,以及用計算屬性的方式存儲了 store 的 getters。現在具體看看它的實現過程。

/* 使用 Vue 內部的響應式註冊 state */
function resetStoreVM (store, state, hot) {
/* 存放之前的vm對象 */
const oldVm = store._vm 
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
/* 通過 Object.defineProperty 方法為 store.getters 定義了 get 方法。當在組件中調用 this.$store.getters.xxx 這個方法的時候,會訪問 store._vm[xxx]*/
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
const silent = Vue.config.silent
/* 設置 silent 為 true 的目的是為了取消 _vm 的所有日誌和警告 */
Vue.config.silent = true
/*  這裡new了一個Vue對象,運用Vue內部的響應式實現註冊state以及computed*/
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
/* 使能嚴格模式,Vuex 中對 state 的修改只能在 mutation 的回調函數裡 */
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
/* 解除舊 vm 的 state 的引用,並銷燬這個舊的 _vm 對象 */
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}

state 的響應式大概就是這樣實現的,也就是初始化 resetStoreVM 方法的過程。

看看 Store 的 commit 方法

我們知道 commit 方法是用來觸發 mutation 的。

commit (_type, _payload, _options) {
/* unifyObjectStyle 方法校參 */
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
/* 找到相應的 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)
})
})
/* 通知所有訂閱者,傳入當前的 mutation 對象和當前的 state */
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'
)
}
}

該方法先進行參數風格校驗,然後利用 _withCommit 方法執行本次批量觸發 mutation 處理函數。執行完成後,通知所有 _subscribers(訂閱函數)本次操作的 mutation 對象以及當前的 state 狀態。

Vue 相關文章輸出計劃

最近總有朋友問我 Vue 相關的問題,因此接下來我會輸出 9 篇 Vue 相關的文章,希望對大家有一定的幫助。我會保持在 7 到 10 天更新一篇。

  1. 【前端詞典】Vuex 注入 Vue 生命週期的過程
  2. 【前端詞典】淺析 Vue 響應式原理
  3. 【前端詞典】新老 VNode 進行 patch 的過程
  4. 【前端詞典】如何開發功能組件並上傳 npm
  5. 【前端詞典】從這幾個方面優化你的 Vue 項目
  6. 【前端詞典】從 Vue-Router 設計講前端路由發展
  7. 【前端詞典】在項目中如何正確的使用 Webpack
  8. 【前端詞典】Vue 服務端渲染
  9. 【前端詞典】Axios 與 Fetch 該如何選擇

建議你關注我的公眾號,第一時間就可以接收最新的文章。

【前端詞典】從源碼解讀Vuex注入Vue生命週期的過程

如果你想加群交流,也可以添加有點智能的機器人,自動拉你進群:

【前端詞典】從源碼解讀Vuex注入Vue生命週期的過程

熱門文章傳送門

  1. 【前端詞典】滾動穿透問題的解決方案
  2. 【前端詞典】5 種滾動吸頂實現方式的比較(性能升級版)
  3. 【前端詞典】提高幸福感的 9 個 CSS 技巧
  4. 【前端詞典】分享 8 個有趣且實用的 API
  5. 【前端詞典】從輸入 URL 到展現涉及哪些緩存環節(非常詳細)

相關文章

【前端詞典】Vue響應式原理其實很好懂

三個很不錯的Vue資料

【前端詞典】學習Vue源碼的必要知識儲備

【前端詞典】如何向老闆解釋反向代理