记Vuex--源码初体验.

992 阅读8分钟

前言

使用Vue插件Vuex也有一端时间了.手里的项目我也使用Vuex抽离了一部分Common数据 可能这就是那句 Flux 架构就像眼镜:您自会知道什么时候需要它. 之前在面试的过程中.也被问到过一些感觉挺奇怪的问题.抱着学习的态度.花了几天时间看了下源码了解总结了一下.最近还看到Vuex4也发布了.支持Vue3.具体大家可以看看官方文档.

Vuex是什么?

官方文档上写出:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。 它是"单向数据流"的模式.

单向数据流 数据源状态State的变动去驱动视图View,我们在视图View上的操作又出发了动作Actions导致了数据源状态State的变化.

  • 解决了什么? 当我们多组件状态共享时.也就是说:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态. 的时候.我们可以使用Vuex.我们通过维护一个全局的单例模式管理.这样不管组件在哪里,我们都能获取或者触发行为.将开发者的状态集中在数据的更新,而不是数据的传递上去.

面试官的问题

一些当初我被问到我觉得挺奇怪的问题.和我的一些回答.也不知道是否正确.欢迎大家在评论纠正和告诉我更好的回答.

  • Vuex的数据形式是什么样的? 我: 您说的是单向数据流嘛.我理解是一种数据驱动视图.视图变动的动作又改变了视图这一个环形的单向数据流的概念.(就把上面的环形图说了一下子.)

  • Vue是双向数据绑定的.Vuex是怎么实现的呢?单向数据流和双向绑定又有什么区别呢?

我: Vuexset state的时候执行了Vue.set()方法实现了响应式定义state.之后我想了下这个问题.Vuex主要是组件间的数据处理.而双向绑定是Vue这种MVVM类型框架的特点.感觉两种不是一种东西.我就说Vuex主要是我们组件间处理状态的时候.构建的一个状态树.而Vue的是视图层面的.之后我就把话题引导了双向绑定的实现上去了.我这边的想法就是.概念上两种没有可比性.希望大家能给我个更好的答案.

  • 我们如果不用Vuex.在window下去维护个状态管理对象不可以么?

我: ... 我当时有点懵.最后这样说的.如果状态树很小的情况下我们当然可以去用事件总线(EventBus)去解决.但是如果是大型的单页面应用的话.我们要维护的就非常大了.不易管理.而且我们也不方便去跟踪数据的动向.Vuex在源码上控制了如果不经过mutations去修改state是会报错的.通过mutationsdevtools做了记录.所以我的结论是.小型的没必要使用.可以自己维护.大型的不可以.

  • 我们的state数据为什么要在计算属性computed里面?放在data里面不行么?

我: ... 我又懵了.想了一会.放在data里面的数据是我们进入页面在created里面就定义好的.我们如果动态去更新数据.视图也不会再改变了.而computed里面的数据可以实现一个动态更新的效果.

State

Vuex使用单一状态树.每个应用下只有一个Store实例.State是唯一的数据源的存在.在Vuex中存储的状态对象必须是纯粹的(包含零个或多个key/value键值对).我们在组件中展示状态,最简单的是在计算属性computed中去获取.

const Demo = {
    template: `<div>{{ qimukakax }}</div>`
    computed: {
    	qimukakax() {
        	return this.$store.state.qimukakax
        }
    }
}

每当store中的数据发生变化的时候,会重新求取,达到更新相关联DOM的效果,个人认为这也是在计算属性中声明,而不在data中声明的原因.如果我们想要获取多个状态的时候.我们可以通过mapState辅助函数去帮助我们.这个函数返回一个对象,我们可以使用对象扩展运算符去简化的配合计算属性使用.

import { mapState } from 'vuex'

export default {
	computed: mapState({
    		qimuakakax: state => state.qimukakax
    	})
     	// 还可以这样 computed: mapState(['qimukakax'])
}

