阅读 383

vuex 源代码简析

说明

  • 以下内容均是以 Vuex 2.0.0 版本展开分析。
  • 此篇文章,是自己对 Vuex 实现分析的记录性文章,如有任何错误,欢迎指正交流。

Vuex 示意图

在说之前先来看一张官方文档提供的一张图

vuex

  1. 首先组件渲染依赖了部分全局的 State 内部预先定义的属性。
  2. 组件内部需要更新之前依赖的 State 内部属性,则需要调度(Dispatch)触发 Action 响应状态变化(也可直接提交 mutation)。
  3. 响应状态变化函数内部必须提交 mutation 去更改状态(类似发事件)。
  4. State 内部属性的更改,触发依赖其属性的组件重新渲染。

基础示例剖析

了解了大致流程,接下来我们就以基础示例入手,剖析其实现了哪些功能,根据其实现的功能逐步去捋清其代码实现

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

// 注册插件
Vue.use(Vuex)

// 根状态对象。每个Vuex实例只是一个状态树。
const state = { count: 0 }

// mutations 实际上是改变状态的操作。每个 mutation 处理程序都将整个状态树作为第一个参数,然后是附加的有效负载参数。
// mutations 必须是同步的,并且可以通过插件记录下来,以便调试。
const mutations = {
  increment (state) {
    state.count++
  },
  decrement (state) {
    state.count--
  }
}

// actions 是导致副作用并可能涉及异步操作的函数。
const actions = {
  increment: ({ commit }) => commit('increment'),
  decrement: ({ commit }) => commit('decrement'),
  incrementIfOdd ({ commit, state }) {
    if ((state.count + 1) % 2 === 0) {
      commit('increment')
    }
  },
  incrementAsync ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('increment')
        resolve()
      }, 1000)
    })
  }
}

// getters are functions
const getters = {
  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
}

// 模块
const moduleDemo = {
  state: { moduleCount: 1 },
  mutations: {
    // state: 模块的局部状态对象。
    moduleIncrement(state) {
      state.moduleCount++
    },
  },
  actions: {
    moduleIncrement: ({ commit }) => commit('moduleIncrement'),
  },
  getters: {
    moduleCountPlus: ({ moduleCount }) => moduleCount++
  }
}

// Vuex实例是通过组合 state、 mutations 、actions 和 getter 创建的。
const store = new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
  modules: {
    moduleDemo
  },
})

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

复制代码
  • 根据上述基础使用,我们大概可以梳理出以下几点:
  • 注册插件。
  • 定义 store 所需配置参数。
  • 实例化 Store 类.
  • 实例化 Vue 并传入 store。

在看具体的代码实现之前,我们大致的先了解一下整个 Vuex 入口文件内的大致内容:

import devtoolPlugin from './plugins/devtool'
import applyMixin from './mixin'
import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
import { isObject, isPromise, assert } from './util'

let Vue // 绑定安装

// Store 全局单例模式管理
class Store { ... }

// 更新模块
function updateModule(targetModule, newModule) { ... }

// 重置 Store
function resetStore(store) { ... }

// 重置 Store 上 Vue 实例
function resetStoreVM(store, state) { ... }

// 安装模块
function installModule(store, rootState, path, module, hot) { ... }

// 注册 mutations 构造器选项
function registerMutation(store, type, handler, path = []) { ... }

// 注册 action
function registerAction(store, type, handler, path = []) { ... }

// 包装 getters
function wrapGetters(store, moduleGetters, modulePath) { ... }

// 启用严格模式
function enableStrictMode(store) {}

// 获取嵌套的状态
function getNestedState(state, path) {}

// 插件注册方法
function install(_Vue) {}

// 自动注册插件
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

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

在了解了其内部构造,我们就根据上述梳理,逐点分析其实现。

注册插件

我们知道,Vue 的插件都需要给 Vue 提供一个注册钩子函数 installl, 执行 Vue.use(Vuex) 实际内部走的是 install 函数的内部调用。

install


// 插件注册:vue 内部在调用会把 Vue 透传过来
function install(_Vue) {
  // 避免重复注册
  if (Vue) {
    console.error(
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  // 绑定安装
  Vue = _Vue
  // 应用全局 Vue.mixins
  applyMixin(Vue)
}

// 自动注册机制
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

复制代码

applyMixin

export default function (Vue) {
  // 获取 Vue 版本号
  const version = Number(Vue.version.split('.')[0])

  // 版本号为 2.x
  if (version >= 2) {
    // 若存在 init 钩子则把 VuexInit 混入 初始化阶段
    // 其它混入 beforeCreate 阶段
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // 覆盖 init 并为 1.x 注入 vuex init 过程。 向后兼容性。
    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钩子,注入到每个实例init钩子列表中。
   */
  function vuexInit() {
    // 获取实例配置参数
    const options = this.$options
    // 注入 store 实例
    if (options.store) {
      this.$store = options.store
      // 若不存在,则去寻找父级 store 实例
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

复制代码

注:_lifecycleHooks

  • Vue 内部配置项,引用其生命周期相关钩子函数的函数名,由于遗留原因而暴露的配置项。

    • Vue 2.0.0 ⬆️ 其引用为:
      [
        'beforeCreate',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated', // 激活
        'deactivated', // 停用
        'errorCaptured' // 捕获错误
      ]
    复制代码
    • Vue v2.0.0-alpha.6 ⬇️ 其引用为:
     /**
      * List of lifecycle hooks.
      */
      _lifecycleHooks: [
        'init',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated',
        'deactivated'
      ]
    复制代码

若对此有兴趣,可以研究一下 Vue.js 版本的更迭。

根据上述分析我们知道,在注册插件时,根据Vue的不同版本选择合适的混入时机,使得创建的每个 Vue 实例在 “初始化阶段” 做些预处理(在实例添加$store 属性,其值为 Store 实例)。那么接下来我们就具体来看看 Store 内部做了些什么?

Store

/**
 * Store
 *
 * @class Store 全局单例模式管理
 */
class Store {
  constructor(options = {}) {
    assert(Vue, `在创建商店实例之前必须调用 Vue.use(Vuex)`)
    assert(typeof Promise !== 'undefined', `vuex 需要一个 promise polyfill 在这个浏览器。`)

    const {
      state = {}, // Object | Function Vuex store 实例的根 state 对象
      plugins = [], // Array<Function> 一个数组,包含应用在 store 上的插件方法。这些插件直接接收 store 作为唯一参数,可以监听 mutation
      strict = false // 严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
    } = options // Vuex.Store 构造器选项

    // store 内部状态
    this._options = options
    // 是否正在提交
    this._committing = false
    // 存储着 所有 actions
    this._actions = Object.create(null)
    // 存储着 所有 mutations
    this._mutations = Object.create(null)
    // 存储着 所有 Getters
    this._wrappedGetters = Object.create(null)
    this._runtimeModules = Object.create(null)
    // 订阅函数池
    this._subscribers = []
    // 存储着 Vue 实例
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    const store = this
    const { dispatch, commit } = this
    // 实例方法 - 分发 action 返回一个解析所有被触发的 action 处理器的 Promise。
    this.dispatch = function boundDispatch(type, payload) {
      return dispatch.call(store, type, payload)
    }
    // 实例方法 - 提交 mutation
    this.commit = function boundCommit(type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // 严格模式
    this.strict = strict

    // init root 模块。这还递归地注册所有子模块,并在 this._wrappedgechers 中收集所有模块 getter
    installModule(this, state, [], options)

    // 初始化负责反应性的存储vm(也将_wrappedgechers注册为计算属性)
    resetStoreVM(this, state)

    // 注入应用插件
    plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))
  }

  get state() {
    return this._vm.state
  }

  set state(v) {
    assert(false, `使用 store.replacestate() 显式替换存储状态。`)
  }

  /**
   * 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
   * 如:store.commit('increment')
   * 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
   * 这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数
   *
   * @param {String} type
   * @param {Object} payload
   * @param {Object} options
   * @memberof Store
   */
  commit(type, payload, options) { ... }

  //  分发 action
  dispatch(type, payload) { ... }

  subscribe(fn) { ... }

  watch(getter, cb, options) { ... }

  // 替换 State
  replaceState(state) { ... }

  // 注册模块
  registerModule(path, module) { ... }

  // 注销模块
  unregisterModule(path) { ... }

  hotUpdate(newOptions) { ... }

  // 提交 mutation。
  _withCommit(fn) {
    // 存储当前提交状态
    const committing = this._committing
    // 置为提交状态
    this._committing = true
    // 调用更改状态函数
    fn()
    // 把提交状态置回原来的状态
    this._committing = committing
  }
}

复制代码

简单分析梳理:

  • constructor:

    • 首先在构造函数内部定义了一些属性(这里注释比较清楚)
    • 执行了一些初始化 installModuleresetStoreVM 等方法,下面将着重看一下其内部实现。
  • state 定义了取值函数(getter)和存值函数(setter),做一层代理(防止意外的修改)。

  • 定义了一些实例方法 commitdispatch 等(之后根据实际调用,具体分析)。

具体实现:

installModule - 安装模块

init root 模块。这还递归地注册所有子模块,并在 this._wrappedgechers 中收集所有模块 getter

/**
 * 安装模块
 *
 * @param {Object} store Store 实例
 * @param {Object | Function} rootState Vuex store 实例的根 state 对象
 * @param {Array} path
 * @param {Object} module Vuex.Store 构造器选项
 * @param {*} hot
 */
function installModule(store, rootState, path, module, hot) {
  const isRoot = !path.length // 是否是根还是模块
  // 从 Vuex.Store 构造器选项解构出相关选项
  const {
    state, // Vuex store 实例的根 state 对象。
    actions, // 在 store 上注册 action。处理函数总是接受 context 作为第一个参数,payload 作为第二个参数(可选)。
    mutations, // 在 store 上注册 mutation,处理函数总是接受 state 作为第一个参数(如果定义在模块中,则为模块的局部状态),payload 作为第二个参数(可选)。
    getters, // 在 store 上注册 getter,getter 方法接受以下参数: state, // 如果在模块中定义则为模块的局部状态. getters, // 等同于 store.getters.
    modules  // 包含了子模块的对象,会被合并到 store
  } = module

  // 设置 module 的 state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    // 为根 State 添加模块的 state
    store._withCommit(() => {
      Vue.set(parentState, moduleName, state || {})
    })
  }

  // 若存在 mutations 构造器选项 则将其全部选项注册
  if (mutations) {
    Object.keys(mutations).forEach(key => {
      registerMutation(store, key, mutations[key], path)
    })
  }

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

  // 包装 getters
  if (getters) {
    wrapGetters(store, getters, path)
  }

  // 安装模块
  if (modules) {
    Object.keys(modules).forEach(key => {
      // 递归调用注册每一个模块
      installModule(store, rootState, path.concat(key), modules[key], hot)
    })
  }
}
复制代码

由上述初始化调用 installModule(this, state, [], options) 可知其入参,下面就看看各个选项注册的代码实现。

MutationActiongetter 注册

/**
 * 注册 mutations 构造器选项
 *
 * @param {*} store
 * @param {*} type
 * @param {*} handler
 * @param {*} [path=[]]
 */
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)
  })
}

