花了半天时间,缕清了vuex的源码实现

428 阅读1分钟

vuex的初始化

  • vuex通过export default一个object{},里面包含了许多的对象,其中就包含了Store,这是是一个构造函数,用于接收配置的state,mutation之类的参数,使用createStore都会重新new一个store对象,所以不能重复new。
// 这里必定暴露一个install的方法,因为vuex本身是一个对象,并不是一个function--构造函数内部有一个install方法,并且还在定义了$store为全局用法。
import { createApp } from 'vue'
import { createStore } from 'vuex'

// 创建一个新的 store 实例,
const store = createStore({
  state () {
    return {
      count: 0
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

const app = createApp({ /* 根组件 */ })

// 将 store 实例作为插件安装
app.use(store)

devtools,vuex面板

  • 在install的同时添加到vue开发面板上面,同步实时观察数据的变化,获得其快照。为某个特定的 Vuex 实例打开或关闭 devtools。对于传入 false 的实例来说 Vuex store 不会订阅到 devtools 插件。对于一个页面中有多个 store 的情况非常有用。
const useDevtools = this._devtools !== undefined
  ? this._devtools
  : __DEV__ || __VUE_PROD_DEVTOOLS__

if (useDevtools) {
  addDevtools(app, this)
}

ModuleCollection收集默认模块,进行命名区分

  • 通过ModuleCollection来注册模块,包括内嵌的模块,进行命名区分。分辨各个模块的数据。

  • 通过new Module(rawModule, runtime)来初始化每个模块的内容,通过判断path的length来辨别是否是根级模块,后续通过判断rawModule.modules来识别当前模块是否包含子模块,如果包含子模块,则循环该子模块进行重新register.

register (path, rawModule, runtime = true) {
  if (__DEV__) {
    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)
  }

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

installModule安装模块

  • 将通过ModuleCollection收集到的命名模块进行初始化,同时也将递归其子模块并将其初始化。

  • 每层modul都会有context属性,是将当前module里面的所有数据都集合放在context存储。

const local = module.context = makeLocalContext(store, namespace, path)
  • 通过对当前空间的module进行遍历注册,forEachMutation,forEachAction,forEachGetter分别注册旗下的三大属性,对于还有子module的还需通过forEachChild重新遍历子module,进行调用installModule进行初始化。

  • 下面以mutation为案例,说明mutation里面的方法的传参是如何使用的。

mutation: {
  test (state, payload) {
    // state为当前module里面的state数据,可以直接对state进行修改
    // payload则是传进的参数
  }
}

/*** 源码解析
 * @params {Object} store - 当前store的实例
 * @params {String} type - 命名空间类型
 * @params {Function} handler - 对应的mutation里面的函数
 * @params {Object} local - 当前的处理过的context
 **/
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) // 这里可以知道mutation里面的方法的入参定义
  })
}

commit的提交使用

  • commit的提交方式有两种,具体如下
// 第一种,第一个参数是type,第二个参数就是payload
store.commit('increment', {
  amount: 10
})

// 第二种,把type和payload都放在了一个object里面
store.commit({
  type: 'increment',
  amount: 10
})

// 源码里对着两种方式都做了兼容,第二种是为了更加贴切用户的使用习惯
export function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) { // 当type是对象的时候,会进行额外的处理
    options = payload
    payload = type
    type = type.type
  }
  return { type, payload, options }
}
  • 如果想要在带命名空间的模块内访问全局内容,可以在options里面添加root = true
commit('rootMutation', null, { root: true }) // -> 'rootMutation',直接访问顶级的方法

// 源码
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
  const args = unifyObjectStyle(_type, _payload, _options)
  const { payload, options } = args
  let { type } = args
  // 这里判断options里面的root是否为true,如果不为true的话,就使用命名空间的方式获取方法名。
  if (!options || !options.root) {
    type = namespace + type
    if (__DEV__ && !store._mutations[type]) {
      console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
      return
    }
  }

  store.commit(type, payload, options)
}

通过resetStoreState对数据进行监听

  • 通过vue3里面的reactive方法将store里面的state全部变成响应式的数据
store._state = reactive({
  data: state
})
  • 把所有的getters都收集到this._wrappedGetters里面,getters还是使用Object.defineProperty进行数据监听,只有get方法,没有set方法,不允许通过getters修改数据。
scope.run(() => {
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldState.
    // using partial to return function with only arguments preserved in closure environment.
    computedObj[key] = partial(fn, store)
    computedCache[key] = computed(() => computedObj[key]())
    Object.defineProperty(store.getters, key, {
      get: () => computedCache[key].value, // 仅仅支持get
      enumerable: true // for local getters
    })
  })
})

plugins的使用

  • vuex里面允许开发者开发自己的插件,插件只需要接收一个参数,那就是store的实例对象。开发者可以通过实例对象,对其进行修改。
plugins.forEach(plugin => plugin(this)) // 插件必须是一个函数,不像vue一样接受对象或者函数

辅助函数的使用

  • 这里举例说明一下mapMutations,辅助函数可以在vue组件里面快速访问vuex的mutation里面的方法。

  • 因为有命名空间的存在,所以先要判断是否有命名空间:

function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') { // 没有命名空间
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') { // 有命名空间,进行拼凑路径
      namespace += '/'
    }
    return fn(namespace, map)
  }
}
  • 在没有命名空间的时候,有两种方法,一种是array类型,一种是object类型:
import { mapMutations } from 'vuex'

// 没有命名空间
export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

// 这是有命名空间的
export default {
  methods: {
  ...mapmutations([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
  },
  ...mapmutations('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

// 源码
function normalizeMap (map) { // 这个方法判断是数组还是object类型,并且将其拼凑起来
  if (!isValidMap(map)) {
    return []
  }
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
  • mapMutations的源码,通过获取store里面匹配到的mutation里面方法,然后返回出去。
export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  if (__DEV__ && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) { // 这里匹配到了组件定义的方法
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})