Skip to content
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源码解析 #9

Open
luoway opened this issue Jul 26, 2019 · 0 comments
Open

Vuex源码解析 #9

luoway opened this issue Jul 26, 2019 · 0 comments

Comments

@luoway
Copy link
Owner

luoway commented Jul 26, 2019

Vuex 源码解析

Vuex是什么?

Vuex使用方式

  1. 安装Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
  1. 创建store
const store = new Vuex.Store({
  //store 内容
})
  1. 注入store
const app = new Vue({
  store,
  //vue 实例配置
})

接下来按照使用方式的步骤进行分析。

Vuex.install——安装Vuex

src/index.js

import { Store, install } from './store'
//import ...
export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

根据Vue.use()用法说明,Vue.use(Vuex)执行时会调用Vuex.install(),内容如下:

src/store.js

import applyMixin from './mixin'
//...
let Vue // bind on install
//...
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    //省略开发环境提示
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

再看applyMixin(Vue)

src/mixin.js

export default function (Vue) {
  //省略Vue 1.x、2.x版本区分
  Vue.mixin({ beforeCreate: vuexInit })

  /**
   * Vuex初始化钩子,注入到每个实例的初始化钩子列表。
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

执行Vue.use(Vuex)目的就是在每个Vue实例初始化时,往beforeCreate生命周期中加入vueInit函数。

在“注入store”这一步:

const app = new Vue({
  store,
  //vue 实例配置
})

执行Vue实例的beforeCreate生命周期,vueInit函数被执行,运行时this指向当前vue实例上下文,因此this.$options指向当前vue组件配置对象

配置对象 是指,new Vue(options)时传入的options,实例内可以通过this.$options访问到

传入的store可以被options.store访问到,此时执行

this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store

options.store内容赋值给this.$store,即完成了store的注入。

options.store不存在时,即没有给当前组件配置store时,vueInit会尝试访问父组件已经注入完成的$store,若存在则赋值给当前组件this.$store,实现组件树内共享store

至此,Vuex的安装、注入看完了,接下来具体看创建Store。

Vuex.Store(options)——创建Store

实例化过程

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

let Vue // bind on install

export class Store {
  constructor (options = {}) {
    // 如果没有安装,则自动安装
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }
    // 省略开发中的检测警告
    const {
      plugins = [],
      strict = false
    } = options
    
    // store内部状态
    //...
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    //...
    
    // 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
    
    const state = this._modules.root.state

    // 初始化根模块。
    // 递归地注册所有子模块
    // 并收集所有模块的getters到this._wrappedGetters
    installModule(this, state, [], this._modules.root)

    // 初始化 store vm, 负责实现响应变化
    // (也注册_wrappedGetters为computed属性)
    resetStoreVM(this, state)

    //...
  }
  
  get state () {
    return this._vm._data.$$state
  }
  set state (v) {
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }
  
  dispatch(){}
  commit(){}
  //...
}

上面省略过后的实例化过程中,主要做了五件事:

  • 自动安装Vuex。即CDN引入vue.js时(window.Vue存在),可以省略手动调用Vue.use(Vuex)
  • 初始化Store内部状态
  • 绑定commitdispatch指向当前Store实例
  • installModule
  • resetStoreVM

关于第三点“绑定commit、dispatch指向当前Store实例”,可以这么理解:

const store = new Vuex.Store({
  state: {
    val: 0
  },
  mutations:{
    addVal(state){
      state.val += 1
    }
  }
})//store值为一个Store实例,内容为{state, dispath(), commit()}
const ct = store.commit
ct('addVal')

若未绑定commit方法的this指向,此时变量ct值为未绑定this指向的commit函数,当执行ct('addVal')时,this会指向当前(全局)作用域上下文windowct函数内部使用this就不会按预期地指向store。因此需要对commitdispatch进行绑定。

resetStoreVM

Vuex.Store的基础是state,在Store类中定义为:

  get state () {
    return this._vm._data.$$state
  }
  set state (v) {
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

定义的是一个取值器和一个赋值器。state是一个伪属性,没有真实值,当尝试直接修改state时,

this.$store.state = newState

修改不会生效,而是执行赋值器方法,提示使用store.replaceState()方法。

当读取state时,读取的实际上是this._vm._data.$$statethis._vmresetStoreVM()中赋值,下面具体看resetStoreVM源码

//...
import { forEachValue, isObject, isPromise, assert, partial } from './util'
//...
function resetStoreVM (store, state, hot) {
  //...
  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 使用一个Vue实例来存储状态树
  // 抑制警告,以防用户添加了某些全局mixins
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  //...
}

resetStoreVM函数的关键部分是

store._vm = new Vue({
  data: {
    $$state: state
  },
  computed
})

其中stateresetStoreVM的入参,computed对应store._wrappedGetters,并在store.getters中定义取值器,获取_vm的计算结果。

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

这就是专用的原因:数据响应更新是借由Vue实现的。

需要指出一处特殊的地方:这里的配置对象中data是一个对象,而非通常的函数

这仅仅是因为不需要对Vue实例进行复用,直接赋值对象性能较好。

另一个需要理解的地方是:this._vm._data.$$state_data中访问$$state

_data是Vue实例_vm的真实值(见Vue源码),而_vm.$$state是对_vm._data.$$state取值的伪属性(见Vue源码)。同样是因为直接取真实值性能较好。

至此,可以明确this.$store.state访问的结果是由state_wrappedGetters构成的Vue实例的状态。

接下来看state是如何赋值的。

ModuleCollection

Vuex.Store构造函数实例化过程中可以找到state初始赋值的位置:

import ModuleCollection from './module/module-collection'
//...
export class Store {
  constructor(options = {}){
    //...
    this._modules = new ModuleCollection(options)
    //...
    const state = this._modules.root.state
    installModule(this, state, [], this._modules.root)
    //...
  }
  //...
}
//...

赋予的值为this._modules.root.state,语义上理解为“根模块的状态”。this._modules是一个“模块集合”类的实例,具体看ModuleCollection内容:

src/module/module-collection.js

import Module from './module'
//...

export default class ModuleCollection {
  constructor (rawRootModule) {
    // 注册根模块 (Vuex.Store options)
    this.register([], rawRootModule, false)
  }
  
  //...
  
  register (path, rawModule, runtime = true) {
    // 省略开发环境类型断言

    const newModule = new Module(rawModule, runtime)
    //...

    // 注册嵌套模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
  
  //...
}

//...

可以看出模块集合ModuleCollection中的模块Module内容,是在Module类中定义的。

进一步查看Module类的源码:

src/module/module.js

import { forEachValue } from '../util'

// 存储模块的基本数据结构,包含一些属性和方法
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // 存储子项
    this._children = Object.create(null)
    // 存储开发者传入的原始模块对象
    this._rawModule = rawModule
    const rawState = rawModule.state

    // 存储原始模块的状态
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  get namespaced () {
    return !!this._rawModule.namespaced
  }

  addChild (key, module) {
    this._children[key] = module
  }

  removeChild (key) {
    delete this._children[key]
  }

  getChild (key) {
    return this._children[key]
  }

  update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

  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类负责存储

  • runtime标识
  • 子项的索引
  • 当前模块的状态

并提供

  • 原始模块namespaced的布尔值取值器
  • 增删查子项的方法
  • 更新原始模块(更新内容包括四个字段:namespacedactionsmutationsgetters)的方法
  • 遍历原始模块的子项、actionsmutationsgetters的方法

回过头看ModuleCollection类:

import Module from './module'
//...

export default class ModuleCollection {
  constructor (rawRootModule) {
    // 注册根模块 (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

  getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

  update (rawRootModule) {
    update([], this.root, rawRootModule)
  }

  register (path, rawModule, runtime = true) {
    // 省略开发环境类型断言

    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)
    }

    // 注册嵌套模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  unregister (path) {
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    if (!parent.getChild(key).runtime) return

    parent.removeChild(key)
  }
}

function update (path, targetModule, newModule) {
  // 省略开发环境类型断言

  // 更新目标模块
  targetModule.update(newModule)

  // 更新嵌套模块
  if (newModule.modules) {
    for (const key in newModule.modules) {
      if (!targetModule.getChild(key)) {
        // ...
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
}

上面源码中path是指嵌套模块的路径,形如a/b/c这样的嵌套模块,对应的path值为[a, b, c]

可见ModuleCollection类负责存储

  • 根模块root

并提供

  • get(path):根据路径,获取根模块中某个模块
  • getNamespace(path):根据路径,获取模块命名空间
  • update():递归地更新根模块及其后代的原始模块
  • register():递归地注册模块及其嵌套模块,注册模块是指将传入的原始模块存储为Module实例
  • unregister(path):取消注册模块,即将指定路径的模块从父级的子项索引中移除

现在可以明确在src/store.js

const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)

初始赋值的state是指,ModuleCollection实例根模块的状态。这个状态仅包括根模块,不含嵌套模块,这样的state传递给resetStoreVM不足以实现嵌套模块的数据响应。

下面看installModule()内容。

installModule

src/store.js

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

  // 注册到命名空间映射表
  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)
    })
  }
  
  //...

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

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

installModule()递归地完成了模块的安装,其中就包括state嵌套赋值。

store._withCommit()是定义在Store类上的方法(见源码):

export class Store {
  constructor (options = {}) {
    //...
    this._committing = false
    //...
  }
  //...
  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
}

该方法修改store._committing的目的只有一个

import { assert } from './util'
//...
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

其中,assert在第一个参数为假值时,才会抛出错误。

src/util.js

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

即Vuex在严格模式下,当this._data.$$state被观察到发生变化时,会在开发环境给出警告,而通过_withCommit包装后执行函数,则不会发出警告。

同理,Vuex mutations中状态变化不会发出警告,也是调用了_withCommit方法

下面就来分析Store.commit()方法:

commit

源码位置

//...
export class Store {
  //...
  commit (_type, _payload, _options) {
    // 检查对象风格的提交方式
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
    if (!entry) {
      // 省略警告
      return
    }
    this._withCommit(() => {
      // 执行指定的mutation函数
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub => sub(mutation, this.state))
    //...
  }
  //...
}

//...

function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (process.env.NODE_ENV !== 'production') {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}
//...

首先,使用unifyObjectStyle()函数对入参进行转换,用以支持对象风格的提交方式

然后,通过this._withCommit()包装执行入参type指定的mutation函数,可以避免严格模式警告。

最后,触发subscribers订阅器,这些订阅器是通过subscribe() API添加的。

类似的,Store.dispatch()方法也很简单:

dispatch

源码位置

export class Store {
  //...
  
  dispatch (_type, _payload) {
    // 检查对象风格的提交方式
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      // 省略警告
      return
    }

    //省略_actionSubscribers

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return result.then(res => {
      //省略_actionSubscribers
      return res
    })
  }
	//...
}

commit不同的是,dispatch中执行的_actions是Promise实例,是在installModule()中创建的

源码位置

function installModule (store, rootState, path, module, hot) {
  //...
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  //...
}

registerAction()源码位置

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (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.resolve(res)
    }
    //省略devtool
    return res
  })
}

至此,Vuex常用部分的源码已经解析完成,仍有一些相对独立部分的源码没有提到,请自行了解。

总结

用思维导图总结一下本文解析的源码内容:

Vuex

ModuleCollection

在阅读过源码后,重新回答Vuex官网上提出的这个问题:什么情况下我应该使用 vuex?

什么情况下我应该使用 Vuex?

当你需要在Vue组件中管理共享状态时,就可以使用Vuex。

Vuex不仅提供了基础的状态管理、提交改动、分发事件,还提供了以嵌套模块、命名空间的方式来分治模块的模块化方案,更进一步提供了本文没有提及的插件(plugins)、监听、订阅、动态注册等功能。

即使你只用到了Vuex其中的一部分能力,Vuex提供的丰富能力意味着良好的扩展性,这对项目维护而言是有益的。

那为什么还需要思考这个该不该用的问题呢?因为这是一个技术选型问题,主要影响因素不在于Vuex技术本身,而在于项目本身。该不该用的问题,应当换一种问法:

可以使用Vuex的Vue项目,为什么不用Vuex?

我的回答是:

  • 性价比考量

    项目的状态管理是否复杂,或预期能够复杂到值得引入压缩后超过9KB的Vuex?开发时间有没有压力?

  • 有没有其他选择

    提供状态管理的库不止Vuex一种,是否有更熟悉、更亲睐的其他方案?比如团队内部自研库。

  • 性能问题

    Vuex源码多处使用递归,嵌套的模块越多,嵌套层级越深,性能越差,这种情况下不得不考虑不使用Vuex的更高性能状态管理方案。

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

No branches or pull requests

1 participant