Vuex 2.0 源码分析

做者:滴滴公共前端团队 - 黄轶javascript

你们好,我叫黄轶,来自滴滴公共前端团队,咱们团队最近写了一本书 ——《Vue.js 权威指南》,内容丰富,由浅入深。不过有一些同窗反馈说缺乏 Vuex 的介绍的章节。既然 Vue.js 2.0 已经正式发布了,咱们也要紧跟步伐,和你们聊一聊 Vuex 2.0。本文并不打算讲官网已有的内容,而会经过源码分析的方式,让同窗们从另一个角度认识和理解 Vuex 2.0。html

当咱们用 Vue.js 开发一个中到大型的单页应用时,常常会遇到以下问题:前端

  • 如何让多个 Vue 组件共享状态
  • Vue 组件间如何通信

一般,在项目不是很复杂的时候,咱们会利用全局事件总线 (global event bus)解决,可是随着复杂度的提高,这些代码将变的难以维护。所以,咱们须要一种更加好用的解决方案,因而,Vuex 诞生了。vue

本文并非 Vuex 的科普文章,对于还不了解 Vuex 的同窗,建议先移步 Vuex 官方文档;看英文文档吃力的同窗,能够看 Vuex 的中文文档java

vuex 原理图

Vuex 的设计思想受到了 Flux,Redux 和 The Elm Architecture 的启发,它的实现又十分巧妙,和 Vue.js 配合相得益彰,下面就让咱们一块儿来看它的实现吧。react

目录结构

Vuex 的源码托管在 github,咱们首先经过 git 把代码 clone 到本地,选一款适合本身的 IDE 打开源码,展开 src 目录,以下图所示:git

enter image description here

src 目录下的文件并很少,包含几个 js 文件和 plugins 目录, plugins 目录里面包含 2 个 Vuex 的内置插件,整个源码加起来不过 500-600 行,可谓很是轻巧的一个库。es6

麻雀虽小,五脏俱全,咱们先直观的感觉一下源码的结构,接下来看一下其中的实现细节。github

源码分析

本文的源码分析过程不会是自上而下的给代码加注释,我更倾向因而从 Vuex 提供的 API 和咱们的使用方法等维度去分析。Vuex 的源码是基于 es6 的语法编写的,对于不了解 es6 的同窗,建议仍是先学习一下 es6。vuex

从入口开始

看源码通常是从入口开始,Vuex 源码的入口是 src/index.js,先来打开这个文件。

咱们首先看这个库的 export ,在 index.js 代码最后。

export default {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions
}复制代码

这里能够一目了然地看到 Vuex 对外暴露的 API。其中, Store 是 Vuex 提供的状态存储类,一般咱们使用 Vuex 就是经过建立 Store 的实例,稍后咱们会详细介绍。接着是 install 方法,这个方法一般是咱们编写第三方 Vue 插件的“套路”,先来看一下“套路”代码:

function install (_Vue) {
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

// auto install in dist mode
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}复制代码

咱们实现了一个 install 方法,这个方法当咱们全局引用 Vue ,也就是 window 上有 Vue 对象的时候,会手动调用 install 方法,并传入 Vue 的引用;当 Vue 经过 npm 安装到项目中的时候,咱们在代码中引入第三方 Vue 插件一般会编写以下代码:

import Vue from 'vue'
import Vuex from 'vuex'
...
Vue.use(Vuex)复制代码

当咱们执行 Vue.use(Vuex) 这句代码的时候,实际上就是调用了 install 的方法并传入 Vue 的引用。install 方法顾名思义,如今让咱们来看看它的实现。它接受了一个参数 _Vue,函数体首先判断 Vue ,这个变量的定义在 index.js 文件的开头部分:

let Vue // bind on install复制代码

对 Vue 的判断主要是保证 install 方法只执行一次,这里把 install 方法的参数 _Vue 对象赋值给 Vue 变量,这样咱们就能够在 index.js 文件的其它地方使用 Vue 这个变量了。install 方法的最后调用了 applyMixin 方法,咱们顺便来看一下这个方法的实现,在 src/mixin.js 文件里定义:

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /** * Vuex init hook, injected into each instances init hooks list. */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}复制代码

这段代码的做用就是在 Vue 的生命周期中的初始化(1.0 版本是 init,2.0 版本是 beforeCreated)钩子前插入一段 Vuex 初始化代码。这里作的事情很简单——给 Vue 的实例注入一个 $store 的属性,这也就是为何咱们在 Vue 的组件中能够经过 this.$store.xxx 访问到 Vuex 的各类数据和状态。

认识 Store 构造函数

咱们在使用 Vuex 的时候,一般会实例化 Store 类,而后传入一个对象,包括咱们定义好的 actions、getters、mutations、state等,甚至当咱们有多个子模块的时候,咱们能够添加一个 modules 对象。那么实例化的时候,到底作了哪些事情呢?带着这个疑问,让咱们回到 index.js 文件,重点看一下 Store 类的定义。Store 类定义的代码略长,我不会一下就贴上全部代码,咱们来拆解分析它,首先看一下构造函数的实现:

class Store {
  constructor (options = {}) {
    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
    assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)

    const {
      state = {},
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._options = options
    this._committing = false
    this._actions = Object.create(null)
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._runtimeModules = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    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

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], options)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    resetStoreVM(this, state)

    // apply plugins
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }
  ...
}复制代码

构造函数的一开始就用了“断言函数”,来判断是否知足一些条件。

assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)复制代码

这行代码的目的是确保 Vue 的存在,也就是在咱们实例化 Store 以前,必需要保证以前的 install 方法已经执行了。

assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)复制代码

这行代码的目的是为了确保 Promsie 可使用的,由于 Vuex 的源码是依赖 Promise 的。Promise 是 es6 提供新的 API,因为如今的浏览器并非都支持 es6 语法的,因此一般咱们会用 babel 编译咱们的代码,若是想使用 Promise 这个 特性,咱们须要在 package.json 中添加对 babel-polyfill 的依赖并在代码的入口加上 import 'babel-polyfill' 这段代码。

