Vuex3源码学习笔记之Vuex的数据操作

state 是在模块安装 installModule 函数中设置的。 通过 getNestedState 方法获取父级模块,然后把所有模块下面的 state 数据都拷贝到 root 的 state 中。

目录
  1. 数据存储和读取
    1. state
    2. getters
  2. 数据的操作
    1. commit
    2. dispatch
    3. 动态注册模块
    4. 动态卸载模块
  3. 参考资料

数据存储和读取#

state#

state是在模块安装installModule函数中设置的。

// set state
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1)); // ! 获取父级的 state
  const moduleName = path[path.length - 1]; // ! 获取模块名
  store._withCommit(() => {
    Vue.set(parentState, moduleName, module.state); // ! 设置子模块,建立父子关系,并且为响应性数据
  });
}

通过getNestedState方法获取父级模块,然后把所有模块下面的state数据都拷贝到 root 的state中。

这样用户可以直接通过store.state获取store的所有数据,但是不能直接去修改数这些据。如果修改的话,在开发环境中会报错。

// ! 获取 state,访问的是 Vue 的 data 里面的属性,触发响应式 @API
get state() {
  return this._vm._data.$$state
}
 
// ! 设置 state,开发环境会报错,不能直接设置,必须使用 replaceState 替换
set state(v) {
  if (process.env.NODE_ENV !== 'production') {
    assert(false, `use store.replaceState() to explicit replace store state.`)
  }
}

因为把state的数据放入到 Vue 的data属性下面的$$state中,state数据变成响应式的数据。

另外,其实可以通过 APIreplaceState来修改数据,即使用store.replaceState(state)方法替换state

// ! 替换 state 的数据,这里也需要显示提交 commit 进行数据修改
replaceState(state) {
  this._withCommit(() => {
    this._vm._data.$$state = state
  })
}

getters#

store.getters属性是在初始化_vm的函数resetStoreVM中才开始设置的,并且通过computed对象,把它的值变成了 Vue 实例的计算属性。

store.getters = {}; // ! 创建 getters 属性
const wrappedGetters = store._wrappedGetters; // ! 获取 wrappedGetters 对象
const computed = {}; // ! 设置计算属性对象
// ! 遍历 wrappedGetters
forEachValue(wrappedGetters, (fn, key) => {
  // use computed to leverage its lazy-caching mechanism
  // direct inline function use will lead to closure preserving oldVm.
  // using partial to return function with only arguments preserved in closure environment
  // ! 把 wrappedGetter 函数的返回值赋值到 VM 的 computed 中
  computed[key] = partial(fn, store); // ! fn(store) -> wrappedGetter(store)
  // ! 定义 store.getters 的属性
  // ! store.getters.xxx -> store._vm[xxx] -> store._vm.computed[xxx]
  Object.defineProperty(store.getters, key, {
    get: () => store._vm[key],
    enumerable: true, // for local getters
  });
 
  // ...
 
  store._vm = new Vue({
    data: {
      $$state: state, // !  store.state -> store._vm.data.$$state
    },
    computed, // ! store._vm.computed[xxx] -> store._vm[xxx]
  });
 
  // ...
});

store.getters设置为 Vue 实例中的计算属性,但是它比计算属性更加严格。因为getters只能读取,而不能操作,而 Vue 的计算属性是可以设置setter的。

getters变成了计算属性,也继承了计算属性的特性,它依赖于state数据,只有在state数据变化时,它的值才会改变。

数据的操作#

在 Vuex 中修改state时,必须以显示的提交commit的形式来操作,不能直接去修改,如果直接去修改,设置了strict = true后在开发环境中会报错。

commit#

下面看下Storecommit方法的代码。

// ! commit 方法 @API
commit(_type, _payload, _options) {
  // check object-style commit
  const { type, payload, options } = unifyObjectStyle(
    _type,
    _payload,
    _options
  )
 
  const mutation = { type, payload }
  const entry = this._mutations[type] // ! 获取 type 的 mutation 函数数组
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
 
  // ! 使用 commit 执行 mutation 时在严格模式下不会报错
  this._withCommit(() => {
    entry.forEach(function commitIterator(handler) {
      handler(payload) // ! 执行 commit 的所有函数
    })
  })
 
  this._subscribers.forEach(sub => sub(mutation, this.state)) // ! 执行 mutation 后,执行所有订阅函数
 
  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'
    )
  }
}

首先会使用unifyObjectStyle函数规范化参数,然后获取规范化的typepayload组成一个mutation对象,再通过type获取对应的函数组,然后通过_withCommit执行函数组的所有函数,注意执行时需要把payload传入。

执行完type的函数后,再执行所有的mutation订阅函数。

先看下unifyObjectStyle函数的代码

// ! 规范化 commit 和 dispatch 函数的参数
// ! e.g. commit(type: string, payload?: any, options?: Object)
// !      commit({ type: string, payload?: any }, options? Object)
function unifyObjectStyle(type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload;
    payload = type;
    type = type.type;
  }
 
  if (process.env.NODE_ENV !== "production") {
    assert(
      typeof type === "string", // ! type 必须是 string 类型
      `expects string as the type, but found ${typeof type}.`
    );
  }
 
  return { type, payload, options };
}

