这是对vuex3.x版本的源码分析。
本次分析会按以下方法进行:
- 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
- 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
- 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。
本章只讲解vuex中的Mutations,这也是vuex官网中“核心概念”的第三个。
想了解vuex中的其他源码分析,欢迎参考我发布的下列文章:
VUEX 3 源码分析——1. 理解State
VUEX 3 源码分析——2. 理解Getter
VUEX 3 源码分析——3. 理解Mutations
VUEX3 源码分析——4. 理解Action
VUEX 3 源码分析——5. 理解Module
VUEX 3 源码分析——6. 理解命名空间namespace
VUEX 3 源码分析——7. 模块的动态注册和卸载
初始化
- 在 installModule 函数中,将每个定义的mutation按key-value遍历,存入store._mutations中.
- 注意这里的每个store._mutations[k]都是一个数组,数组中的元素是一个函数(wrappedMutationHandler,接收payload参数),此函数负责执行mutation。
// store-util.js
export function installModule(store, rootState, path, module, hot) {
...
// forEachMutation方法和getter中的forEachGetter类似
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
}
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)
})
}
commit的调用
- 在Store类上,定义了commit方法。
- 调用commit方法后,按传入的参数_type从store._mutations[type]中取出所有的执行函数
- 调用_withCommit方法依次执行mutation函数
// store.js
export class Store {
...
commit(_type, _payload, _options) {
const {type, payload, options} = unifyObjectStyle(_type, _payload, _options)
const mutation = {type, payload}
const entry = this._mutations[type]
if (!entry) {...}
// _withCommit 是添加了额外的逻辑,这里先不管具体细节,等于执行entry.forEach
this._withCommit(() => {
entry.forEach(function commitItertor(handler) {
handler(payload)
})
})
}
}
// store-util.js
export function unifyObjectStyle(type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
if (__DEV__) {assert(...)}
return {type, payload, options} // 给外层赋值解构用
}
至此,可以对官网中的所有内容做相关解释:
- 为什么mutation有两种提交方式:
unifyObjectStyle 函数处理了两种提交逻辑
store.commit({
type: 'increment',
amount: 10
})
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
// store-util.js
export function unifyObjectStyle(type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
if (__DEV__) {assert(...)}
return {type, payload, options} // 给外层赋值解构用
}
- 为什么mutation必须是同步函数
mutation在内部是没有针对异步函数做特殊处理,如常见的async-awailt promise等。 - 为什么只能使用commit的形式提交mutation,而不是直接调用mutation来提交:
store初始化时没有mutation属性,只有一个_mutations数据结构,这个数据并不执行mutation,只是存储相关的函数。
另外,我们还能通过源码发现一些官网没有写出,但实际存在的功能:
- 在执行mutation时制定了上下文为store,所以必然可以使用getter和其他内容,
// store-util.js
function registerMutation(store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = []) //短路求值
entry.push(function wrappedMutationHandler(payload) {
// hander 就是我们定义的某个mutation,它使用了call指定上下文为store
handler.call(store, local.state, payload)
})
}
如何开发一个mutation的订阅插件
- 在执行commit时,将对应的mutation执行完毕后,还执行了相关的订阅函数。
- store定义了subscribe方法,方便用户来订阅某些内容,执行一些额外的操作,比如日志记录、时间旅行调试等。
- genericSubscribe 函数主要有两个功能:1. 将用户定义的额外操作(即fn)加入到this._subscribers中。2.返回一个函数,这个函数的作用是取消订阅,即将fn从this._subscribers删除。
// ./store.js
export class Store {
...
constructor(options = {}) {
this._subscribers = []
}
commit(_type, _payload, _options) {
const {type, payload, options} = unifyObjectStyle(_type, _payload, _options)
const mutation = {type, payload}
const entry = this._mutations[type]
if (!entry) {...}
this._withCommit(() => {
entry.forEach(function commitItertor(handler) {
handler(payload)
})
})
this._subscribers.slice().forEach(sub => sub(mutation, this.state))
}
subscribe(fn, options) {
return genericSubscribe(fn, this._subscribers, options)
}
}
// store-util.js
export 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)
}
}
}
- 了解了上述源码后,就可以开发一个订阅mutation的插件
- 这里也解释了为什么vuex不建议直接修改state的内容,而是通过mutation来修改。因为vuex有订阅的功能,方便用户开发额外功能或者测试调试。而如果指定修改state则不会触发这些功能。
const createLoggingPlugin = (mutationType) => {
return (store) => {
store.subscribe((mutation, state) => {
if (mutation.type === mutationType) {
console.log(111);
}
})
}
}
// 使用插件
const store = new Vuex.Store({
...
plugins: [createLoggingPlugin('increment')]
})
以上就是官网上Mutation的相关源码,下一遍会分析Mutations的实现源码。