再来看看 assert 这个函数,它并非浏览器原生支持的,它的实如今 src/util.js 里,代码以下:

export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}复制代码

很是简单,对 condition 判断,若是不不为真,则抛出异常。这个函数虽然简单,但这种编程方式值得咱们学习。

再来看构造函数接下来的代码:

const {
  state = {},
  plugins = [],
  strict = false
} = options复制代码

这里就是利用 es6 的结构赋值拿到 options 里的 state,plugins 和 strict。state 表示 rootState,plugins 表示应用的插件、strict 表示是否开启严格模式。

接着往下看:

// store internal state
this._options = options
this._committing = false
this._actions = Object.create(null)
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._runtimeModules = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()复制代码

这里主要是建立一些内部的属性:
this._options 存储参数 options。
this._committing 标志一个提交状态,做用是保证对 Vuex 中 state 的修改只能在 mutation 的回调函数中,而不能在外部随意修改 state。
this._actions 用来存储用户定义的全部的 actions。
this._mutations 用来存储用户定义全部的 mutatins。
this._wrappedGetters 用来存储用户定义的全部 getters 。
this._runtimeModules 用来存储全部的运行时的 modules。
this._subscribers 用来存储全部对 mutation 变化的订阅者。
this._watcherVM 是一个 Vue 对象的实例,主要是利用 Vue 实例方法 $watch 来观测变化的。

继续往下看:

// bind commit and dispatch to self
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复制代码

这里的代码也不难理解,把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上,dispatch 和 commit 的实现咱们稍后会分析。this.strict 表示是否开启严格模式,在严格模式下会观测全部的 state 的变化,建议在开发环境时开启严格模式,线上环境要关闭严格模式,不然会有必定的性能开销。

Vuex 的初始化核心

installModule

咱们接着往下看:

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], options)

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))复制代码

这段代码是 Vuex 的初始化的核心,其中,installModule 方法是把咱们经过 options 传入的各类属性模块注册和安装;resetStoreVM 方法是初始化 store._vm,观测 state 和 getters 的变化;最后是应用传入的插件。

下面,咱们先来看一下 installModule 的实现:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const {
    state,
    actions,
    mutations,
    getters,
    modules
  } = module

  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }

  if (mutations) {
    Object.keys(mutations).forEach(key => {
      registerMutation(store, key, mutations[key], path)
    })
  }

  if (actions) {
    Object.keys(actions).forEach(key => {
      registerAction(store, key, actions[key], path)
    })
  }

  if (getters) {
    wrapGetters(store, getters, path)
  }

  if (modules) {
    Object.keys(modules).forEach(key => {
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}复制代码

installModule 函数可接收5个参数,store、rootState、path、module、hot,store 表示当前 Store 实例,rootState 表示根 state,path 表示当前嵌套模块的路径数组,module 表示当前安装的模块,hot 当动态改变 modules 或者热更新的时候为 true。

先来看这部分代码:

const isRoot = !path.length
 const {
   state,
   actions,
   mutations,
   getters,
   modules
 } = module复制代码

代码首先经过 path 数组的长度判断是否为根。咱们在构造函数调用的时候是 installModule(this, state, [], options),因此这里 isRoot 为 true。module 为传入的 options,咱们拿到了 module 下的 state、actions、mutations、getters 以及嵌套的 modules。

接着看下面的代码:

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, state || {})
  })
}复制代码

这里判断当不为根且非热更新的状况,而后设置级联状态,这里乍一看很差理解,咱们先放一放,稍后来回顾。

再往下看代码:

if (mutations) {
  Object.keys(mutations).forEach(key => {
    registerMutation(store, key, mutations[key], path)
  })
}

if (actions) {
  Object.keys(actions).forEach(key => {
    registerAction(store, key, actions[key], path)
  })
}

if (getters) {
  wrapGetters(store, getters, path)
}复制代码

这里分别是对 mutations、actions、getters 进行注册,若是咱们实例化 Store 的时候经过 options 传入这些对象,那么会分别进行注册,我稍后再去介绍注册的具体实现。那么到这,若是 Vuex 没有 module ,这个 installModule 方法能够说已经作完了。可是 Vuex 巧妙了设计了 module 这个概念,由于 Vuex 自己是单一状态树,应用的全部状态都包含在一个大对象内,随着咱们应用规模的不断增加,这个 Store 变得很是臃肿。为了解决这个问题,Vuex 容许咱们把 store 分 module(模块)。每个模块包含各自的 state、mutations、actions 和 getters,甚至是嵌套模块。因此,接下来还有一行代码:

if (modules) {
  Object.keys(modules).forEach(key => {
    installModule(store, rootState, path.concat(key), modules[key], hot)
  })
}复制代码

这里经过遍历 modules,递归调用 installModule 去安装子模块。这里传入了 store、rootState、path.concat(key)、和 modules[key],和刚才不一样的是,path 不为空,module 对应为子模块,那么咱们回到刚才那段代码:

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, state || {})
  })
}复制代码

当递归初始化子模块的时候,isRoot 为 false,注意这里有个方法getNestedState(rootState, path),来看一下 getNestedState 函数的定义:

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}复制代码

这个方法很简单,就是根据 path 查找 state 上的嵌套 state。在这里就是传入 rootState 和 path,计算出当前模块的父模块的 state,因为模块的 path 是根据模块的名称 concat 链接的,因此 path 的最后一个元素就是当前模块的模块名,最后调用

store._withCommit(() => {
  Vue.set(parentState, moduleName, state || {})
})复制代码

把当前模块的 state 添加到 parentState 中。
这里注意一下咱们用了 store._withCommit 方法,来看一下这个方法的定义:

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}复制代码

