dwqs / blog

:dog: :clap: :star2: Welcome to star
MIT License
3.78k stars 442 forks source link

Vuex 源码分析 #58

Open dwqs opened 7 years ago

dwqs commented 7 years ago

之前大致分析过了 vue-cli 源码vue-router 源码, 这两个工具也是 vue 生态中比较重要的组件. 而最近因为业务上的需要, 接触到了 vuex 的一些组件源码. vuex 集中于MVC模式中的Model层, 规定所有的数据操作必须通过 action - mutation - state change 的流程来进行, 再结合 vue 的数据视图双向绑定特性来实现页面的展示更新:

vuex

现在就具体来分析一下其流程.

写本文时, vuex 的版本是 2.4.0

目录结构

打开 Vuex 项目, 先了解下其目录结构:

vuex dir

Vuex提供了非常强大的状态管理功能, 源码代码量却不多, 目录结构划分也很清晰. 先大体介绍下各个目录文件的功能:

入口

在 2.4.0 中, vuex 提供了 UMD 和 ESM(ES module) 两个构建入口, 分别对应 src/index.jssrc/index.esm.js 文件. 在入口文件中, 主要是导出 vuex 提供给 Vue 应用的 API:

// src/index.js

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

在 Vue 应用中, vuex 是作为 vue 的一个插件为 Vue 应用提供强大的状态管理功能. 因而, 在能够使用 vuex 的功能之前, 先要在 Vue 应用的入口文件中安装 vuex:

// ...

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

// install vuex plugin
Vue.use(Vuex)

// ...

Vue 在安装插件时, 会去调用插件提供的 install 方法:

// src/store.js

import applyMixin from './mixin'
// ...

let Vue;

export class Store {
    // ...
    constructor (options = {}) {
        // ...
        // 浏览器环境下的自动安装:to fix #731
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
          install(window.Vue)
        }

        // ...
    }
}

// ...

export function install (_Vue) {
  if (Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // Vue 变量赋值 
  Vue = _Vue
  applyMixin(Vue)
}

src/store.js 文件中, 先声明了一个局部变量 Vue 来保存Vue 引用, 该变量有如下作用:

install 方法中, 调用了 applyMixin 方法:

// src/mixins.js

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

  if (version >= 2) {
    // 2.x 通过 hook 的方式注入
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 兼容 1.x
    // 使用自定义的 _init 方法并替换 Vue 对象原型的_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 hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store 注入
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 子组件从其父组件引用 $store 属性
      this.$store = options.parent.$store
    }
  }
}

applyMixin 方法的主要功能将初始化 Vue 实例时传入的 store 设置到 this 对象的 $store 属性上, 子组件则从其父组件引用$store 属性, 层层嵌套进行设置. 这样, 任何一个组件都能通过 this.$store 的方式访问 store 对象了.

store对象构造

store 对象构造的源码定义在 src/store.js 中, 梳理源码之前, 先大致了解下其构造流程:

store 构造流程

环境判断

store 的构造函数中, vuex 先对构造 store 需要的一些环境变量进行断言:

import { forEachValue, isObject, isPromise, assert } from './util'

//...

let Vue; 

// ...
if (process.env.NODE_ENV !== 'production') {
      // 根据变量 Vue 的值判断是否已经安装过 vuex
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      // 当前环境是否支持 Promise
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      // 是否是通过 new 操作符来创建 store 对象的
      assert(this instanceof Store, `Store must be called with the new operator.`)
 }
// ... 

assert 函数的定义是在 src/util 中:


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

初始化变量

在环境变量判断之后, 在构造函数中会定义一些变量, 这些变量一部分来自 options, 一部分是内部定义:

import ModuleCollection from './module/module-collection'

//...

let Vue; 

// ...

// 从 options 中获取 plugins/strict/state 等变量
const {
  plugins = [],
  strict = false
} = options

let {
  state = {}
} = options
if (typeof state === 'function') {
  state = state()
}

/**
 * store 内部变量
 */
// 是否在进行提交状态标识
this._committing = false   
// 用户定义的 actions
this._actions = Object.create(null)  
// 用户定义的 mutations
this._mutations = Object.create(null)
// 用户定义的 getters
this._wrappedGetters = Object.create(null)
// 收集用户定义的 modules
this._modules = new ModuleCollection(options)
// 模块命名空间map
this._modulesNamespaceMap = Object.create(null)
// 存储所有对 mutation 变化的订阅者
this._subscribers = []
// 创建一个 Vue 实例, 利用 $watch 监测 store 数据的变化
this._watcherVM = new Vue()

// ...

收集 modules 时, 传入调用 Store 构造函数传入的 options 对象, ModuleCollection 类的定义在 src/modules/module-collection.js 中:

import Module from './module'
import { assert, forEachValue } from '../util'

export default class ModuleCollection {
    constructor (rawRootModule) {
    // 注册根module
    this.register([], rawRootModule, false)
  }

  // ...

  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
        // 对 module 进行断言, 判断 module 是否符合要求
        // module 的 getters/actions/ mutations 等字段是可遍历的对象
        // 且 key 的值类型是函数
       assertRawModule(path, rawModule)
    }

    // 创建 module 对象
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 递归创建子 module 对象
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  // ...
}

