Vuex4源码解读

326 阅读4分钟

Vuex是vue官方提供的一个状态管理库,采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。自Vue3发布之初,官方也推出了匹配Vue3版本的Vuex4。本文就和大家一起看下Vuex4的设计思想和主要核心API的实现。

Vuex在面试中也是被经常问到的话题,如:

  • 组件内部为什么可以访问到Store实例?
  • vuex数据响应式原理?
  • 了解过vuex的严格模式吗?
  • mutations和actions使用的区别?

通过本篇源码的学习,上面的问题就自然清楚了。

在我们的项目应用中,我们大概以下面代码示例方式使用Vuex

// store.js
import { createStore } from 'vuex';

export default createStore({
  state: {
      num: 1
  },
  mutations: {},
  actions: {},
  modules: {}
});
// App.vue
<template>
    <div></div>
</template>

<script>
import { defineComponent, computed } from 'vue'
import { useStore } from 'vuex';
export default defineComponent({
    setup() {
        const store = useStore()
        const stateNum = computed(() => store.state.num)
        
        return {stateNum}
    }
})
</script>
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';

createApp(App).use(store).mount('#app');

1. createStore

createStore方法用来创建Store实例,之后通过调用app.use()注册全局。关于use的源码解读部分请参考之前的一篇文章# Vue3源码之初始化渲染流程解读一

// src\store.js
export function createStore (options) {
  return new Store(options)
}

createStore作为一个方法被导出,接受optios状态配置参数,内部实例一个Store类并返回。

// 只关心核心代码
export class Store {
  constructor (options = {}) {
    const {
      // 用户自定义插件
      plugins = [],
      // 严格模式
      // 在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
      strict = false,
      // 打开-关闭devtools
      devtools
    } = options

    // store internal state
    this._committing = false
    // 保存actions
    this._actions = Object.create(null)
    // actions 订阅
    this._actionSubscribers = []
    // 保存mutations
    this._mutations = Object.create(null)
    // 保存Getter
    this._wrappedGetters = Object.create(null)
    // 模块化
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    // 保存Store实例订阅
    this._subscribers = []
    this._makeLocalGettersCache = Object.create(null)
    // 是否开启开发调试工具
    this._devtools = devtools
    // this指向Store实例
    // Store实例绑定commit和dispatch方法,在options api const store = useStore()使用commit和dispatch方法
    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)
    }

    this.strict = strict
    // 获取用户声明state
    const state = this._modules.root.state

    // 注册配置module
    installModule(this, state, [], this._modules.root)

    // 初始化state
    resetStoreState(this, state)

    // 注册组件
    plugins.forEach(plugin => plugin(this))
  }

  install (app, injectKey) {
    // provide 为 composition API 中使用
    //  可以传入 injectKey  如果没传取默认的 storeKey 也就是 store
    app.provide(injectKey || storeKey, this)
    // 为 option API 中使用
    app.config.globalProperties.$store = this
  }
  // 类属性getter
  get state () {
    return this._state.data
  }
  // 修改拦截
  set state (v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

  commit (_type, _payload, _options) {
    // 检查commit调用参数方式,统一调用参数
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    // 依据commit提交参数type查找mutations配置对应处理方法
    const entry = this._mutations[type]
    if (!entry) {
      // 开发环境,为找到对应处理函数--提示
      if (__DEV__) {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    // 循环处理mutations绑定函数
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 通知订阅
    this._subscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .forEach(sub => sub(mutation, this.state))

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

  dispatch (_type, _payload) {
    // 检查dispatch调用参数方式并统一化
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    // 依据action调用参数type获取actions声明处理函数
    const entry = this._actions[type]
    if (!entry) {
      // 开发环境-未找到对应处理函数-提示
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
    // 处理订阅action
    try {
      this._actionSubscribers
        .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (__DEV__) {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }
    // 执行actions处理函数
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
    // 返回所有被触发action promise
    return new Promise((resolve, reject) => {
      result.then(res => {
        try {
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in after action subscribers: `)
            console.error(e)
          }
        }
        resolve(res)
      }, error => {
        try {
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in error action subscribers: `)
            console.error(e)
          }
        }
        reject(error)
      })
    })
  }
}

源码中我们看到,向Vue中注册Vuex插件主要提供两种访问store方法:

  • 基于provide/inject,供我们在setup中使用
  • 添加全局实例属性$store,用以在组件内部通过this访问

2. 响应式数据处理

在上面的createStore源码中,有其中一步操作用来初始化state数据,看下resetStoreState方法

import { reactive} from 'vue'
export function resetStoreState (store, state, hot) {
  // 获取就state
  const oldState = store._state
  store.getters = {}
  // 数据响应式
  store._state = reactive({
    data: state
  })
  // 开发环境下,热更新
  if (oldState) {
    if (hot) {
      store._withCommit(() => {
        oldState.data = null
      })
    }
  }
}

在这里我们看到,Vuex中的state数据响应式主要依赖vue3响应式API reactive来创建

3. useStore

useStore是针对在vue3中采用组合式api的使用方式提供的hook api

// src\injectKey.js
import { inject } from 'vue'
// 默认store key
export const storeKey = 'store'

export function useStore (key = null) {
  return inject(key !== null ? key : storeKey)
}

useStore的实现非常简单,在createStore源码中通过provide方式暴露Store实例,在组件组合式api中,通过inject获取到对应Store实例。

说明:provide/inject是vue中用以向深层组件传递数据的一种方法。provide/inject绑定的数据本身并不具有响应式,但是我们通过provide传递的数据如果为一个响应式数据,那么inject获取到的数据就是响应式数据。