因为咱们是在修改 state,Vuex 中全部对 state 的修改都会用 _withCommit函数包装,保证在同步修改 state 的过程当中 this._committing 的值始终为true。这样当咱们观测 state 的变化时,若是 this._committing 的值不为 true,则能检查到这个状态修改是有问题的。

看到这里,有些同窗可能会有点困惑,举个例子来直观感觉一下,以 Vuex 源码中的 example/shopping-cart 为例,打开 store/index.js,有这么一段代码:

export default new Vuex.Store({
  actions,
  getters,
  modules: {
    cart,
    products
  },
  strict: debug,
  plugins: debug ? [createLogger()] : []
})复制代码

这里有两个子 module,cart 和 products,咱们打开 store/modules/cart.js,看一下 cart 模块中的 state 定义,代码以下:

const state = {
  added: [],
  checkoutStatus: null
}复制代码

咱们运行这个项目,打开浏览器,利用 Vue 的调试工具来看一下 Vuex 中的状态,以下图所示:

enter image description here

能够看到,在 rootState 下,分别有 cart 和 products 2个属性,key 根据模块名称而来,value 就是在每一个模块文件中定义的 state,这就把模块 state 挂载到 rootState 上了。

咱们了解完嵌套模块 state 是怎么一回过后,咱们回过头来看一下 installModule 过程当中的其它 3 个重要方法:registerMutation、registerAction 和 wrapGetters。顾名思义,这 3 个方法分别处理 mutations、actions 和 getters。咱们先来看一下 registerMutation 的定义:

registerMutation

function registerMutation (store, type, handler, path = []) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler(getNestedState(store.state, path), payload)
  })
}复制代码

registerMutation 是对 store 的 mutation 的初始化,它接受 4 个参数,store为当前 Store 实例,type为 mutation 的 key,handler 为 mutation 执行的回调函数,path 为当前模块的路径。mutation 的做用就是同步修改当前模块的 state ,函数首先经过 type 拿到对应的 mutation 对象数组, 而后把一个 mutation 的包装函数 push 到这个数组中,这个函数接收一个参数 payload,这个就是咱们在定义 mutation 的时候接收的额外参数。这个函数执行的时候会调用 mutation 的回调函数,并经过 getNestedState(store.state, path) 方法获得当前模块的 state,和 playload 一块儿做为回调函数的参数。举个例子:

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}复制代码

这里咱们定义了一个 mutation,经过刚才的 registerMutation 方法,咱们注册了这个 mutation,这里的 state 对应的就是当前模块的 state,n 就是额外参数 payload,接下来咱们会从源码分析的角度来介绍这个 mutation 的回调是什么时候被调用的,参数是如何传递的。

咱们有必要知道 mutation 的回调函数的调用时机,在 Vuex 中,mutation 的调用是经过 store 实例的 API 接口 commit 来调用的,来看一下 commit 函数的定义:

commit (type, payload, options) {
  // check object-style commit
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  const mutation = { type, payload }
  const entry = this._mutations[type]
  if (!entry) {
    console.error(`[vuex] unknown mutation type: ${type}`)
    return
  }
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  if (!options || !options.silent) {
    this._subscribers.forEach(sub => sub(mutation, this.state))
  }
}复制代码

commit 支持 3 个参数,type 表示 mutation 的类型,payload 表示额外的参数,options 表示一些配置,好比 silent 等,稍后会用到。commit 函数首先对 type 的类型作了判断,处理了 type 为 object 的状况,接着根据 type 去查找对应的 mutation,若是找不到,则输出一条错误信息,不然遍历这个 type 对应的 mutation 对象数组,执行 handler(payload) 方法,这个方法就是以前定义的 wrappedMutationHandler(handler),执行它就至关于执行了 registerMutation 注册的回调函数,并把当前模块的 state 和 额外参数 payload 做为参数传入。注意这里咱们依然使用了 this._withCommit 的方法提交 mutation。commit 函数的最后,判断若是不是静默模式,则遍历 this._subscribers,调用回调函数,并把 mutation 和当前的根 state 做为参数传入。那么这个 this._subscribers 是什么呢?原来 Vuex 的 Store 实例提供了 subscribe API 接口,它的做用是订阅(注册监听) store 的 mutation。先来看一下它的实现:

subscribe (fn) {
  const subs = this._subscribers
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}复制代码

subscribe 方法很简单,他接受的参数是一个回调函数,会把这个回调函数保存到 this._subscribers 上,并返回一个函数,当咱们调用这个返回的函数,就能够解除当前函数对 store 的 mutation 的监听。其实,Vuex 的内置 logger 插件就是基于 subscribe 接口实现对 store 的 muation的监听,稍后咱们会详细介绍这个插件。

registerAction

在了解完 registerMutation,咱们再来看一下 registerAction 的定义:

function registerAction (store, type, handler, path = []) {
const entry = store._actions[type] || (store._actions[type] = [])
  const { dispatch, commit } = store
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler({
      dispatch,
      commit,
      getters: store.getters,
      state: getNestedState(store.state, path),
      rootState: store.state
    }, payload, cb)
    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
    }
  })
 }复制代码

registerAction 是对 store 的 action 的初始化,它和 registerMutation 的参数一致,和 mutation 不一样一点,mutation 是同步修改当前模块的 state,而 action 是能够异步去修改 state,这里不要误会,在 action 的回调中并不会直接修改 state ,仍然是经过提交一个 mutation 去修改 state(在 Vuex 中,mutation 是修改 state 的惟一途径)。那咱们就来看看 action 是如何作到这一点的。

函数首先也是经过 type 拿到对应 action 的对象数组,而后把一个 action 的包装函数 push 到这个数组中,这个函数接收 2 个参数,payload 表示额外参数 ,cb 表示回调函数(实际上咱们并无使用它)。这个函数执行的时候会调用 action 的回调函数,传入一个 context 对象,这个对象包括了 store 的 commit 和 dispatch 方法、getter、当前模块的 state 和 rootState 等等。接着对这个函数的返回值作判断,若是不是一个 Promise 对象,则调用 Promise.resolve(res) 给res 包装成了一个 Promise 对象。这里也就解释了为什么 Vuex 的源码依赖 Promise,这里对 Promise 的判断也和简单,参考代码 src/util.js,对 isPromise 的判断以下:

export function isPromise (val) {
  return val && typeof val.then === 'function'
}复制代码

其实就是简单的检查对象的 then 方法,若是包含说明就是一个 Promise 对象。

接着判断 store._devtoolHook,这个只有当用到 Vuex devtools 开启的时候,咱们才能捕获 promise 的过程当中的 。 action 的包装函数最后返回 res ,它就是一个地地道道的 Promise 对象。来看个例子:

actions: {
  checkout ({ commit, state }, payload) {
    // 把当前购物车的商品备份起来
    const savedCartItems = [...state.cart.added]
    // 发送结账请求,并愉快地清空购物车
    commit(types.CHECKOUT_REQUEST)
    // 购物 API 接收一个成功回调和一个失败回调
    shop.buyProducts(
      products,
      // 成功操做
      () => commit(types.CHECKOUT_SUCCESS),
      // 失败操做
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}复制代码

这里咱们定义了一个 action,经过刚才的 registerAction 方法,咱们注册了这个 action,这里的 commit 就是 store 的 API 接口,能够经过它在 action 里提交一个 mutation。state 对应的就是当前模块的 state,咱们在这个 action 里便可以同步提交 mutation,也能够异步提交。接下来咱们会从源码分析的角度来介绍这个 action 的回调是什么时候被调用的,参数是如何传递的。

咱们有必要知道 action 的回调函数的调用时机,在 Vuex 中,action 的调用是经过 store 实例的 API 接口 dispatch 来调用的,来看一下 dispatch 函数的定义:

dispatch (type, payload) {
  // check object-style dispatch
   if (isObject(type) && type.type) {
     payload = type
     type = type.type
   }
   const entry = this._actions[type]
   if (!entry) {
     console.error(`[vuex] unknown action type: ${type}`)
     return
   }
   return entry.length > 1
     ? Promise.all(entry.map(handler => handler(payload)))
     : entry[0](payload)
 }复制代码

dispatch 支持2个参数,type 表示 action 的类型,payload 表示额外的参数。前面几行代码和 commit 接口很是相似,都是找到对应 type 下的 action 对象数组,惟一和 commit 不一样的地方是最后部分,它对 action 的对象数组长度作判断,若是长度为 1 则直接调用 entry[0](payload), 这个方法就是以前定义的 wrappedActionHandler(payload, cb),执行它就至关于执行了 registerAction 注册的回调函数,并把当前模块的 context 和 额外参数 payload 做为参数传入。因此咱们在 action 的回调函数里,能够拿到当前模块的上下文包括 store 的 commit 和 dispatch 方法、getter、当前模块的 state 和 rootState,可见 action 是很是灵活的。

wrapGetters

了解完 registerAction 后,咱们来看看 wrapGetters的定义:

function wrapGetters (store, moduleGetters, modulePath) {
  Object.keys(moduleGetters).forEach(getterKey => {
    const rawGetter = moduleGetters[getterKey]
    if (store._wrappedGetters[getterKey]) {
      console.error(`[vuex] duplicate getter key: ${getterKey}`)
      return
    }
    store._wrappedGetters[getterKey] = function wrappedGetter (store) {
      return rawGetter(
        getNestedState(store.state, modulePath), // local state
        store.getters, // getters
        store.state // root state
      )
    }
  })
}复制代码

wrapGetters 是对 store 的 getters 初始化,它接受 3个 参数, store 表示当前 Store 实例,moduleGetters 表示当前模块下的全部 getters, modulePath 对应模块的路径。细心的同窗会发现,和刚才的 registerMutation 以及 registerAction 不一样,这里对 getters 的循环遍历是放在了函数体内,而且 getters 和它们的一个区别是不容许 getter 的 key 有重复。

这个函数作的事情就是遍历 moduleGetters,把每个 getter 包装成一个方法,添加到 store._wrappedGetters 对象中,注意 getter 的 key 是不容许重复的。在这个包装的方法里,会执行 getter 的回调函数,并把当前模块的 state,store 的 getters 和 store 的 rootState 做为它参数。来看一个例子:

export const cartProducts = state => {
  return state.cart.added.map(({ id, quantity }) => {
    const product = state.products.all.find(p => p.id === id)
    return {
      title: product.title,
      price: product.price,
      quantity
    }
  })
}复制代码

这里咱们定义了一个 getter,经过刚才的 wrapGetters 方法,咱们把这个 getter 添加到 store._wrappedGetters 对象里,这和回调函数的参数 state 对应的就是当前模块的 state,接下来咱们从源码的角度分析这个函数是如何被调用,参数是如何传递的。

咱们有必要知道 getter 的回调函数的调用时机,在 Vuex 中,咱们知道当咱们在组件中经过 this.$store.getters.xxxgetters 能够访问到对应的 getter 的回调函数,那么咱们须要把对应 getter 的包装函数的执行结果绑定到 `this.$store 上。这部分的逻辑就在 resetStoreVM 函数里。咱们在 Store 的构造函数中,在执行完 installModule 方法后,就会执行 resetStoreVM 方法。来看一下它的定义:

resetStoreVM

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

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })

  // 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._vm = new Vue({
    data: { state },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation.
    store._withCommit(() => {
      oldVm.state = null
    })
    Vue.nextTick(() => oldVm.$destroy())
  }
}复制代码

这个方法主要是重置一个私有的 _vm 对象,它是一个 Vue 的实例。这个 _vm 对象会保留咱们的 state 树,以及用计算属性的方式存储了 store 的 getters。来具体看看它的实现过程。咱们把这个函数拆成几个部分来分析:

const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })复制代码

这部分留了现有的 store._vm 对象,接着遍历 store._wrappedGetters 对象,在遍历过程当中,依次拿到每一个 getter 的包装函数,并把这个包装函数执行的结果用 computed 临时变量保存。接着用 es5 的 Object.defineProperty 方法为 store.getters 定义了 get 方法,也就是当咱们在组件中调用this.$store.getters.xxxgetters 这个方法的时候,会访问 store._vm[xxxgetters]。咱们接着往下看:

// 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._vm = new Vue({
   data: { state },
   computed
 })
 Vue.config.silent = silent

 // enable strict mode for new vm
 if (store.strict) {
   enableStrictMode(store)
 }复制代码

这部分的代码首先先拿全局 Vue.config.silent 的配置,而后临时把这个配置设成 true,接着实例化一个 Vue 的实例,把 store 的状态树 state 做为 data 传入,把咱们刚才的临时变量 computed 做为计算属性传入。而后再把以前的 silent 配置重置。设置 silent 为 true 的目的是为了取消这个 _vm 的全部日志和警告。把 computed 对象做为 _vm 的 computed 属性,这样就完成了 getters 的注册。由于当咱们在组件中访问 this.$store.getters.xxxgetters 的时候,就至关于访问 store._vm[xxxgetters],也就是在访问 computed[xxxgetters],这样就访问到了 xxxgetters 对应的回调函数了。这段代码最后判断 strict 属性决定是否开启严格模式,咱们来看看严格模式都干了什么:

function enableStrictMode (store) {
  store._vm.$watch('state', () => {
    assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
  }, { deep: true, sync: true })
}复制代码

严格模式作的事情很简单,监测 store._vm.state 的变化,看看 state 的变化是否经过执行 mutation 的回调函数改变,若是是外部直接修改 state,那么 store._committing 的值为 false,这样就抛出一条错误。再次强调一下,Vuex 中对 state 的修改只能在 mutation 的回调函数里。

回到 resetStoreVM 函数,咱们来看一下最后一部分:

if (oldVm) {
  // dispatch changes in all subscribed watchers
  // to force getter re-evaluation.
  store._withCommit(() => {
    oldVm.state = null
  })
  Vue.nextTick(() => oldVm.$destroy())
}复制代码

这里的逻辑很简单,因为这个函数每次都会建立新的 Vue 实例并赋值到 store._vm 上,那么旧的 _vm 对象的状态设置为 null,并调用 $destroy 方法销毁这个旧的 _vm 对象。

那么到这里,Vuex 的初始化基本告一段落了,初始化核心就是 installModule 和
resetStoreVM 函数。经过对 mutations 、actions 和 getters 的注册,咱们了解到 state 的是按模块划分的,按模块的嵌套造成一颗状态树。而 actions、mutations 和 getters 的全局的,其中 actions 和 mutations 的 key 容许重复,但 getters 的 key 是不容许重复的。官方推荐咱们给这些全局的对象在定义的时候加一个名称空间来避免命名冲突。
从源码的角度介绍完 Vuex 的初始化的玩法,咱们再从 Vuex 提供的 API 方向来分析其中的源码,看看这些 API 是如何实现的。

Vuex API 分析

Vuex 常见的 API 如 dispatch、commit 、subscribe 咱们前面已经介绍过了,这里就再也不赘述了,下面介绍的一些 Store 的 API,虽然不经常使用,可是了解一下也不错。

watch(getter, cb, options)

watch 做用是响应式的监测一个 getter 方法的返回值,当值改变时调用回调。getter 接收 store 的 state 做为惟一参数。来看一下它的实现:

watch (getter, cb, options) {
    assert(typeof getter === 'function', `store.watch only accepts a function.`)
    return this._watcherVM.$watch(() => getter(this.state), cb, options)
  }复制代码

函数首先断言 watch 的 getter 必须是一个方法,接着利用了内部一个 Vue 的实例对象 `this._watcherVM 的 $watch 方法,观测 getter 方法返回值的变化,若是有变化则调用 cb 函数,回调函数的参数为新值和旧值。watch 方法返回的是一个方法,调用它则取消观测。

registerModule(path, module)

registerModule 的做用是注册一个动态模块,有的时候当咱们异步加载一些业务的时候,能够经过这个 API 接口去动态注册模块,来看一下它的实现:

registerModule (path, module) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    this._runtimeModules[path.join('.')] = module
    installModule(this, this.state, path, module)
    // reset store to update getters...
    resetStoreVM(this, this.state)
  }复制代码

函数首先对 path 判断,若是 path 是一个 string 则把 path 转换成一个 Array。接着把 module 对象缓存到 this._runtimeModules 这个对象里,path 用点链接做为该对象的 key。接着和初始化 Store 的逻辑同样,调用 installModule 和 resetStoreVm 方法安装一遍动态注入的 module。

unregisterModule(path)

和 registerModule 方法相对的就是 unregisterModule 方法,它的做用是注销一个动态模块,来看一下它的实现:

unregisterModule (path) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    delete this._runtimeModules[path.join('.')]
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      Vue.delete(parentState, path[path.length - 1])
    })
    resetStore(this)
  }复制代码

函数首先仍是对 path 的类型作了判断,这部分逻辑和注册是同样的。接着从 this._runtimeModules 里删掉以 path 点链接的 key 对应的模块。接着经过 this._withCommit 方法把当前模块的 state 对象从父 state 上删除。最后调用 resetStore(this) 方法,来看一下这个方法的定义:

function resetStore (store) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  const state = store.state
  // init root module
  installModule(store, state, [], store._options, true)
  // init all runtime modules
  Object.keys(store._runtimeModules).forEach(key => {
    installModule(store, state, key.split('.'), store._runtimeModules[key], true)
  })
  // reset vm
  resetStoreVM(store, state)
}复制代码

这个方法做用就是重置 store 对象,重置 store 的 _actions、_mutations、_wrappedGetters 等等属性。而后再次调用 installModules 去从新安装一遍 Module 对应的这些属性,注意这里咱们的最后一个参数 hot 为true,表示它是一次热更新。这样在 installModule 这个方法体类,以下这段逻辑就不会执行

function installModule (store, rootState, path, module, hot) {
  ... 
  // set state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }
  ...
}复制代码

因为 hot 始终为 true,这里咱们就不会从新对状态树作设置,咱们的 state 保持不变。由于咱们已经明确的删除了对应 path 下的 state 了,要作的事情只不过就是从新注册一遍 muations、actions 以及 getters。