看起来这个函数既能接受对象也能接受数组,我们看下mapState的源码到底是怎么样子的:

export const mapState = normalizeNamespace((namespace, states) => {
  // 新的对象
  const res = {}
  // 判断states 是不是数组或对象.不是的话抛出Error!
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      // 获取根模块的 state 和 getters
      let state = this.$store.state
      let getters = this.$store.getters
      // 存在命名空间的话,去对应的module下去拿到 state 和 getters属性
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      // 此步操作区分了val是字符串还是函数的情况
      // qimukakax: state => this.store.state.qimukakax
      // qimukakax: 'qimukakax'
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    // 在devtools中做标记
    res[key].vuex = true
  })
  return res
})

函数首先调用了normalizeMap方法.我们再看一下这个方法:

function normalizeMap (map) {
  //  判断states 是不是数组或对象.不是的话返回空数组.
  if (!isValidMap(map)) {
    return []
  }
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

首先判断是否是数组.如果是数组.调用数组的map方法转化每个元素变成{key,val: key} 的形式.对象的话就调用Object.keys去遍历对象的每个key,之后再去转换.举个例子:

normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]

回到函数体.我们起初定义了一个空对象.在我们调用了normalizeMap之后.为新的空对象的每个元素都返回新的mappedState函数,之后根据是否在命名空间内获取到stategetters,如果val是函数.就直接调用.把当前 store 上的 stategetters 作为参数,得到返回值.否则直接取值this.$store.state[val]作为返回值.

我们虽然使用了Vuex但是我们要注意.并不是使用了就需要把所有的状态都放到Vuex中去管理.应该根据我们实际的开发状况去确定.

Getter

有时候我们要对state中的数据进行一些特殊的操作.要是多个组件都要完成此操作的话.Vuexstore中定义了getter属性.它就像计算属性一样.返回值会被缓存起来.而且只有当依赖值发生变化才会重新去计算.我们可以通过暴露的store.getter对象去访问对应的值.Getter接受两个参数:

getter : {
	// 两个参数  state 和 其他的getter属性
	doSomething: (state, getter) {
    		return getter.qimukakax.name
       }
}

