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

前言

這篇文章是【前端詞典】系列文章的第 13 篇文章,接下的 9 篇我會圍繞着 Vue 展開,但願這 9 篇文章可使你們加深對 Vue 的瞭解。固然這些文章的前提是默認你對 Vue 有必定的基礎。若是一點基礎都沒有,建議先看官方文檔。html

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

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

爲什麼使用 Vuex

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

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

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

Vuex 不限制你的代碼結構,但須要遵照一些規則:redux

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

Vuex 注入 Vue 生命週期的過程

咱們在安裝插件的時候,總會像下面同樣用 Vue.use() 來載入插件,但是 Vue.use() 作了什麼呢?api

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);
複製代碼

Vue.use() 作了什麼

安裝 Vue.js 插件。若是插件是一個對象,必須提供 install 方法。若是插件是一個函數,它會被做爲 install 方法。install 方法調用時,會將 Vue 做爲參數傳入。緩存

以上是 官方文檔 的解釋。bash

接下來咱們從源碼部分來看看 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 該如何選擇

建議你關注個人公衆號,第一時間就能夠接收最新的文章。

若是你想加羣交流,也能夠添加有點智能的機器人,自動拉你進羣:

熱門文章傳送門

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