回调 resetStore 方法,接下来遍历 this._runtimeModules 模块,从新安装全部剩余的 runtime Moudles。最后仍是调用 resetStoreVM 方法去重置 Store 的 _vm 对象。

hotUpdate(newOptions)

hotUpdate 的做用是热加载新的 action 和 mutation。 来看一下它的实现:

hotUpdate (newOptions) {
  updateModule(this._options, newOptions)
  resetStore(this)
}复制代码

函数首先调用 updateModule 方法去更新状态,其中当前 Store 的 opition 配置和要更新的 newOptions 会做为参数。来看一下这个函数的实现:

function updateModule (targetModule, newModule) {
  if (newModule.actions) {
    targetModule.actions = newModule.actions
  }
  if (newModule.mutations) {
    targetModule.mutations = newModule.mutations
  }
  if (newModule.getters) {
    targetModule.getters = newModule.getters
  }
  if (newModule.modules) {
    for (const key in newModule.modules) {
      if (!(targetModule.modules && targetModule.modules[key])) {
        console.warn(
          `[vuex] trying to add a new module '${key}' on hot reloading, ` +
          'manual reload is needed'
        )
        return
      }
      updateModule(targetModule.modules[key], newModule.modules[key])
    }
  }
}复制代码

首先咱们对 newOptions 对象的 actions、mutations 以及 getters 作了判断,若是有这些属性的话则替换 targetModule(当前 Store 的 options)对应的属性。最后判断若是 newOptions 包含 modules 这个 key,则遍历这个 modules 对象,若是 modules 对应的 key 不在以前的 modules 中,则报一条警告,由于这是添加一个新的 module ,须要手动从新加载。若是 key 在以前的 modules,则递归调用 updateModule,热更新子模块。

调用完 updateModule 后,回到 hotUpdate 函数,接着调用 resetStore 方法从新设置 store,刚刚咱们已经介绍过了。

replaceState

replaceState的做用是替换整个 rootState,通常在用于调试,来看一下它的实现:

replaceState (state) {
    this._withCommit(() => {
      this._vm.state = state
    })
  }复制代码

函数很是简单,就是调用 this._withCommit 方法修改 Store 的 rootState,之因此提供这个 API 是因为在咱们是不能在 muations 的回调函数外部去改变 state。

到此为止,API 部分介绍完了,其实整个 Vuex 源码下的 src/index.js 文件里的代码基本都过了一遍。

辅助函数

Vuex 除了提供咱们 Store 对象外,还对外提供了一系列的辅助函数,方便咱们在代码中使用 Vuex,提供了操做 store 的各类属性的一系列语法糖,下面咱们来一块儿看一下:

mapState

mapState 工具函数会将 store 中的 state 映射到局部计算属性中。为了更好理解它的实现,先来看一下它的使用示例:

// vuex 提供了独立的构建工具函数 Vuex.mapState
import { mapState } from 'vuex'
export default {
  // ...
  computed: mapState({
    // 箭头函数可让代码很是简洁
    count: state => state.count,
    // 传入字符串 'count' 等同于 `state => state.count`
    countAlias: 'count',
    // 想访问局部状态,就必须借助于一个普通函数,函数中使用 `this` 获取局部状态
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}复制代码

当计算属性名称和状态子树名称对应相同时,咱们能够向 mapState 工具函数传入一个字符串数组。

computed: mapState([
  // 映射 this.count 到 this.$store.state.count
  'count'
])复制代码

经过例子咱们能够直观的看到,mapState 函数能够接受一个对象,也能够接收一个数组,那它底层到底干了什么事呢,咱们一块儿来看一下源码这个函数的定义:

export function mapState (states) {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      return typeof val === 'function'
        ? val.call(this, this.$store.state, this.$store.getters)
        : this.$store.state[val]
    }
  })
  return res
}复制代码

函数首先对传入的参数调用 normalizeMap 方法,咱们来看一下这个函数的定义:

function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}复制代码

这个方法判断参数 map 是否为数组,若是是数组,则调用数组的 map 方法,把数组的每一个元素转换成一个 {key, val: key}的对象;不然传入的 map 就是一个对象(从 mapState 的使用场景来看,传入的参数不是数组就是对象),咱们调用 Object.keys 方法遍历这个 map 对象的 key,把数组的每一个 key 都转换成一个 {key, val: key}的对象。最后咱们把这个对象数组做为 normalizeMap 的返回值。

回到 mapState 函数,在调用了 normalizeMap 函数后,把传入的 states 转换成由 {key, val} 对象构成的数组,接着调用 forEach 方法遍历这个数组,构造一个新的对象,这个新对象每一个元素都返回一个新的函数 mappedState,函数对 val 的类型判断,若是 val 是一个函数,则直接调用这个 val 函数,把当前 store 上的 state 和 getters 做为参数,返回值做为 mappedState 的返回值;不然直接把 this.$store.state[val] 做为 mappedState 的返回值。

那么为什么 mapState 函数的返回值是这样一个对象呢,由于 mapState 的做用是把全局的 state 和 getters 映射到当前组件的 computed 计算属性中,咱们知道在 Vue 中 每一个计算属性都是一个函数。

为了更加直观地说明,回到刚才的例子:

import { mapState } from 'vuex'
export default {
  // ...
  computed: mapState({
    // 箭头函数可让代码很是简洁
    count: state => state.count,
    // 传入字符串 'count' 等同于 `state => state.count`
    countAlias: 'count',
    // 想访问局部状态,就必须借助于一个普通函数,函数中使用 `this` 获取局部状态
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}复制代码

通过 mapState 函数调用后的结果,以下所示:

import { mapState } from 'vuex'
export default {
  // ...
  computed: {
    count() {
      return this.$store.state.count
    },
    countAlias() {
      return this.$store.state['count']
    },
    countPlusLocalState() {
      return this.$store.state.count + this.localCount
    }
  }
}复制代码

咱们再看一下 mapState 参数为数组的例子:

computed: mapState([
  // 映射 this.count 到 this.$store.state.count
  'count'
])复制代码

通过 mapState 函数调用后的结果,以下所示:

computed: {
  count() {
    return this.$store.state['count']
  }
}复制代码

mapGetters

mapGetters 工具函数会将 store 中的 getter 映射到局部计算属性中。它的功能和 mapState 很是相似,咱们来直接看它的实现:

export function mapGetters (getters) {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    res[key] = function mappedGetter () {
      if (!(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
      }
      return this.$store.getters[val]
    }
  })
  return res
}复制代码

mapGetters 的实现也和 mapState 很相似,不一样的是它的 val 不能是函数,只能是一个字符串,并且会检查 val in this.$store.getters 的值,若是为 false 会输出一条错误日志。为了更直观地理解,咱们来看一个简单的例子:

import { mapGetters } from 'vuex'
export default {
  // ...
  computed: {
    // 使用对象扩展操做符把 getter 混入到 computed 中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}复制代码

通过 mapGetters 函数调用后的结果,以下所示:

import { mapGetters } from 'vuex'
export default {
  // ...
  computed: {
    doneTodosCount() {
      return this.$store.getters['doneTodosCount']
    },
    anotherGetter() {
      return this.$store.getters['anotherGetter']
    }
  }
}复制代码

再看一个参数 mapGetters 参数是对象的例子:

computed: mapGetters({
  // 映射 this.doneCount 到 store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})复制代码

通过 mapGetters 函数调用后的结果,以下所示:

computed: {
  doneCount() {
    return this.$store.getters['doneTodosCount']
  }
}复制代码

mapActions

mapActions 工具函数会将 store 中的 dispatch 方法映射到组件的 methods 中。和 mapState、mapGetters 也相似,只不过它映射的地方不是计算属性,而是组件的 methods 对象上。咱们来直接看它的实现:

export function mapActions (actions) {
  const res = {}
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      return this.$store.dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
}复制代码

能够看到,函数的实现套路和 mapState、mapGetters 差很少,甚至更简单一些, 实际上就是作了一层函数包装。为了更直观地理解,咱们来看一个简单的例子:

import { mapActions } from 'vuex'
export default {
  // ...
  methods: {
    ...mapActions([
      'increment' // 映射 this.increment() 到 this.$store.dispatch('increment')
    ]),
    ...mapActions({
      add: 'increment' // 映射 this.add() to this.$store.dispatch('increment')
    })
  }
}复制代码

通过 mapActions 函数调用后的结果,以下所示:

import { mapActions } from 'vuex'
export default {
  // ...
  methods: {
    increment(...args) {
      return this.$store.dispatch.apply(this.$store, ['increment'].concat(args))
    }
    add(...args) {
      return this.$store.dispatch.apply(this.$store, ['increment'].concat(args))
    }
  }
}复制代码

mapMutations

mapMutations 工具函数会将 store 中的 commit 方法映射到组件的 methods 中。和 mapActions 的功能几乎同样,咱们来直接看它的实现:

export function mapMutations (mutations) {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      return this.$store.commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
}复制代码

函数的实现几乎也和 mapActions 同样,惟一差异就是映射的是 store 的 commit 方法。为了更直观地理解,咱们来看一个简单的例子:

import { mapMutations } from 'vuex'
export default {
  // ...
  methods: {
    ...mapMutations([
      'increment' // 映射 this.increment() 到 this.$store.commit('increment')
    ]),
    ...mapMutations({
      add: 'increment' // 映射 this.add() 到 this.$store.commit('increment')
    })
  }
}复制代码

通过 mapMutations 函数调用后的结果,以下所示:

import { mapActions } from 'vuex'
export default {
  // ...
  methods: {
    increment(...args) {
      return this.$store.commit.apply(this.$store, ['increment'].concat(args))
    }
    add(...args) {
      return this.$store.commit.apply(this.$store, ['increment'].concat(args))
    }
  }
}复制代码

插件

Vuex 的 store 接收 plugins 选项,一个 Vuex 的插件就是一个简单的方法,接收 store 做为惟一参数。插件做用一般是用来监听每次 mutation 的变化,来作一些事情。

在 store 的构造函数的最后,咱们经过以下代码调用插件:

import devtoolPlugin from './plugins/devtool'

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))复制代码

咱们一般实例化 store 的时候,还会调用 logger 插件,代码以下:

import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

export default new Vuex.Store({
  ...
  plugins: debug ? [createLogger()] : []
})复制代码

在上述 2 个例子中,咱们分别调用了 devtoolPlugin 和 createLogger() 2 个插件,它们是 Vuex 内置插件,咱们接下来分别看一下他们的实现。

devtoolPlugin

devtoolPlugin 主要功能是利用 Vue 的开发者工具和 Vuex 作配合,经过开发者工具的面板展现 Vuex 的状态。它的源码在 src/plugins/devtool.js 中,来看一下这个插件到底作了哪些事情。

const devtoolHook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook

  devtoolHook.emit('vuex:init', store)

  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}复制代码

咱们直接从对外暴露的 devtoolPlugin 函数看起,函数首先判断了devtoolHook 的值,若是咱们浏览器装了 Vue 开发者工具,那么在 window 上就会有一个 __VUE_DEVTOOLS_GLOBAL_HOOK__ 的引用, 那么这个 devtoolHook 就指向这个引用。

接下来经过 devtoolHook.emit('vuex:init', store) 派发一个 Vuex 初始化的事件,这样开发者工具就能拿到当前这个 store 实例。

接下来经过 devtoolHook.on('vuex:travel-to-state', targetState => { store.replaceState(targetState) })监听 Vuex 的 traval-to-state 的事件,把当前的状态树替换成目标状态树,这个功能也是利用 Vue 开发者工具替换 Vuex 的状态。

最后经过 store.subscribe((mutation, state) => { devtoolHook.emit('vuex:mutation', mutation, state) }) 方法订阅 store 的 state 的变化,当 store 的 mutation 提交了 state 的变化, 会触发回调函数——经过 devtoolHook 派发一个 Vuex mutation 的事件,mutation 和 rootState 做为参数,这样开发者工具就能够观测到 Vuex state 的实时变化,在面板上展现最新的状态树。

loggerPlugin

一般在开发环境中,咱们但愿实时把 mutation 的动做以及 store 的 state 的变化实时输出,那么咱们能够用 loggerPlugin 帮咱们作这个事情。它的源码在 src/plugins/logger.js 中,来看一下这个插件到底作了哪些事情。

// Credits: borrowed code from fcomb/redux-logger

import { deepCopy } from '../util'

export default function createLogger ({ collapsed = true, transformer = state => state, mutationTransformer = mut => mut } = {}) {
  return store => {
    let prevState = deepCopy(store.state)

    store.subscribe((mutation, state) => {
      if (typeof console === 'undefined') {
        return
      }
      const nextState = deepCopy(state)
      const time = new Date()
      const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
      const formattedMutation = mutationTransformer(mutation)
      const message = `mutation ${mutation.type}${formattedTime}`
      const startMessage = collapsed
        ? console.groupCollapsed
        : console.group

      // render
      try {
        startMessage.call(console, message)
      } catch (e) {
        console.log(message)
      }

      console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
      console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
      console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))

      try {
        console.groupEnd()
      } catch (e) {
        console.log('—— log end ——')
      }

      prevState = nextState
    })
  }
}