因为commit的时候有两种提交方法,一种是直接提交一个由typepayload的键值对组成的mutation对象,另外一种是把它们分开来传入,unifyObjectStyle函数可以确保能获取到typepayload的准确值。

再看_withCommit方法的代码。

// ! 包装 mutation 函数,设置 _committing 状态
// ! 正常通过 commit 修改数据前 _committing 为 true,防止用户随意更改 vuex 的数据
_withCommit(fn) {
  const committing = this._committing // ! 缓存原来的状态
  this._committing = true // ! 执行前设置为 true,此时通过 commit 修改值在严格模式下不会报错
  fn() // ! 执行函数
  this._committing = committing // ! 恢复原来的状态
}

这里把mutation函数进行了一层包装,在执行函数前会把_committing设置为true,这样后面通过store._vm实例中的侦听器$watch检测state变化时,会验证它的值。当它的值为true时,说明修改state的值是通过提交commit修改的,这时设置strict=true后就不会报错了。

dispatch#

下面看下Storedispatch方法的代码

// ! 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 (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
 
    // ! action 执行前,先执行 action 的所有订阅器的 before 函数
    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)
      }
    }
 
    const result =
      entry.length > 1
        ? Promise.all(entry.map(handler => handler(payload))) // ! 并行执行 action 的所有异步操作
        : entry[0](payload) // ! 只有一个 action 则同步执行
 
    // ! action 执行后,执行 action 的所有订阅器的 after 函数
    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相似,都是先规范化参数,然后再执行函数,也会执行订阅器。

但是这里有两点不同,其中一点是执行action函数的方式。我们知道通过调用commit方法执行mutation函数时,是同步执行的。但是通过dispatch执行action函数时,如果有多个函数,会进行异步执行,只有一个函数时才会使用同步执行。

另一点是actions的订阅器也和mutation的订阅器不一样,它可以设置两个执行函数,一个是before,在action函数执行前执行的。另外一个after,在action函数执行后执行的。

另外,action函数是不能直接修改数据的,它还是需要通过显示的提交commit来执行mutation函数,通过mutation函数来修改数据。

动态注册模块#

Vuex中模块的注册是在初始化Store实例的时候完成的,如果我们需要在实例创建之后再创建一个模块,就可以使用registerModule方法,动态注册一个模块。

看下动态注册模块函数registerModule的代码

// ! 动态注册模块,在模块初始化后手动注册模块 @API
registerModule(path, rawModule, options = {}) {
  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) // ! 注册模块
 
  // ! 安装模块
  installModule(
    this,
    this.state,
    path,
    this._modules.get(path),
    options.preserveState
  )
  // reset store to update getters...
  // ! 初始化 store._vm
  resetStoreVM(this, this.state)
}

先处理下path参数,一般情况下都是输入字符串类型的模块名,这里需要把它转换成数组类型的path

动态注册模块和初始化时注册模块逻辑唯一不同的是,它不需要重新去创建根模块。

在初始化Store实例的时候,是需要先收集所有的模块,创建一个ModuleCollection实例对象的,把这个对象赋值给Store类的_modules属性,实际上就是创建了一个{root: Module instance}结构的根模块对象。

因为根模块是所有模块的父模块,动态注册一个子模块时,就不需要再创建重新根模块。而是只注册这个字模块,把子模块添加到根模块下面。然后需要安装这个子模块,把模块的原始数据都添加到store的实例属性中,最后初始化store._vm,更新getters属性。上面的按照模块和初始化store._vm的方法已经初始化Store实例对象中学过了,这里就不再赘述了。

store实例中既然有动态注册模块的方法,那么也会有动态卸载模块的方法。

动态卸载模块#

查看unregisterModule方法的代码

// ! 动态卸载模块 @API
unregisterModule(path) {
  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(() => {
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1]) // ! 删除 state 在该路径下的引用
  })
  resetStore(this) // ! 重置 store
}

首先还是处理下path参数,然后使用ModuleCollection类的unregister方法,把该模块从根模块中删除掉,然后把它的state数据也从根的state中删除,这里删除state使用了_withCommit的方法,在strict=true时不会报错,最后重置下store

先看下删除模块的方法unregister的代码。

// ! 模块注销的方法
unregister(path) {
  const parent = this.get(path.slice(0, -1))
  const key = path[path.length - 1]
  if (!parent.getChild(key).runtime) return
 
  parent.removeChild(key) // ! 移除子模块
}

先找到需要卸载的模块名,然后使用了ModuleremoveChild方法,移除这个模块。

再看下resetStore函数的代码。

// ! 重置 store:先清空,再重新安装模块和初始化 VM
function resetStore(store, hot) {
  // ! 重新设置值为空对象
  store._actions = Object.create(null);
  store._mutations = Object.create(null);
  store._wrappedGetters = Object.create(null);
  store._modulesNamespaceMap = Object.create(null);
  const state = store.state;
  // init all modules
  installModule(store, state, [], store._modules.root, true);
  // reset vm
  resetStoreVM(store, state, hot);
}

先重置了一些在模块安装时设置的属性,把它们的值清空成一个空对象,然后重新获取 root 的state的值,再重新安装其他未删除的模块和初始化store._vm属性。

这样,Vuex 的数据操作就基本梳理完成。

参考资料#