vue2状态管理:vuex

184 阅读5分钟

高频面试题:vue中的vuex?

答案:vuex是状态管理仓库,其管理的状态是响应式的,修改也只能显式提交mutation的方式修改。一般使用的场景为:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态。vuexstategettermutationactionmodule五个核心。

// main.js文件
Vue.use(Vuex);
var moduleAA = {
  // namespaced: true,
  state: {
    aaCount: 0
  },
  mutations: {
    incrementAA(state) {
      state.aaCount++;
    }
  }
};
var moduleA = {
  // namespaced: true,
  state: {
    aCount: 0
  },
  getters: {
    getCountA(state) {
      return state.aCount + 100;
    }
  },
  mutations: {
    incrementA(state) {
      state.aCount++;
    }
  },
  actions: {
    incrementA(context) {
      context.commit("incrementA");
    }
  },
  modules: {
    aa: moduleAA
  }
};
var moduleB = {
  // namespaced: true,
  state: {
    bCount: 0
  },
  getters: {
    getCountB(state) {
      return state.bCount + 10;
    }
  },
  mutations: {
    incrementB(state) {
      state.bCount++;
    }
  }
};

const store = new Vuex.Store({
  // namespaced: true,
  state: {
    count: 0
  },
  getters: {
    getCount(state) {
      return state.count + 1;
    }
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment(context) {
      context.commit("increment");
    }
  },
  modules: {
    a: moduleA,
    b: moduleB
  }
});

new Vue({
  el: "#app",
  store,
  template: `<div>
    <button @click="increment">点击修改B模块中的数据</button>
    <p>{{changeData}}</p>
  </div>`,
  computed: {
    changeData() {
      return store.getters.getCountB;
    }
  },
  methods: {
    increment() {
      this.$store.commit("incrementB");
    }
  }
});

以上例子可以直接粘贴到main.js文件中运行测试,namespaced: true表示支持命名空间。

一、安装

vue通过Vue.use(Vuex)的方式进行vuex的安装,Vue.use定义在initGlobalAPI(Vue)阶段的initUse(Vue)中:

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

当执行Vue.use(vuex)时,获取到this.installedPlugins,如果其中存在当前的plugin则直接返回,否则获取除去前1位(vuex)后的参数数组,然后将this(Vue)补充至首位,就是将第一个参数由vuex换成了Vue,再看vuex中的install函数。

installvuex的构成属性之一,与StoreversionmapStatemapMutationsmapGettersmapActionscreateNamespacedHelpers共同构成了vuex对象:

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

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

install定义在同级目录下的store.js文件中:

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

function applyMixin (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // ...
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

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

vuex中的installfunction类型,所以,再以Vuex为执行主体,Vue为参数,执行plugin.install.apply(plugin, args),相当于执行install (Vue)

install(Vue)中首次安装则会执行_Vue = Vue,这里的_Vue就可以在全局使用,如果进行第二次重复安装,则会执行if (Vue && _Vue === Vue) return进行终止操作。接下来会执行applyMixin(Vue)

applyMixin(Vue)中如果vue版本号大于2,则通过Vue.mixin({ beforeCreate: vuexInit })beforeCreate中混入vuexInit函数。在执行钩子函数beforeCreate时,如果在当前实例中存在options.store并且options.store为函数则执行store函数,否则直接返回options.store

二、实例化Store

export class Store {
  constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

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

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()

    // 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
    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)

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

    // apply plugins
    plugins.forEach(plugin => plugin(this))

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

Store是构造函数,实例化过程中主要构建了modules树。

1、ModuleCollection

构建Module树,那么先看Moduleclass类:

// Base data struct for store's module, package with some attribute and method
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  addChild (key, module) {
    this._children[key] = module
  }
  getChild (key) {
    return this._children[key]
  }
  // 这里先省略其他方法...
}

这里的Module是一个class类,实例化的过程中会产生包含runtime_children_rawModulerawStatestate的一个对象。

在实例化Store的过程中是通过this._modules = new ModuleCollection(options)的方式进行了Module树的构建:

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }
    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)
      })
    }
  }
}

ModuleCollection中的构造函数,执行了this.register([], rawRootModule, false),首先通过const newModule = new Module(rawModule, runtime)的方式将rawModule作为参数传入实例化了Module的一个实例。

首次执行this.register([], rawRootModule, false)path[],将newModule赋值给this.root

再次执行this.register([], rawRootModule, false)path不为[],则执行到else逻辑,通过path.slice(0, -1)的方式将当前路径中的最后一位剔除,然后,通过this.get,即path.reduce的方式从根开始寻找parent。然后通过parent.addChild(path[path.length - 1], newModule),即this._children[key] = module的方式构建父子关系。

最后,如果存在rawModule.modules,通过path.concat(key)的方式拼接路径,作为this.register执行的路径参数继续深度递归执行,实现整棵Module树的构建。

2、installModule

通过installModule(this, state, [], this._modules.root)的方式为每个节点安装mutationactiongetter

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)

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

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

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

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

这里在安装前先通过makeLocalContext(store, namespace, path)的方式,将path作为参数,进行本地化local处理:

/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 */
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

当前local的目的是通过闭包的方式,在Module树中将当前节点的数据进行锁定,后续安装中可以直接访问到当前节点中的state;并且在执行Store中的commitdispatch之前先进行未定义方法的校验,然后再执行Store中的commitdispatch。这里的local类似于代理模式的保护代理,对于访问未定义的mutation或者action前直接return,起到保护的作用。

(1)registerMutation
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

通过wrappedMutationHandler对真实的逻辑进行包裹,然后定义到store._mutations中,这里就用到了local中的state

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

通过wrappedMutationHandler对真实的逻辑进行包裹,这里将当前节点local中的local.dispatchlocal.commitlocal.getterslocal.statelocal.states store.gettersstore.state作为参数来源,调用真实的hander函数,如果返回值不是Promise类型,将其通过Promise.resolve(res)进行处理。

(3)registerGetter
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

这里分别将local以及store中的stategetters作为参数,进行wrappedGetter的包裹,然后以type方法名为键值赋值给store._wrappedGetters

3、resetStoreVM

这里通过resetStoreVM(this, state)Store_vm进行响应式处理:

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  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.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent
  // ...
}

这里通过new Vue的方式,借助Vue构造函数的特性,将$$state: state作为data,进行响应式处理;将computed作为计算属性,进行缓存处理,只有数据改变时,才会进行真实getter的执行

总结

VuexVue中数据集中化管理的方案,其中通过stategettermutationactionmodule五个核心属性进行管理,并且module也让数据管理变得模块化。