Vuex 原始碼深度解析

yck發表於2018-09-10

該文章內容節選自團隊的開源專案 InterviewMap。專案目前內容包含了 JS、網路、瀏覽器相關、小程式、效能優化、安全、框架、Git、資料結構、演算法等內容,無論是基礎還是進階,亦或是原始碼解讀,你都能在本圖譜中得到滿意的答案,希望這個面試圖譜能夠幫助到大家更好的準備面試。

Vuex 思想

在解讀原始碼之前,先來簡單瞭解下 Vuex 的思想。

Vuex 全域性維護著一個物件,使用到了單例設計模式。在這個全域性物件中,所有屬性都是響應式的,任意屬性進行了改變,都會造成使用到該屬性的元件進行更新。並且只能通過 commit 的方式改變狀態,實現了單向資料流模式。

Vuex 解析

Vuex 安裝

在看接下來的內容前,推薦本地 clone 一份 Vuex 原始碼對照著看,便於理解。

在使用 Vuex 之前,我們都需要呼叫 Vue.use(Vuex) 。在呼叫 use 的過程中,Vue 會呼叫到 Vuex 的 install 函式

install 函式作用很簡單

  • 確保 Vuex 只安裝一次
  • 混入 beforeCreate 鉤子函式,可以在元件中使用 this.$store
export function install (_Vue) {
  // 確保 Vuex 只安裝一次
  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)
}

// applyMixin
export default function (Vue) {
  // 獲得 Vue 版本號
  const version = Number(Vue.version.split('.')[0])
  // Vue 2.0 以上會混入 beforeCreate 函式
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // ...
  }
  // 作用很簡單,就是能讓我們在元件中
  // 使用到 this.$store
  function vuexInit () {
    const options = this.$options
    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
    }
  }
}
複製程式碼

Vuex 初始化

this._modules

本小節內容主要解析如何初始化 this._modules

export class Store {
  constructor (options = {}) {
    // 引入 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.`)
    }
    // 獲取 options 中的屬性
    const { 
      plugins = [],
      strict = false
    } = options

    // store 內部的狀態,重點關注 this._modules
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    
    const store = this
    const { dispatch, commit } = this
    // bind 以下兩個函式上 this 上
    // 便於 this.$store.dispatch
    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)
    }
}
複製程式碼

接下來看 this._modules 的過程,以 以下程式碼為例

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  state: { ... },
  modules: {
    a: moduleA,
    b: moduleB
  }
})
複製程式碼

對於以上程式碼,store 可以看成 root 。在第一次執行時,會初始化一個 rootModule,然後判斷 root 中是否存在 modules 屬性,然後遞迴註冊 module 。對於 child 來說,會獲取到他所屬的 parent, 然後在 parent 中新增 module

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }
  register (path, rawModule, runtime = true) {
    // 開發環境斷言
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }
    // 初始化 Module
    const newModule = new Module(rawModule, runtime)
    // 對於第一次初始化 ModuleCollection 時
    // 會走第一個 if 條件,因為當前是 root
    if (path.length === 0) {
      this.root = newModule
    } else {
      // 獲取當前 Module 的 parent
      const parent = this.get(path.slice(0, -1))
      // 新增 child,第一個引數是
      // 當前 Module 的 key 值
      parent.addChild(path[path.length - 1], newModule)
    }

    // 遞迴註冊
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // 用於儲存 children
    this._children = Object.create(null)
    // 用於儲存原始的 rawModule
    this._rawModule = rawModule
    const rawState = rawModule.state

    // 用於儲存 state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
}
複製程式碼

installModule

接下來看 installModule 的實現

// installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
  // 判斷是否為 rootModule
  const isRoot = !path.length
  // 獲取 namespace,root 沒有 namespace
  // 對於 modules: {a: moduleA} 來說
  // namespace = 'a/'
  const namespace = store._modules.getNamespace(path)

  // 為 namespace 快取 module
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // 設定 state
  if (!isRoot && !hot) {
    // 以下邏輯就是給 store.state 新增屬性
    // 根據模組新增
    // state: { xxx: 1, a: {...}, b: {...} }
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // 該方法其實是在重寫 dispatch 和 commit 函式
  // 你是否有疑問模組中的 dispatch 和 commit
  // 是如何找到對應模組中的函式的
  // 假如模組 A 中有一個名為 add 的 mutation
  // 通過 makeLocalContext 函式,會將 add 變成
  // a/add,這樣就可以找到模組 A 中對應函式了
  const local = module.context = makeLocalContext(store, namespace, path)
  
  // 以下幾個函式遍歷,都是在
  // 註冊模組中的 mutation、action 和 getter
  // 假如模組 A 中有名為 add 的 mutation 函式
  // 在註冊過程中會變成 a/add
  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)
  })

  // 這裡會生成一個 _wrappedGetters 屬性
  // 用於快取 getter,便於下次使用
  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)
  })
}
複製程式碼

resetStoreVM

接下來看 resetStoreVM 的實現,該屬性實現了狀態的響應式,並且將 _wrappedGetters 作為 computed 屬性。

// resetStoreVM(this, state)
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // 設定 getters 屬性
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 遍歷 _wrappedGetters 屬性
  forEachValue(wrappedGetters, (fn, key) => {
    // 給 computed 物件新增屬性
    computed[key] = () => fn(store)
    // 重寫 get 方法
    // store.getters.xx 其實是訪問了
    // store._vm[xx]
    // 也就是 computed 中的屬性
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 使用 Vue 來儲存 state 樹
  // 同時也讓 state 變成響應式
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 當訪問 store.state 時
  // 其實是訪問了 store._vm._data.$$state
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // 確保只能通過 commit 的方式改變狀態
  if (store.strict) {
    enableStrictMode(store)
  }
}
複製程式碼

常用 API

commit 解析

如果需要改變狀態的話,一般都會使用 commit 去操作,接下來讓我們來看看 commit 是如何實現狀態的改變的

commit(_type, _payload, _options) {
  // 檢查傳入的引數
  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
  }
  // _withCommit 函式將 _committing
  // 設定為 TRUE,保證在 strict 模式下
  // 只能 commit 改變狀態
  this._withCommit(() => {
    entry.forEach(function commitIterator(handler) {
      // entry.push(function wrappedMutationHandler(payload) {
      //   handler.call(store, local.state, payload)
      // })
      // handle 就是 wrappedMutationHandler 函式
      // wrappedMutationHandler 內部就是呼叫
      // 對於的 mutation 函式
      handler(payload)
    })
  })
  // 執行訂閱函式
  this._subscribers.forEach(sub => sub(mutation, this.state))
}
複製程式碼

dispatch 解析

如果需要非同步改變狀態,就需要通過 dispatch 的方式去實現。在 dispatch 呼叫的 commit 函式都是重寫過的,會找到模組內的 mutation 函式。

dispatch(_type, _payload) {
  // 檢查傳入的引數
  const { type, payload } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  // 找到對於的 action 函式
  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))

  // 在註冊 action 的時候,會將函式返回值
  // 處理成 promise,當 promise 全部
  // resolve 後,就會執行 Promise.all
  // 裡的函式
  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}
複製程式碼

各種語法糖

在元件中,如果想正常使用 Vuex 的功能,經常需要這樣呼叫 this.$store.state.xxx 的方式,引來了很多的不便。為此,Vuex 引入了語法糖的功能,讓我們可以通過簡單的方式來實現上述的功能。以下以 mapState 為例,其他的幾個 map 都是差不多的原理,就不一一解析了。

function normalizeNamespace(fn) {
  return (namespace, map) => {
    // 函式作用很簡單
    // 根據引數生成 namespace
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}
// 執行 mapState 就是執行
// normalizeNamespace 返回的函式
export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  // normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
  // normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
  // function normalizeMap(map) {
  //   return Array.isArray(map)
  //     ? map.map(key => ({ key, val: key }))
  //     : Object.keys(map).map(key => ({ key, val: map[key] }))
  // }
  // states 引數可以參入陣列或者物件型別
  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
      }
      // 返回 State
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
複製程式碼

最後

以上是 Vue 的原始碼解析,雖然 Vuex 的整體程式碼並不多,但是卻是個值得閱讀的專案。如果你在閱讀的過程中有什麼疑問或者發現了我的錯誤,歡迎在評論中討論。

如果你想學習到更多的前端知識、面試技巧或者一些我個人的感悟,可以關注我的公眾號一起學習

Vuex 原始碼深度解析

相關文章