ModuleCollection 主要将传入的 options 对象整个构造为一个 module 对象, 并循环调用 register 为其中的 modules 属性进行模块注册, 使其都成为 module 对象, 最后 options 对象被构造成一个完整的组件树. 详细源码可以查看 module-collection.js.

Module 类的定义在 src/modules/module.js:

import { forEachValue } from '../util'

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    // 当前 module
    this._rawModule = rawModule
    // 当前 module 的 state
    const rawState = rawModule.state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  // ...

  // 执行 installModule 时会用到的一些实例方法
  forEachChild (fn) {
    forEachValue(this._children, fn)
  }

  forEachGetter (fn) {
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }

  forEachAction (fn) {
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }

  forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
}

详细源码可以查看 module.js.

接着往下看:


// 绑定 this 到 store
const store = this
const { dispatch, commit } = this

// 确保 dispatch/commit 方法中的 this 对象正确指向 store
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)
}

// ...

上述代码主要是把 Store 类的 dispatchcommit 的方法的 this 指针指向当前 store 的实例上. 这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this.

dispatch 的功能是触发并传递一些参数(payload)给与 type 对应的 action, 其具体实现如下:

// ...

dispatch (_type, _payload) {
    // 获取 type 和 payload 参数
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    // 根据 type 获取所有对应的处理过的 action 函数集合
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    // 执行 action 函数
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
}

//...  

unifyObjectStyle 的实现

commit 会将 action type 提交给对应的 mutation, 然后执行对应 mutation 函数修改 module 的状态, 其实现如下:

// ...

commit (_type, _payload, _options) {
    // 解析参数
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    // 根据 type 获取所有对应的处理过的 mutation 函数集合
    const mutation = { type, payload }
    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)
      })
    })

    // 执行所有的订阅者函数
    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 的实现

梳理完 dispatchcommit, 接着看后面的代码:

import devtoolPlugin from './plugins/devtool'
// ...

// 是否开启严格模式(true/false)
this.strict = strict

// 安装 modules
installModule(this, state, [], this._modules.root)

// 初始化 store._vm, 观测 state 和 getters 的变化
resetStoreVM(this, state)

// 安装插件
plugins.forEach(plugin => plugin(this))

if (Vue.config.devtools) {
  devtoolPlugin(this)
}

// ...

后续的代码主要是安装 modules、vm 组件设置和安装通过 options 传入的插件以及根据 Vue 全局的 devtools 设置, 是否启用 devtoolPlugin 插件. 接下来就先分析下 vm 组件部分, 之后再分析安装 modules 的部分.

vm 组件设置

resetStoreVM 的定义如下:

function resetStoreVM (store, state, hot) {
  // 旧的 vm 实例
  const oldVm = store._vm

  // 定义 getters 属性
  store.getters = {}
  // 获取处理的 getters 函数集合
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  // 循环所有处理过的getters, 
  // 并新建 computed 对象进行存储 getter 函数执行的结果, 
  // 然后通过Object.defineProperty方法为 getters 对象建立属性
  // 使得我们通过 this.$store.getters.xxxgetter 能够访问到 store._vm[xxxgetters]
 forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
  })

  // 临时保存全局 Vue.config.silent 的配置
  const silent = Vue.config.silent
  // 将全局的silent设置为 true, 取消这个 _vm 的所有日志和警告
  // in case the user has added some funky global mixins
  Vue.config.silent = true

  // 设置新的 vm, 传入 state
  // 把 computed 对象作为 _vm 的 computed 属性, 这样就完成了 getters 的注册
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })

  // 还原 silent 设置
  Vue.config.silent = silent

  // enable strict mode for new vm
  if (store.strict) {
    // 严格模式下, 在mutation之外的地方修改 state 会报错
    enableStrictMode(store)
  }

  // 销毁旧的 vm 实例
  if (oldVm) {
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

enableStrictMode 的实现

module 安装

安装 modules 是 vuex 初始化的核心. ModuleCollection 方法把通过 options 传入的 modules 属性对其进行 Module 处理后, installModule 方法则会将处理过的 modules 进行注册和安装, 其定义如下:

// ...

installModule(this, state, [], this._modules.root)

//...

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = 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, module.state)
    })
  }

  // 设置上下文环境
  const local = module.context = makeLocalContext(store, namespace, path)

  // 注册 mutations
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 注册 actions 
  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  // 注册 getters 
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 递归安装子 module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

// ...

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

registerMutation

该方法是获取 store 中的对应 mutation type 的处理函数集合:

function registerMutation (store, type, handler, local) {
  // 获取 type(module.mutations 的 key) 对应的 mutations, 没有就创建一个空数组
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // push 处理过的 mutation handler
  entry.push(function wrappedMutationHandler (payload) {
    // 调用用户定义的 hanler, 并传入 state 和 payload 参数
    handler.call(store, local.state, payload)
  })
}

registerAction

该方法是对 storeaction 的初始化:

function registerAction (store, type, handler, local) {
  // 获取 type(module.actions 的 key) 对应的 actions, 没有就创建一个空数组
  const entry = store._actions[type] || (store._actions[type] = [])
  // push 处理过的 action handler
  // 在组件中调用 action 则是调用 wrappedActionHandler 
  entry.push(function wrappedActionHandler (payload, cb) {
    //  调用用户定义的 hanler, 并传入context对象、payload 参数和回调函数 cb
    let res = handler.call(store, {
      dispatch: local.dispatch,  
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)

    if (!isPromise(res)) {
      // 将 res 包装为一个 promise
      res = Promise.resolve(res)
    }
    // 当 devtools 开启的时候, 能捕获 promise 的报错
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      // 返回处理结果
      return res
    }
  })
}

在调用用户定义的 action handler 时, 给改 handler 传入了三个参数: context 对象, payload 和一个回调函数(很少会用到). context 对象包括了 storecommitdispatch 方法、当前模块的 getters/staterootState/rootGetters 等属性, 这也是我们能在 action 中获取到 commit/dispatch 方法的原因.

registerGetter

该方法是对 storegetters 的初始化:

function registerGetter (store, type, rawGetter, local) {
  // 根据 type(module.getters 的 key) 判断 getter 是否存在
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 包装 getter
  // 在组件中调用 getter 则是调用 wrappedGetter 
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // 调用用户定义的 getter 函数
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

子 module 安装

注册完了根组件的 actionsmutations 以及 getters 后, 递归调用自身, 为子组件注册其stateactionsmutations 以及 getters 等.

module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})

辅助函数

Vuex 除了提供我们 store 对象外, 还对外提供了一系列以 mapXXX 命名的辅助函数, 提供了操作 store 的各种属性的一系列语法糖. 辅助函数的定义均在 src/helpers.js 中, 由于 mapXXX 等函数的实现大同小异, 本文则只挑选常用的 mapActionsmapGetters 进行简单分析.

在分析之前, 先看两个函数的实现: normalizeNamespacenormalizeMap.

//normalizeNamespace
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      // 如果传给 mapXXX 的第一个参数不是一个字符串
      // 则将 namespace 赋值给 map 参数并将 namespace 设置为空
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

normalizeNamespace 函数的主要功能返回一个新的函数, 在新的函数中规范化 namespace 参数, 并调用函数参数fn.

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

normalizeMap 函数的作用则是将传递给 mapXXX 的参数统一转化为对象返回, 例如:

// normalize actions
normalizeMap(['test', 'test1']) ==> {test: 'test', val: 'test'}

// normalize getters
normalizeMap({
    'test': 'getTestValue',
    'test2': 'getTestValue2',
}) ==> {test: 'test', val: 'getTestValue'}

mapGetters

该函数会将 store 中的 getter 映射到局部计算属性中:

export const mapGetters = normalizeNamespace((namespace, getters) => {
  // 返回结果
  const res = {}

  // 遍历规范化参数后的对象
  // getters 就是传递给 mapGetters 的 map 对象或者数组
  normalizeMap(getters).forEach(({ key, val }) => {
    val = namespace + val
    res[key] = function mappedGetter () {
      // 一般不会传入 namespace 参数
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      // 如果 getter 不存在则报错
      if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      // 返回 getter 值, store.getters 可见上文 resetStoreVM 的分析
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

mapActions

该方法会将 store 中的 dispatch 方法映射到组件的 methods 中:

export const mapActions = normalizeNamespace((namespace, actions) => {
  // 返回结果
  const res = {}

  // 遍历规范化参数后的对象
  // actions 就是传递给 mapActions 的 map 对象或者数组
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // 保存 store dispatch 引用
      let dispatch = this.$store.dispatch
      if (namespace) {
         // 根据 namespace 获取 module
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        // 绑定 module 上下文的 dispatch
        dispatch = module.context.dispatch
      }

      // 调用 action 函数
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

总结

Vuex@2.4 的源码分析就暂时到这了. Vuex 的确是一个很强大的状态管理工具, 并且非常灵活, 但有一个问题就是, 如果严格按照单向数据流的方式进行开发, 并参考官方文档给予的项目目录结构, 随着应用的复杂度越来越高, 开发者会写非常多的模板类代码. 这个问题同样出现在 Redux, 因而在 Redux 社区出现了诸如 dvamirror 这样的解决方案来减少模板类代码的开发, 提高开发效率; 同时, React 社区也出现了更轻巧的状态管理工具, 如statty. 而在 Vuex 社区, 貌似还没有出现的类似的解决方案(如果你知道, 还请 issue 留链接), 因而个人在阅读过 Vuex 的源码之后, 造了一些相关的轮子, 欢迎参考和使用:

当然, 也可以根据项目需要, 采用其它的状态管理方案, 例如 mobx.

m9rco commented 7 years ago

感谢分享

evan-lin9 commented 7 years ago

确实dva将redux封装的更佳简化,通过reducer和effects实现非常强大的功能

webjohnjiang commented 5 years ago

这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this

你好,这里通过 this.$store来调用dispatch时,this肯定是指向 store的吧?

sichenguo commented 5 years ago

图片挂了