Vuex源码 - 结合小实例解读

883 阅读7分钟

作为Vue全家桶之一的vuex,在Vue框架中起着很重要的角色。Vuex源码篇幅虽然不多,但解读起来还是需要下一些功夫的。下面我们就参照vuex官方指南文档内容进行分析。

vuex是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

解决了什么问题?

Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果只是简单的单页应用,最好不要使用 Vuex,使用 Vuex 可能是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果构建一个中大型单页应用,需要考虑如何更好地在组件外部管理状态,Vuex 将会成为最佳选择。

vuex使用场景.png

Vuex设计思想是什么?

Vuex是一个专门为Vue.js框架设计的、用于对Vue.js应用程序进行状态管理的库,它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,以一个单例存放,同时利用Vue.js的响应式机制来进行高效的状态管理与更新。正是因为Vuex使用了Vue.js内部的“响应式机制”,所以Vuex是一个专门为Vue.js设计并与之高度契合的框架(优点是更加简洁高效,缺点是只能跟Vue.js搭配使用)。
vuex状态自管理应用包含以下几个部分:
state:驱动应用的数据源;
view:以声明方式将 state 映射到视图;
actions:响应在 view 上的用户输入导致的状态变化。
来看一下“单向数据流”理念的简单示意图:


Vuex的完整数据流程图

Vuex最佳实践演示 - 一个小实例:

vuex在项目中应用的目录,在src下创建store文件:

├── store  
│   ├── modules
│       ├── todos.js  
│   ├── getter.js   
│   ├── index.js   
├── APP.vue     
└── main.js

index.js

//index.js
import Vue from 'vue';
import Vuex from 'vuex';
import getters from './getters'
import app from './modules/todos'
//Load Vuex
Vue.use(Vuex)
//create store
export default new Vuex.Store({
  modules: { app },
  getters
})

getters.js

//getters.js
const getters = {
  allTodos: (state) => state.app.todos,
  red: (state) => state.app.red,
  blue: (state) => state.app.blue
}
export default getters

todos.js

// todos.js
const app = {
	state: {
    todos: [{
      id: 1,
      title: 'one'
    },{
      id: 2,
      title: 'two'
    }],
    red: 0,
    blue: 0
 },
  mutations: {
    ADD_COUNT: (state) => {
      state.red++,
      state.blue++
    },
  MINUS_COUNT: (state)=> {
      state.red--,
      state.blue--
    }
  },
  actions: {
    addCounts: ({commit})=>{
      commit('ADD_COUNT')
    },
    minusCounts: ({commit})=>{
      commit('MINUS_COUNT')
    }
  }
}
export default app

main.js

//main.js
import store from './store'

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')

APP.vue

import { mapGetters } from 'vuex'
......
export default {
  name: 'app',
  computed: mapGetters(['allTodos'])
}

子组件应用:

//组件应用
computed:{
		red(){
			return this.$store.state.app.red
		},
		blue(){
			return this.$store.state.app.blue
		}
	},
	methods: {
		addOne() {
			this.$store.dispatch('addCounts')
		},
		minusOne() {
			this.$store.dispatch('minusCounts')
		},
	}

Vuex核心源码分析:

先看下vuex官方指南文档,再结合上面的小实例对核心源码进行分析。
    vuex官网指南:vuex.vuejs.org/zh/guide/
    vuex源码:github.com/vuejs/vuex
    git上下载的源码文件很多,但是核心执行源码存在/src里,源码文件不多,目录划分也很清晰,每个文件都有着各自的功能:

  • module:提供 module 对象与 module 对象树的创建功能;
  • plugins:提供开发辅助插件,如 “时光穿梭” 功能,state 修改的日志记录功能等;
  • helpers.js:提供 action、mutations 以及 getters 的查找 API;
  • index.js:是源码主入口文件,提供 store 的各 module 构建安装;
  • mixin.js:提供了 store 在 Vue 实例上的装载注入;
  • store.js:实现Store构造方法;
  • util.js:提供了工具方法如 find、deepCopy、forEachValue 以及 assert 等方法。

