New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vuex 源码分析 #58

Open
dwqs opened this Issue Sep 3, 2017 · 2 comments

Comments

Projects
None yet
3 participants
@dwqs
Owner

dwqs commented Sep 3, 2017

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

vuex

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

写本文时, vuex 的版本是 2.4.0

目录结构

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

vuex dir

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

  • module: 提供module对象与module对象树的创建功能
  • plugins: 提供开发的辅助插件
  • helpers.js: 提供 mapGetters, mapActions 等 API
  • index.js/index.esm.js: 源码主入口文件
  • mixin.js: 在 Vue 实例上注入 store
  • util.js: 提供 vuex 开发的一系列工具方法, 如 forEachValue/assert

入口

在 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 引用, 该变量有如下作用:

  • 插件不必将 Vue.js 作为一个依赖打包
  • 作为避免重复安装的 vuex 的条件判断
  • Store 中调用 vue 全局 API 的提供者
  • 创建 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 的源码之后, 造了一些相关的轮子, 欢迎参考和使用:

  • Revuejs: 基于 Vue.js 2 的一个轻量状态管理方案
  • Lue: 基于 Vue.js 2 和 Vuex 2 的库

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

@PuShaoWei

This comment has been minimized.

Show comment
Hide comment
@PuShaoWei

PuShaoWei Sep 14, 2017

感谢分享

PuShaoWei commented Sep 14, 2017

感谢分享

@SkyLin0909

This comment has been minimized.

Show comment
Hide comment
@SkyLin0909

SkyLin0909 Oct 9, 2017

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

SkyLin0909 commented Oct 9, 2017

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment