Vuex原理详解

  一、Vuex是什么

    Vuex是专门为Vuejs应用程序设计的状态管理工具。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生改变。它集中于MVC模式中的Model层,规定所有数据操作必须通过 action - mutation - statechange 的流程来进行,再结合Vue的数据视图双向绑定特效来实现页面的展示更新。

    1.Vuex框架运行流程

     

  如图我们可以看到Vuex为Vue Components建立起了一个完整的生态圈,包括开发中的api调用,vuex的简单分析:

   Vue组件接收交互行为,调用dispatch方法触发action相关处理,若页面状态需要改变,则调用commit方法提交mutation修改state,通过getters获取到state新值,提供了mapState、MapGetters、MapActions、mapMutations等辅助函数给开发在vm中处理store,重新渲染Vue Components,页面随之更新。

二、目录结构介绍

  

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

    module:提供module对象与module对象树的创建功能;

    plugins:提供开发辅助插件,如“时间穿梭”功能,state修改的日志记录功能等;

    helpers.js:提供action、mutation以及getters的查找API;

    index.js:是源码主入口文件,提供store的各module构建安装;

    mixin.js:提供了store在Vue实例上的装载注入;

    util.js:提供了工具方法如find、deepCopy、forEachValue以及assert等方法。

 三、初始化装载与注入

了解大概的目录及对应功能后,下面开始进行源码分析。

1.装载实例

 先看一个简单的🌰:

   

    store.js文件中,加载Vue框架,创建并导出一个空配置的store对象实例。

   

    然后在index.js中,正常初始化一个页面根级别的Vue组件,传入这个自定义的store对象。

   那么问题来了,使用Vuex只需要Vue.use(Vuex),并在Vue的配置中传入一个store对象的实例,store是如何实现注入的?

2.装载分析

 index.js文件代码执行开头,定义局部变量,用于判断是否已经装载和减少全局作用域查找。

let Vue

 然后判断若处于浏览器环境下且加载过Vue,则执行install方法。

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

  install方法将Vuex装载到Vue对象上, Vue.use(Vuex) 也是通过它执行,先看下Vue.use方法实现:

function (plugin:Function | Object) {
  /* istanbul ignore if */
  if(plugin.installed) {
    return
  }
  // additional parameters
  const args = toArray(arguments,1)
  args.unshift(this)
  if(typeof plugin.install === 'function') {
    //实际执行插件的install方法
    plugin.install.apply(plugin , args)
  }else {
    plugin.apply(null , args)
  }
  plugin.installed = true
  return this
}

 如果是首次加载,将局部Vue变量赋值为全局的Vue对象,并执行applyMixin方法,install实现如下:

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

 来看下applyMinxin方法内部代码。如果是2.x以上版本,可以使用hook的形式进行注入,或使用封装并替换Vue对象原型的_init方法,实现注入。

export default function (Vue) {
  //获取vue版本号

  const version = Number(Vue.version.split('.')[0])
  //判断版本是否大于2
  if(version >= 2) {
    const useInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(useInit ? { 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)
    }
  }
}
function vuexInit () {
  //通过 this.$options 对象获取任何数据和方法
  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设置到this对象的$store属性上,子组件从其父组件引用$store属性,层层嵌套进行设置,在任意组件中执行this.$store都能找到装载的那个store对象。

几张图理解下store的传递。

  页面Vue结构:

      

 

   对应store流向:

      

 

 四、store对象构造

  上面对Vuex框架的装载以及注入自定义store对象进行了分析,接下来详细分析store对象的内部功能和具体实现,那么问题又来了,为什么actions、getters、mutations中能从arguments[0]中拿到store的相关数据?

  store对象实现逻辑比较复杂,先看下构造方法的整体逻辑流程来帮助后面的理解:

  

4.1 环境判断

  开始分析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.`)
}

 在store构造函数中执行环境判断,以下都是Vuex工作的必要条件:

  1.已经执行安装函数进行装载;

  2.支持Promise语法

 

 assert函数是一个简单的断言函数的实现,一行代码即可实现

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

 

4.2 数据初始化、module树构造

 环境判断后,根据new构造传入的options或默认值,初始化内部数据。

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

//store状态初始化
this._committing = false  // 是否在进行提交状态标识
this._actions = Object.create(null)  // actions操作对象
this._mutations = Object.create(null)  // mutations操作对象
this._wrappedGetters = Object.create(null)  // 封装后的getters集合对象
this._modules = new ModuleCollection(options)  // vuex支持store分模块传入,存储分析后的modules
this._modulesNamespaceMap = Object.create(null)  // 模块命名空间
this._subscribers = []  // 订阅函数集合,Vuex提供了subscribe功能
this._watcherVM = new Vue()  // Vue组件用于watch监视变化

调用 new Vuex.store(options)时传入的options对象,用于构造ModuleCollection类,下面看看其功能

constructor(rawRootModule) {
  // register root module (Vuex.store options)
  this.root = new Module(rawRootModule, false)

  // register all nested modules
  if(rawRootModule.modules) {
    forEachValue(rawRootModule.modules, (rawModule, key) => {
      this.register([key], rawModule, false)
    })
  }
}

ModuleCollection主要将传入的options对象整个构造为一个modele对象,并循环调用 this.register([key], rawModule, false) 为其中的modules属性进行模块注册,使其都成为module对象,最后options对象被构造成一个完整的组件树。ModuleCollection类还提供了modules的更替功能,详细实现可以查看源文件 module-collection.js

4.3 dispatch与commit设置

 继续回到store的构造函数代码。

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

 封装替换原型中的dispatch和commit方法,将this指向当前store对象。

 dispatch和commit方法具体实现如下:

dispatch (_type, _payload) {
  //check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)  // 配置参数处理
  
  // 当前type下所有action处理函数集合
  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的功能是触发并传递一些参数(payload)给对应的type的action。因为其支持2种调用方法,所以在dispatch中,先进行参数(payload)给对应type的action。因为其支持2种调用方法,所以在dispatch中,先进行参数的适配处理,然后判断action type是否存在,若存在就逐个执行(注:上面代码中的this._actions[type] 以及下面的this._mutations[type]均是处理过的函数集合,具体内容留到后面进行分析)。

 commit方法和dispatch相比虽然都是触发type,但是对应的处理却相对复杂,代码如下。

commit (_type, _payload, _options) {
  //check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]
  if(!entry) {
    console.error(`[vuex] unknown mutation type: ${type}`)
    return
  }
  //专用修改state方法,其他修改state方法均是非法修改
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  //订阅者函数遍历执行,传入当前的mutation对象和当前的state
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if(options && options.silent) {
    console.warn(
      `
        [vuex] mutation type: ${type}. Silent option has been removed. ` + `
        Use the filter functionality in the vue-devtools
      `
    )
  }
}

 该方法同样支持2种调用方法。先进行参数适配,判断触发mutation type,利用_widthCommit方法执行本次批量触发mutation处理函数,并传入payload参数。执行完成后,通知所有_subscribers(订阅函数)本次操作的mutation对象以及当前的state状态,如果传入了已经移除的silent选项则进行提示警告。

4.4 state修改方法

  _withCommit是一个代理方法,所有触发mutation的进行state修改的操作都经过它,由此来统一管理监控state状态的修改。实现代码如下。

_widthCommit (fn) {
  // 保存之前的提交状态
  const committing = this._committing
  
  //进行本次提交,若不设置为true,直接修改state,strict模式下,vuex将会产生非法修改state的警告
  this._committing = true

  //执行state的修改操作
  fn()

  //修改完成,还原本次修改之前的状态
  this._committing = committing
}

  缓存执行时的committing状态将当前状态设置为true后进行本次提交操作,待操作完毕后,将committing状态还原为之前的状态。

4.5 module安装

   绑定dispatch和commit方法之后,进行严格模式的设置,以及模块的安装(installModule)。 由于占用资源较多影响页面性能,严格模式建议只在开发模式开启,上线后需要关闭。

// 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, [], this._modules.root)

4.5.1 初始化rootState

   上述代码的备注中,提到installModule方法初始化组件树根组件、注册所有子组件,并将其中所有的getters存储到this._wrappedGetters属性中,让我们看看其中的代码实现。

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

  //register in namespace map
  if(namespace) {
    store._modulesNamespaceMap[namespace] = module
  }

  //非根组件设置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)
    })
  }
}

  判断是否是根目录,以及是否设置了命名空间,若存在则在namespace中进行module的存储,在不是根组件且不是hot条件的情况下,通过getNestedState方法拿到该module父级的state,拿到其所在的moduleName,调用Vue.set(parentState, moduleName, module.state) 方法将其state设置到父级对象的moduleName属性中,由此实现该模块的state注册(首次执行这里,因为是根目录注册,所以并不会执行该条件中的方法)。getNestedState方法代码很简单,分析path拿到state,如下。

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

4.5.2 module上下文环境设置

const local = module.context = makeLoaclContext(store, namespace, path)

  命名空间和根目录条件判断完毕后,接下来定义local变量和module.context的值,执行makeLocalContext方法,为该module设置局部的dispatch、commit方法以及getter和state(由于namespace的存在需要做兼容处理)。

4.5.3 mutations、actions以及getters注册

 定义local环境后,循环注册我们在options中配置的action以及mutation等。逐个分析各注册函数之前,先看下模块间的逻辑关系流程图:

 

 

 下面分析代码逻辑:

//注册对应模块的mutation,供state修改使用
module.forEachMutation((mutation, key) => {]
  const namespaceType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})

//注册对应模块的action,供数据操作、提交mutation等异步操作使用
module.forEachAction((action, key) => {
  const namespaceType = namespace + key
  registerAction(store, namespaceType, action, local)
})

//注册对应模块的getters,供state读取使用
module.forEachGetter(getter, key) {
  const namespaceType = namespace + key
  registerGetter(store, namespacedTyp, getter, local)
})

  registerMutation方法中,获取store中的对应mutation type的处理函数集合,将新的处理函数push进去。这里将我们设置在mutation type上对应的handler进行了封装,给原函数传入了state。在执行commit('xxx', payload)的时候,type为xxx的mutation的所有handler都会接受到state以及payload,这就是在handler里面拿到state的原因。

function registerMutation (store, type, handler, local) {
  //取出对应type的mutations-handler集合
  const entry = store._mutations[type] || (store._actions[type] = [])
  //存储新的封装过的action-handler
  entry.push(function wrapperActionHandler (payload, cb) {
    //传入state 等对象供我们原action-handler使用
    let res = handler({
      dispatch: local.dispatch,  
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    //action需要支持promise进行链式调用,这里进行兼容处理
    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
    }
  })
}

function registerGetter (store, type, rawGetter, local) {
  //getters值允许存在一个处理函数,若重复需要报错
  if(store._wrappedGetters[type]) {
    console.error(`[vuex] duplicats getter key: ${type}`)
    return
  }

  //存储封装过的getters处理函数
  store._wrappedGetters[type] = function wrappedGetter (store) {
    //为原getters传入对应状态
    return rawGetter(
      local.state,  //local state
      local.getters,  //local getters
      store.state,  //root state
      store.getters  //root getters
    )
  }
}

  action handler比mutation handler以及getter wrapper多拿到dispatch和commit操作方法,因此action可以进行dispatch  action和commit mutation操作。

4.5.4 子module安装

  注册完了根组件的actions、mutations以及getter后,递归调用自身,为子组件注册其state,actions、mutation以及getters等。

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

4.5.5实例结合

  前面介绍了dispatch和commit方法以及actions等的实现,下面结合一个官方的购物车实例中的部分代码来加深理解。

Vuex配置代码:

/*
*  store-index.js store配置文件
*/

import Vue from 'vue'

import Vuex from 'vuex'

import * as actions from './actions'

import * as getters from './getters'

import cart from './modules/cart'

import products from './modules/products'

import createLogger from '../../../src/plugin/logger'

 

Vue.use(Vuex)

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

export default new Vuex.Store({

  actions,

  getters,

  modules: {

    cart,

    products

  },

  strict: debug,

  plugins: debug ? [createLogger()] : []

}) 

Vuex组件module中各模块state配置代码部分:

/**
* cart.js
*
**/

const state = {
  added: [],
  checkoutStatus: null
}
/**
* products.js
*
**/
const state = {
  all: []
}

  加载上述配置后,页面state结构如下图:

            

 

 

   state中的属性配置都是按照option配置中module path的规则来进行的,下面看action的操作实例。

  Vuecart组件代码部分:

/**
* Cart.vue  省略template代码,只看script部分
*
**/
export default {
  methods: {
    //购物车中的购买按钮,点击后会触发结算,源码中会调用dispatch方法
    checkout (products) {
      this.&store.dispatch('checkout',products)
    }
  }
}

  Vuexcart.js组件action配置代码部分:

const action = {
  checkout ({ commit, state }, products) {
    const saveCartItems = [...state.added]  //存储添加到购物车的商品
    commit(types.CHECKOUT_REQUEST)   //设置提交结算状态
    shop.butProducts(  //提交api请求,并传入成功与失败的cb-func
      products,
      () => commit(type.CHECKOUT_SUCCESS),   //请求返回成功则设置提交成功状态
      () => commit(types.CHECKOUT_FALLURE, { savedCartItems })  //请求返回失败则设置提交失败状态
    )
  }
}

  Vue组件中点击购买执行当前module的dispatch方法,传入type值为'checkout', payload值为'products',在源码中dispatch方法在所有注册过的actions中查找'checkout'的对应执行数组,取出循环执行。执行的是被封装过的被命名为wrappedActionHandler的方法,真正传入的checkout的执行函数在wrappedActionHandler这个方法中被执行,源码如下(注:前面贴过,这里看一次):

function wrappedActionHandler (payload, cb) {
  let res = handler({
    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)
  }
  if(store._devtoolHook) {
    return res.catch(err => {
      store._devtoolHook.emit('vuex:error', err)
      throw err
    })
  } else {
    return res
  }
}

  handler在这里就是传入的checkout函数,其执行需要的commit以及state就是在这里被传入,payload也传入了,在实例中对应接受的参数名为products。commit的执行也是同理的,实例中checkout还进行了一次commit操作,提交一次type值为types.CHECKOUT_REQUEST的修改,因为mutation名字是唯一的,这里进行了常量形式的调用,防止命名重复,执行跟源码分析中一致,调用function wrappedMutationHandler (payload) { handler(local.state, payload)} 封装函数来实际调用配置的mutation方法。

看完源码分析和上面的小实例,应该能理解dispatch action 和 commit mutation的工作原理了。接着看源码,看看getters是如何实现state实时访问的。

4.6 store._vm组件设置

  执行完各module的install后,执行resetStoreVM方法,进行store组件的初始化。

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

  综合前面的分析可以了解到,Vuex其实构建的就是一个名为store的vm组件,所有配置的state、action、mutation以及getters都是其组件的属性,所有的操作都是对这个vm组件进行的。

  resetStoreVM方法的内部实现:

function resetStoreVM(store, state) {
  const oldVm = store._vm  //缓存前vm组件

  //bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  //循环所有处理过的getters,并新建computed对象进行存储,通过Object.defineProperty方法为getters对象建立属性,使得我们通过
  //this.$store.getters.xxxgetter能够访问到该getters
  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
    })
  })

  //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设为静默模式,避免报出用户加载的某些插件触发的警告
  Vue.config.silent = true
  //设置新的storeVm,将当前初始化的state以及getters作为computed属性(刚刚遍历生成的)
  store._vm = new Vue({
    data: {state},
    computed
  })
  //恢复Vue的模式
  Vue.config.silent = silent
  
  //enable strict mode for new vm
  is(store.strict) {
    //该方法对state执行$watch以禁止从mutation外部修改state
    enableStrictMode(store)
  }
  //若不是初始化过程执行的该方法,将旧的组件state设置为null,强制更新所有监听者(watchers),待更新生效,DOM更新完成后,执行vm
  //组件的destroy方法进行销毁,减少内存的占用
  if(oldVm) {
    //dispatch change in all subscribed watchers
    //to force getter re-evaluation.
    store._withCommit(() => {
      oldVm.state = null
    })
    Vue.nextTick(() => oldVm.$destroy())
  }
}

  resetStoreVm方法创建了当前store实例的_vm组件,至此store就创建完毕了。上面代码涉及到了严格模式的判断,看一下严格模式

如何实现的。

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

很简单的应用,监视state的变化,如果没有通过this._withCommit()方法进行state修改,则报错。

4.7 pugin注入

最后执行plugin的植入。

plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))

devtoolPlugin提供的功能有3个:

// 1.触发Vuex组件初始化的hook
devtoolHook.emit('vuex:init', store)

// 2.提供"时空穿梭"功能,即state操作的前进和倒退
devtoolHook.on('vuex:travel-to-state', targetState => {
  store.replaceState(targetState)
})

// 3.mutation被执行时,触发hook,并提供被触发的mutation函数和当前的state状态
store.subscribe((mutation, state) => {
  devtoolHook.emit('vuex:mutation', mutation, state)
})

五、总结

   1.问:使用Vuex只需执行 Vue.use(Vuex),并在Vue的配置中传入一个store对象的示例,store是如何实现注入的?

  “

    答: Vue.use(Vuex) 方法执行的是install方法,它实现了Vue实例对象的init方法封装和注入,使传入的store对象被

       设置到Vue上下文环境的&store中。因此在Vue Component任意地方都能够通过this.$store访问到该store。

  ”  

   2.问:state内部支持模块配置和模块嵌套,如何实现的?

  “

    答:在store构造方法中有makeLocalContext方法,所有module都会有一个local context,根据配置时的path进行

      匹配。所以执行如dispatch('submitOrder', payload)这类action时,默认的拿到都是module的local state,如

      果要访问最外层或者是其他module的state,只能从rootState按照path路径逐步进行访问。

  ”

   3.问:在执行dispatch触发action(commit同理)的时候,只需传入(type,payload),action执行函数中第一个参数

     store从哪里获取的?

  “

    答:store初始化时,所有配置的action和mutation以及getters均被封装过。在执行如dispatch('submitOrder', payload)

      的时候,action中type为submitOrder的所有处理方法都是被封装后的,其第一个参数为当前的store对象,所以能够

      获取到{dispatch, commit, state, rootState}等数据。

  ”

   4.问:Vuex如何区分state是外部直接修改,还是通过mutation方法修改的?

  “

    答:Vuex中修改state的唯一渠道就是执行commit('xx', payload) 方法,其底层通过执行 this._withCommit(fn) 设置

      _committing标志变量为true,然后才能修改state,修改完毕还需要还原_committing变量。外部修改虽然能够

      直接修改state,但是并没有修改_committing标志位,所以只要watch一下state,state change时判断是否

      _committing值为true,即可判断修改的合法性。

  ”

  5.问:调试时的“时空穿梭”功能是如何实现的?

  “

    答:devtoolPlugin中提供了此功能。因为dev模式下所有的state change都会被记录下来,‘时空穿梭’功能其实就是

      将当前的state替换为记录中某个时刻的state状态,利用store.replaceState(targetState)方法将执行

      this._vm.state = state实现。

  ”

   原文地址:https://tech.meituan.com/2017/04/27/vuex-code-analysis.html

posted @ 2020-09-12 10:52  你吃辣条儿吗  阅读(8093)  评论(0编辑  收藏  举报