我们也可以通过getter返回一个函数.并给函数传参.便于我们接下来的操作. 我们也可以通过mapGetters辅助函数去把getter映射到局部的计算属性中,同样支持数组和对象形式传递参数.我们看下源码:

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  if (__DEV__ && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      // 对val in this.$store.getters的值坐校验
      if (__DEV__ && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      // 不接受函数的形式.只接受字符串
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

我们可以看到函数的实现和mapState的实现类似.但它的val只能是字符串.并会对val in this.$store.getters的值做校验.我这里举两个例子.

import { mapGetters } from 'vuex'

export default {
	computed: {
    		...mapGetters(['qimukakax'])
    	}
    	// 等价
       // qimuakakax() {
       //	return this.$store.getters['qimuakakax']
       // }
       // ...mapGetters({
       //	qimukakax: 'didididi'
       //})
}

我们在看一下getterstore实例中的注册过程:

function registerGetter (store, type, rawGetter, local) {
  // 已经存在实例中的.不在重复记录
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 记录getter
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

registerMutation 以及 registerAction 不同,getters是不允许key重复的.从这里我们也可以看到我们的getters是接受4个参数的.接下来我们看一下它是如何存储的:

function makeLocalGetters (store, namespace) {
  // 判断如果没有getters.则创建一个新的
  if (!store._makeLocalGettersCache[namespace]) {
    const gettersProxy = {}
    const splitPos = namespace.length
    Object.keys(store.getters).forEach(type => {
      // 如果命名空间不匹配.则不操作.
      if (type.slice(0, splitPos) !== namespace) return

      // 获取本地getter名称
      const localType = type.slice(splitPos)
      
      // 为getters添加代理并存储下来.
      Object.defineProperty(gettersProxy, localType, {
        get: () => store.getters[type],
        enumerable: true
      })
    })
    store._makeLocalGettersCache[namespace] = gettersProxy
  }

  return store._makeLocalGettersCache[namespace]
}

我们在组件中可以通过this.$store.getters.qimukakax访问到对应的回调函数.其实绑定这部分的逻辑在resetStoreVM函数中.在Store的构造函数中.执行了installModule方法后,就会执行resetStoreVM方法:

// 生成一个Vue实例去管理state状态,同时将getters交给computed处理
function resetStoreVM (store, state, hot) {
  // 保留现有的store._vm
  const oldVm = store._vm

  // 在store中声明getters对象
  store.getters = {}
  // 重置本地缓存
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 遍历wrappedGetters.拿到每个getter的包装函数.partial是./until.js中的闭包执行函数.
  // 把执行结果用computed临时保存起来.接着用Object.defineProperty()为store.getters定义get方法.
  // 当我们在组件中调用this.$store
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
  // 拿到全局的Vue.config.silent的配置.之后临时设置为true.
  // 目的是为了取消这个_vm的所有日志和警告.
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 使用Vue实例来存储Vuex的state状态树
  // 用computed去缓存getters返回的值
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // 启用警告
  if (store.strict) {
    enableStrictMode(store)
  }
  // 如果旧的存在则销毁
  if (oldVm) {
    if (hot) {
      // 解除旧的_vm的引用.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

这个方法主要是重置一个私有的_vm对象,它是一个Vue的实例.这个对象会保留我们state的状态树,并利用了计算属性的方式存储了storegetters.具体的部分可以看我代码上面的注释.警告的严格模式主要是:

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

做到了监听store._vm.state的变化,通过store._committing判断state的变化是否是通过mutation.如果是外部直接修改 state,那么 store._committing 的值为 false,这样就抛出一条错误。强调一下,Vuex 中对 state 的修改只能在 mutation 的回调函数里.

Mutations

如同上面说的.更改Vuexstore中状态的唯一方法就是提交mutation.它类似于事件.每个里面都包含一个字符串的事件类型(type) 和一个回调函数(handler) . 并且我们不能直接调用handler.我们要通过调用 store.commit方法:

store.commit('qimukakax')

store.commit接收额外的参数.文档中叫做 载荷(payload) . 大多数情况下.载荷是一个对象.

store.commit('qimukakax', {name: 'qimukakax'})

我们从源码上看一下mutation的注册:

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

registerMutation是对storemutation的初始化,它接受4个参数.store为当前的Store实例.type则为mutationkey.handlermutation的回调函数.local为当前模块的上下文.我们知道mutation的作用是同步的修改当前的state,函数首先根据type去拿到对应的mutation对象数组.之后把一个mutation的包装函数push到数组中.从这里我们也可以看到.我们的mutations是接受3个参数的.接下来就等待调用的时机.在Vuex中我们是通过commit去调用的.我们看一下commit函数的定义:

  commit (_type, _payload, _options) {
    // 处理了type为object的情况
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    // 查找是否有对应的mutations不存在就输出错误信息.
    const entry = this._mutations[type]
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    // 提交mutation
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 注册监听store的mutation,和插件相关.
    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'
      )
    }
  }

我们看到函数一共接受3个参数.type表示 mutation 的类型,payload 表示额外的参数,options 表示一些配置,比如 silent 等.首先利用工具函数处理了typeobject的情况.之后判断是否存在.通过this._withCommit遍历提交commit.这里面的handler(payload)就是我们之前定义的wrappedMutationHandler(payload).相当于执行了回调函数.之后遍历this._subscribers,我们来看一下这是什么:

// 增加mutaitons的监听函数
subscribe (fn, options) {
  return genericSubscribe(fn, this._subscribers, options)
}
// 统一封装mutations、actions的监听观察者函数
function genericSubscribe (fn, subs, options) {
  if (subs.indexOf(fn) < 0) {
    options && options.prepend
      ? subs.unshift(fn)
      : subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

该方法相当于是接受一个回调函数.以及this._subscribers上.并返回一个函数.个人理解是为了配合插件记录使用的.在使用中我们要注意Mutation必须是同步函数.这里我附上一下文档中的解释:

Mutation必须是同步函数

在组件中我们可以通过commit去提交mutation.或者我们也可以使用mapMutations辅助函数将组件中的methods映射为store.commit调用.举个例子:

import { mapMutations } from 'vuex'

export default {
	methods: {
    		...mapMutations(['qimukakax']) 
             // this.qimukakax() 映射为 this.$store.commit('qimuakkax')
       }
}

我们来看一下mapMutations部分的源码:

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  // 新的对象
  const res = {}
  // 判断states 是不是数组或对象.不是的话抛出Error!
  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
})

我们可以发现实现的套路和前面两个辅助函数差不多.区别在于val,那边是commit方法.上边说到Mutation必须是同步的.当然Vuex也为我们提供了异步的解决办法.

Actions

action类似于mutation.但不同的是:

  • action提交的是mutation,而不是直接改变状态
  • action可以包含任意异步操作

action函数中接受一个和store实例有相同属性方法的context,如果我们想要调用commit可以直接解构出来使用:

actions: {
	changeData({commit}) {
    		commit('qimukakax')
    }
}

我们看一下action的注册过程都发生了什么:

function registerAction (store, type, handler, local) {
  // 根据type获取action对象数组
  const entry = store._actions[type] || (store._actions[type] = [])
  // 传入包装函数.
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 判断是否为Promise 方法在src/until.js
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    // 如果开启Vuex devtools 则捕获promise的过程.
    // 最终都返回的是res.肯定是一个Promise对象.
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

registerAction是对store中的action的初始化.它和 registerMutation 的参数一致.不同点在于action是异步的修改state.但这里指的是通过提交mutation去修改.(在Vuex中,mutation是修改state的唯一途径).首先拿到action的对象数组.之后在将一个包装函数传入数组.在handler中我们传入了上文说到的context对象.包括了各种的方法和状态.最后对函数进行Promise判断.确保最后返回的是一个Promise函数.在Vuex中我们通过dispatch这个API去调用下面我们看一下定义:

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

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

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

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

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

函数的前面部分和commit十分类似.不同点在于后边的如果entry长度为一则直接调用entry[0](payload).这就是上边定义的wrappedActionHandler(payload).以及异步的处理过程.最后都传入this._actionSubscribers监听并做了异常捕获处理.在组件中我们通过mapActions去分发.类似于mapMutations.我们这边直接看下源码:

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  // 判空
  if (__DEV__ && !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  // 处理后遍历触发dispatch
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

函数也类似于mapMutations.不同点在于是通过dispatch去映射到methods的.在实际开发中.我建议把一些接口的处理异步操作都放在actions去执行.

Modules

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿,为了解决这个问题.Vuexstore分割成模块.让每个模块有自己的state、mutation、action、getter.并支持了嵌套. 通常情况下局部状态通过context.state暴露,根节点状态通过context.rootState去暴露.在上面的描述中.我们应该都有看到namespaced: true,这是Vuex提供的为了使组件有更高的封装度和复用性.增加的命名空间.当模块被注册后.我们的所有属性都会根据命名空间自动调整命名.如果我们想要在全局命名空间内分发action和提交mutation,我们只需要将{root: true}作为第三个参数给dispatchcommit即可.在组件中使用时.我们也要加上对应的路径信息.

我们看下源码的installModule部分的实现:

// 注册各个模块的信息
function installModule (store, rootState, path, module, hot) {
  // 判断是否是根模块
  const isRoot = !path.length
  // 根据path获取命名空间
  const namespace = store._modules.getNamespace(path)
  
  // 如果当前模块设置了namespaced 或 继承了父模块的namespaced,则在modulesNamespaceMap中存储当前模块
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module
  }

  // 如果不是根模块的注册
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      // 将当前模块的state注册到父模块的state上去并且是响应式的
      Vue.set(parentState, moduleName, module.state)
    })
  }
  // 设置当前的上下文
  const local = module.context = makeLocalContext(store, namespace, path)
  // 分别注册 mutation action getter 并递归注册子模块
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  
  module.forEachAction((action, key) => {
    // 区分两种写法
    // asyncFn(context, payload) {}
    // asyncFn:{
    // 	root: true,
    //  handler(context,payload) {}
    // }
    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)
  })
}

函数接受5个参数根据语义我们大概知道前面4个的意思.第5个参数表示当动态改变modules或者热更新的时候为true.当我们递归初始化子模块的时候isRootfalse. 进入判断内.这里有getNestedState方法:

// 获取到嵌套的模块中的state
function getNestedState (state, path) {
  return path.reduce((state, key) => state[key], state)
}

根据path去寻找state上嵌套的state.计算出当前父模块的state,由于模块的 path 是根据模块的名称 concat 连接的,所以 path 的最后一个元素就是当前模块的模块名.接下来调用 this._withCommit.我们看一下这个方法.

  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }

由于我们是在修改state,Vuex中所有修改state的地方都会被_withCommit函数包装,保证在同步修改 state 的过程中this._committing的值始终为true。这样当我们观测 state 的变化时,如果 this._committing 的值不为 true,则能检查到这个状态修改是有问题的.接下来我们来看一下local是怎么定义的:

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 (__DEV__ && !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 (__DEV__ && !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
}

此函数表示若设置了命名空间则创建一个本地的commit、dispatch方法,否则将使用全局的store.

推荐大家去阅读下官方文档的Modules部分.会加深我们的理解.

Vuex4

  • 安装

Vue3一样它的安装过程有了变动.我们用createStore函数去建立一个store实例.

import { createApp } from 'vue'
import { createStore } from 'vuex'
// Create a new store instance.
const store = createStore({
  state () {
    return {
      count: 0
    }
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})
const app = createApp({ /* your root component */ })
// Install the store instance as a plugin
app.use(store)
  • 使用

setup中使用.我们可以调用useStore方法.等同于Option API中的this.$store

import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()
  }
}
  • State和Getters

我们依然利用计算属性去声明:

import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
  setup () {
    const store = useStore()
    return {
      qimuakakax: computed(() => store.state.qimukakax),
      qimukakax1: computed(() => store.getters.qimukakax1)
    }
  }
}
  • Mutations和Actions

我们直接在setup中使用钩子内部提供的commitdispatch即可

import { useStore } from 'vuex'
export default {
  setup () {
    const store = useStore()
    return {
      qimukakax: () => store.commit('qimukakax'),
      qimukakax1: () => store.dispatch('qimukakax1')
    }
  }
}
  • Ts

Vuex4移除了this.$store的全局类型.解决了issue#994.所以我们要自己加一下.

// vuex-shim.d.ts

import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'

declare module '@vue/runtime-core' {
  // Declare your own store states.
  interface State {
    count: number
  }

  interface ComponentCustomProperties {
    $store: Store<State>
  }
}

写在后面

个人学习笔记.感觉写的挺乱的😂.这也是个人第一次去尝试看下源码.并记下的一点点东西.我觉得在爬坑的路上.去看下我们经常使用的库或者框架的源码还是比较重要的.个人建议先文档了解熟悉,再慢慢啃源码. 我认为我们可以从上面学到很多的东西.编程方法,思想等等.当然不推荐我上边的这个顺序去阅读.按照入口文件部分一个个去看才是正解.

参考资料

黄轶老师的分析(就是版本有点低)

大佬的思路阅读教程

Vuex4文档

ssh大佬的公众号