数据存储和读取#
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#
下面看下Store中commit方法的代码。
// ! 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函数规范化参数,然后获取规范化的type和payload组成一个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的时候有两种提交方法,一种是直接提交一个由type和payload的键值对组成的mutation对象,另外一种是把它们分开来传入,unifyObjectStyle函数可以确保能获取到type和payload的准确值。
再看_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#
下面看下Store中dispatch方法的代码
// ! 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) // ! 移除子模块
}先找到需要卸载的模块名,然后使用了Module的removeChild方法,移除这个模块。
再看下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 的数据操作就基本梳理完成。