Vuex源码目录结构

├── module  
│   ├── module-collection.js  
│   ├── module.js   
├── plugins     // 插件
│   ├── devtool.js  
│   ├── logger.js  
├── helpers.js  //辅助函数
├── index.esm.js 
├── index.js    //入口文件
├── mixin.js 
├── store.js 
└── util.js

初始化装载与注入

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
}

constructor - 构造方法

在实例的store文件夹的index.js里通过new store来注册store,下面是具体Store构造方法,可以结合代码注释看:

constructor (options = {}) {
		......
    // store internal state
    /* 用来判断严格模式下是否是用mutation修改state的 */
    this._committing = false
    /* 存放action */
    this._actions = Object.create(null)
    this._actionSubscribers = []
    /* 存放mutation */
    this._mutations = Object.create(null)
    /* 存放getter */
    this._wrappedGetters = Object.create(null)
    /* module收集器 */
    this._modules = new ModuleCollection(options)
    /* 根据namespace存放module */
    this._modulesNamespaceMap = Object.create(null)
    /* 存放订阅者 */
    this._subscribers = []
    /* 用以实现Watch的Vue实例 */
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    /*将dispatch与commit调用的this绑定为store对象本身,否则在组件内部this.dispatch时的this会指向组件的vm*/
    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)
    }

    // strict mode
    /*严格模式(使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误)*/
    this.strict = strict

    const state = this._modules.root.state

    /*初始化根module,这也同时递归注册了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才独有保存的Module对象*/
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
    resetStoreVM(this, state)

    // apply plugins  应用插件
    plugins.forEach(plugin => plugin(this))

    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }

dispatch - 分发Action

在实例的子组件应用里会用到dispatch,dispatch的功能是触发并传递一些参数(payload)给对应 type 的 action。在dispatch 中,先进行参数的适配处理,然后判断 action type 是否存在,若存在就逐个执行。

/* 调用action的dispatch方法 */
  dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    /* actions中取出type对应的ation */
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

    /* 是数组则包装Promise形成一个新的Promise,只有一个则直接返回第0个 */
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return result.then(res => {
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(`[vuex] error in after action subscribers: `)
          console.error(e)
        }
      }
      return res
    })
  }

commit  -  mutation事件提交

commit方法和dispatch相比虽然都是触发type,但是对应的处理却相对复杂。先进行参数适配,判断触发mutation type,利用_withCommit方法执行本次批量触发mutation处理函数,并传入payload参数。执行完成后,通知所有_subscribers(订阅函数)本次操作的mutation对象以及当前的 state 状态,如果传入了已经移除的 silent 选项则进行提示警告。可以看实例里module的actions的属性。

/* 调用mutation的commit方法 */
  commit (_type, _payload, _options) {
    // check object-style commit 校验参数
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    /* 取出type对应的mutation的方法 */
    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'
      )
    }
  }

state 方法

在实例module里state有常量和变量,可以结合源码来看状态的改变原理。_withCommit是一个代理方法,所有触发mutation的进行state修改的操作都经过它,由此来统一管理监控state状态的修改。缓存执行时的 committing 状态将当前状态设置为 true 后进行本次提交操作,待操作完毕后,将 committing 状态还原为之前的状态。

//保存之前的提交状态
  _withCommit (fn) {
    /* 调用withCommit修改state的值时会将store的committing值置为true,
    内部会有断言检查该值,在严格模式下只允许使用mutation来修改store中的值,
    而不允许直接修改store的数值 */
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
}

Module - 模块管理

installModule方法 - 遍历注册mutation、action、getter、安装module

在实例的module里用到了state、mutation、action,Store的构造类除了初始化一些内部变量以外,主要执行了installModule(初始化module)以及resetStoreVM(通过VM使store“响应式”)。 installModule的作用主要是用为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。最后执行plugin的植入。

/*初始化module*/
function installModule (store, rootState, path, module, hot) {
  /* 是否是根module */
  const isRoot = !path.length
  /* 获取module的namespace */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  /* 如果有namespace则在_modulesNamespaceMap中注册 */
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    /* 获取父级的state */
    const parentState = getNestedState(rootState, path.slice(0, -1))
    /* module的name */
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      /* 将子module设置成响应式的 */
      Vue.set(parentState, moduleName, module.state)
    })
  }

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

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

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

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

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

registerModule - 注册模块

registerModule用以注册一个动态模块,也就是在store创建以后再注册模块的时候用该接口。

/* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */
  registerModule (path, rawModule, options = {}) {
    /* 转化成Array */
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
      assert(path.length > 0, 'cannot register the root module by using registerModule.')
    }

    /*注册*/
    this._modules.register(path, rawModule)
    /*初始化module*/
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // reset store to update getters...
    /* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
    resetStoreVM(this, this.state)
  }

registerModule - 注销模块

同样,与registerModule对应的方法unregisterModule,动态注销模块。实现方法是先从state中删除模块,然后用resetStore来重制store

/* 注销一个动态module */
  unregisterModule (path) {
    /* 转化成Array */
    if (typeof path === 'string') path = [path]

    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
    }

    /*注销*/
    this._modules.unregister(path)
    this._withCommit(() => {
      /* 获取父级的state */
      const parentState = getNestedState(this.state, path.slice(0, -1))
      /* 从父级中删除 */
      Vue.delete(parentState, path[path.length - 1])
    })
    /* 重制store */
    resetStore(this)
  }

getters - 获取数据状态

可以看到实例将getters做成单独的一个getters.js文件进行getter状态管理。

resetStoreVM - 绑定getter

执行完各 module 的 install 后,执行 resetStoreVM 方法,进行 store 组件的初始化,resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法:

/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */
function resetStoreVM (store, state, hot) {
  /* 存放之前的vm对象 */
  const oldVm = store._vm

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

  /* 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性 */
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  const silent = Vue.config.silent
  /* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
  Vue.config.silent = true
  /* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // 恢复Vue的模式
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* 使用严格模式,保证修改store只能通过mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

注册getter

//注册getter
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
    )
  }
}

辅助函数 - mapState, mapMutations, mapGetters, mapActions

实例APP.vue里用到了mapGetters,通过computed计算属性获取state状态。当一个组件需要获取多个状态、多个getter、多个mutations方法、多个action分发事件时候,这些辅助函数帮助我们生成计算属性,这四个辅助函数具体实现方法在helpers.js里:

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
......

插件

Vuex源码里提供了两个插件源码:devtool.js和logger.js。如果已经安装了Vue.js devtools,则会在windows对象上暴露一个VUE_DEVTOOLS_GLOBAL_HOOK。devtoolHook用在初始化的时候会触发“vuex:init”事件通知插件,然后通过on方法监听“vuex:travel-to-state”事件来重置state。最后通过Store的subscribe方法来添加一个订阅者,在触发commit方法修改mutation数据以后,该订阅者会被通知,从而触发“vuex:mutation”事件:

/* 从window对象的__VUE_DEVTOOLS_GLOBAL_HOOK__中获取devtool插件 */
const target = typeof window !== 'undefined'
  ? window
  : typeof global !== 'undefined'
    ? global
    : {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  /* devtool插件实例存储在store的_devtoolHook上 */
  store._devtoolHook = devtoolHook

  // 触发Vuex组件初始化的hook,并将store的引用地址传给devtool插件,使插件获取store的实例 
  devtoolHook.emit('vuex:init', store)

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

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

总结

Vuex是一个非常优秀的库,代码简洁,逻辑清晰。源码中还有一些工具函数类似hotUpdate、watch 以及 subscribe等,感兴趣的小伙伴们有时间可以好好研究一下源码,了解了源码设计思想和实现逻辑后,会豁然开朗,应用起来也会游刃有余。