/**
 * 注册 action
 *
 * @param {Object} store
 * @param {String} type
 * @param {Function} handler
 * @param {Array} [path=[]]
 */
function registerAction(store, type, handler, path = []) {
  const entry = store._actions[type] || (store._actions[type] = [])
  const { dispatch, commit } = store
  entry.push(function wrappedActionHandler(payload, cb) {
    // 注意这里透传的context: 不是 store 实例本身。
      dispatch,
      commit,
      getters: store.getters,
      state: getNestedState(store.state, path),
      rootState: store.state
    }, payload, cb)
    // 判断是否是 promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    // 处理应用的 devtools 插件
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

/**
 * 包装 getters
 *
 * @param {Object} store
 * @param {Object} moduleGetters
 * @param {Array modulePath}
 */
function wrapGetters(store, moduleGetters, modulePath) {
  Object.keys(moduleGetters).forEach(getterKey => {
    const rawGetter = moduleGetters[getterKey]
    if (store._wrappedGetters[getterKey]) {
      console.error(`[vuex] 重复的getter关键: ${getterKey}`)
      return
    }
    store._wrappedGetters[getterKey] = function wrappedGetter(store) {
      return rawGetter(
        getNestedState(store.state, modulePath), // local state
        store.getters, // getters
        store.state // root state
      )
    }
  })
}

/**
 * 获取嵌套的 state
 *
 * @param {Object} state
 * @param {Array} path
 * @returns
 */
function getNestedState(state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

复制代码
  • 上述代码实现逻辑比较清晰,就是把注册信息添加到 _mutations_actions_wrappedGetters 统一管理。

  • 若存在模块则会将其state添加到 root state 中。

  • 上述基础示例,最终安装结果如下:

    
      _mutations: {
        decrement: [
          ƒ wrappedMutationHandler(payload)
        ],
        increment: [
          ƒ wrappedMutationHandler(payload)
        ],
        moduleIncrement: [
          ƒ wrappedMutationHandler(payload)
        ]
      }
    
      _actions: {
        decrement: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        increment: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        incrementAsync: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        incrementIfOdd: [
          ƒ wrappedActionHandler(payload, cb)
        ],
        moduleIncrement: [
          ƒ wrappedActionHandler(payload, cb)
        ]
      }
    
      _wrappedGetters: {
        evenOrOdd: ƒ wrappedGetter(store),
        moduleCountPlus: ƒ wrappedGetter(store)
      }
    
      // root state
      state: {
        count: 0,
        moduleDemo: {
          moduleCount: 1
        }
      }
    
    复制代码

resetStoreVM - 重置 Store 上 Vue 实例


/**
 * 重置 Store 上 Vue 实例
 *
 * @param {*} store
 * @param {*} state
 */
function resetStoreVM(store, state) {
  // 取之前的 vue 实例
  const oldVm = store._vm

  // 绑定存储公共 getter
  store.getters = {}
  // 获取 Store 中所有 getter
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 代理取值函数
  Object.keys(wrappedGetters).forEach(key => {
    const fn = wrappedGetters[key]
    // 利用 computed 的延迟缓存机制
    computed[key] = () => fn(store)
    // 在公共 getter 上定义之前合并的 getter,并做一层取值代理,实际上取得是计算属性定义的 key 值。
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    })
  })

  // 使用 Vue 实例存储状态树抑制警告,以防用户添加了一些 funky global mixins
  const silent = Vue.config.silent
  // 关闭 Vue 内部的警告
  Vue.config.silent = true
  // 添加 _vm 属性,值为 Vue 实例
  store._vm = new Vue({
    data: { state },
    computed
  })
  // 开启 Vue 内部的警告
  Vue.config.silent = silent

  // 启用严格模式 for new vm
  // 严格模式下在非提交的情况下修改 state,抛出错误。
  if (store.strict) {
    enableStrictMode(store)
  }

  // 若存在之前的Vue实例
  if (oldVm) {
    // 在所有订阅的观察者中分派更改,以强制 getter 重新评估。
    store._withCommit(() => {
      oldVm.state = null
    })
    // 在下个更新队列之后销毁之前的 Vue 实例
    Vue.nextTick(() => oldVm.$destroy())
  }
}

/**
 * 启用严格模式
 *
 * @param {Object} store
 * @returns {void}
 * 注:使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
 */
function enableStrictMode(store) {
  store._vm.$watch('state', () => {
    assert(store._committing, `不要在 mutation 处理程序之外对 vuex 存储状态进行改变;更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。`)
  }, { deep: true, sync: true })
}

复制代码

在讲解具体用例前,先来看看 dispatchcommit 的代码实现:

dispatchcommit - 调度和提交

/**
   * 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
   * 如:store.commit('increment')
   * 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
   * 这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数
   *
   * @param {*} type
   * @param {*} payload
   * @param {*} options
   * @memberof Store
   */
  commit(type, payload, options) {
    // 检查对象样式提交 如:
    // store.commit({ type: 'increment',  amount: 10 })
    if (isObject(type) && type.type) {
      options = payload
      payload = type // 使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数
      type = type.type
    }
    const mutation = { type, payload }
    const entry = this._mutations[type] // 查找 mutation
    // 若不存在则抛出错误
    if (!entry) {
      console.error(`[vuex] 未知 mutation 类型: ${type}`)
      return
    }
    // 提交 mutation
    this._withCommit(() => {
      entry.forEach(function commitIterator(handler) {
        handler(payload)
      })
    })
    // 若满足该条件,则:调用订阅池内所有的订阅函数
    if (!options || !options.silent) {
      this._subscribers.forEach(sub => sub(mutation, this.state))
    }
  }

  /**
   * 分发 action
   *
   * @param {*} type
   * @param {*} payload
   * @returns {Promise} 解析所有被触发的 action 处理器的 Promise
   * @memberof Store
   */
  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] 未知 action 类型: ${type}`)
      return
    }
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }
复制代码
  • 讲到这里整个流程就已经分析的差不多了。

  • 这里顺便提一下:Mutation 必须是同步函数

    • 若是异步这会使 devtool 中的 mutation 日志变得不可追踪。【参阅】
  • 以下用例演示了从 dispatch(调度) action其内部触发 commit(提交) 进而调用 mutation 状态修改函数, 来达到更新状态。相当清晰👍

    <template>
      <div id="app">
        <div class="root">
          Clicked: {{ $store.state.count }} times, count is {{ $store.getters.evenOrOdd }}.
          <button @click="$store.dispatch('increment')">+</button>
        </div>
        <div class="module">
          Clicked: {{ $store.state.moduleDemo.moduleCount }} times
          <button @click="$store.dispatch('moduleIncrement')">+</button>
        </div>
      </div>
    </template>
    复制代码

    点击 “+” 调度actions内部对应的处理函数,其内部去提交状态改变(类似分发事件)在 mutations 内部去执行响应的函数,真正改变状态。状态的改变,导致依赖这些状态的组件更新。“Clicked: 1”

    const state = {
      count: 0
    }
    
    const actions = {
      increment: ({ commit }) => commit('increment'),
      ...
    }
    
    const mutations = {
      increment (state) {
        state.count++
      },
      ...
    }
    
    const moduleDemo = {
      state: { moduleCount: 1 },
      mutations: {
        moduleIncrement(state) {
          state.moduleCount++
        },
      },
      actions: {
        moduleIncrement: ({ commit }) => commit('moduleIncrement'),
      },
      ...
    }
    
    复制代码

其它细节

接下来我们就来看看我们在组件中经常使用的辅助函数实现如:

import {
  mapActions,
  mapActions,
  mapMutations,
  mapGetters
} from "vuex";

export default {
  computed: {
    ...mapState([
      'count' // 映射 this.count 为 this.$store.state.count
    ]), // 或 ...mapState({ count: state => state.count })

    ...mapGetters(["evenOrOdd"]),
  },
  methods: {
    // 必须同步提交
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
      // `mapMutations` 也支持载荷:
      'decrement' // 将 `this.decrement(amount)` 映射为 `this.$store.commit('decrement', amount)`
    ]),
    // 处理异步
    ...mapActions([
      "increment",
      "decrement",
      "incrementIfOdd",
      "incrementAsync"
    ]),
  }
};

复制代码

mapState

/**
 * state 映射处理函数
 *
 * @export
 * @param {Array | Object} states
 * @returns {Object}
 */
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
}

/**
 * 规范参数类型
 *
 * @param {*} map
 * @returns {Array}
 */
function normalizeMap(map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
复制代码
  • 我们能看出来,这些辅助函数,主要是做了一层映射,”解决重复和冗余,让你少按几次键“。
  • 这里只是提取了一个进行讲解,其它思路差不多,这里就不多说了。
  • 最后,本文着重点放在贯通流程,及常用实现,里面还有许多细节没有提及。
  • 若有兴趣请参阅 vuex 。建议将其代码拉下来,根据其用例,本地跑起来,断点去调式,结合文档慢慢去看,相信一定收获巨多!

参阅

关注下面的标签,发现更多相似文章
评论