function repeat (str, times) {
  return (new Array(times + 1)).join(str)
}

function pad (num, maxLength) {
  return repeat('0', maxLength - num.toString().length) + num
}复制代码

插件对外暴露的是 createLogger 方法,它实际上接受 3 个参数,它们都有默认值,一般咱们用默认值就能够。createLogger 的返回的是一个函数,当我执行 logger 插件的时候,实际上执行的是这个函数,下面来看一下这个函数作了哪些事情。

函数首先执行了 let prevState = deepCopy(store.state) 深拷贝当前 store 的 rootState。这里为何要深拷贝,由于若是是单纯的引用,那么 store.state 的任何变化都会影响这个引用,这样就没法记录上一个状态了。咱们来了解一下 deepCopy 的实现,在 src/util.js 里定义:

function find (list, f) {
  return list.filter(f)[0]
}

export function deepCopy (obj, cache = []) {
  // just return if obj is immutable value
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // if obj is hit, it is in circular structure
  const hit = find(cache, c => c.original === obj)
  if (hit) {
    return hit.copy
  }

  const copy = Array.isArray(obj) ? [] : {}
  // put the copy into cache at first
  // because we want to refer it in recursive deepCopy
  cache.push({
    original: obj,
    copy
  })

  Object.keys(obj).forEach(key => {
    copy[key] = deepCopy(obj[key], cache)
  })

  return copy
}复制代码

deepCopy 并不陌生,不少开源库如 loadash、jQuery 都有相似的实现,原理也不难理解,主要是构造一个新的对象,遍历原对象或者数组,递归调用 deepCopy。不过这里的实现有一个有意思的地方,在每次执行 deepCopy 的时候,会用 cache 数组缓存当前嵌套的对象,以及执行 deepCopy 返回的 copy。若是在 deepCopy 的过程当中经过 find(cache, c => c.original === obj) 发现有循环引用的时候,直接返回 cache 中对应的 copy,这样就避免了无限循环的状况。

回到 loggerPlugin 函数,经过 deepCopy 拷贝了当前 state 的副本并用 prevState 变量保存,接下来调用 store.subscribe 方法订阅 store 的 state 的变。 在回调函数中,也是先经过 deepCopy 方法拿到当前的 state 的副本,并用 nextState 变量保存。接下来获取当前格式化时间已经格式化的 mutation 变化的字符串,而后利用 console.group 以及 console.log 分组输出 prevState、mutation以及 nextState,这里能够经过咱们 createLogger 的参数 collapsed、transformer 以及 mutationTransformer 来控制咱们最终 log 的显示效果。在函数的最后,咱们把 nextState 赋值给 prevState,便于下一次 mutation。

总结

Vuex 2.0 的源码分析到这就告一段落了,最后我再分享一下看源码的当心得:对于一个库或者框架源码的研究前,首先了解他们的使用场景、官网文档等;而后必定要用他,至少也要写几个小 demo,达到熟练掌握的程度;最后再从入口、API、使用方法等等多个维度去了解他内部的实现细节。若是这个库过于庞大,那就先按模块和功能拆分,一点点地消化。

最后还有一个问题,有些同窗会问,源码那么枯燥,咱们分析学习它的有什么好处呢?首先,学习源码有助于咱们更深刻掌握和应用这个库或者框架;其次,咱们还能够学习到源码中不少编程技巧,能够迁移到咱们平时的开发工做中;最后,对于一些高级开发工程师而言,咱们能够学习到它的设计思想,对未来有一天咱们也去设计一个库或者框架是很是有帮助的,这也是提高自身能力水平的很是好的途径。


欢迎关注DDFE
GITHUB:github.com/DDFE
微信公众号:微信搜索公众号“DDFE”或扫描下面的二维码

相关文章
相关标签/搜索