大家好,我是百慕大三角。最近阅读了vuex源码,在这里分享一下收获吧。这是本人第一次独立阅读源码,有什么理解错误的地方,希望大家能谅解并指正出来。十分感谢!
正确的阅读姿势
可能有些朋友要说了,阅读源码能有什么正确的姿势?不就是把github上的开源项目 clone 到本地去阅读、调试吗?我在尝试去阅读vuex源码前也是这样天真认为的(= 。=)
但其实在阅读源码方面还是有很多技巧的,以下这些都能很好的帮助到我们:
git log --reserse先看初始提交git tag检测出重要版本- 善用 git worktree
- 关注注释中的
TODO和FIXME - 学会使用
debug
那么我们可以在 github 上看到 vuex 中有好几个分支,那么我们就可以先 clone 0.3.0、0.4.0、1.0 分支的源码去阅读,去理解最核心的思想。tips:我们最好 fork 项目到自己的仓库,方便在源码上写注释做笔记。
1.0 分支的 vuex 到底是如何实现的?
在 clone 上述的几个分支后,发现这几个分支的差异并不是很大,这里就看下1.0分支的 vuex 是如何实现的。我们可以看到这个版本的 vuex 目录结构非常简单:
|-- src
| |-- plugins
| |-- devtools.js
| |-- logger.js //
| |-- index.js
| |-- override.js
| |-- utils.js // 一些通用的方法
|-- package.json
|-- ...
我们可以看到用 index.js和 override.js 就实现了vuex的核心功能, inedx.js导出 Store 类和供 Vue.use() 使用的install 方法,override.js 实现了通过 this.$store 访问 state。
override.js
我们先从比较简单的override.js文件看起:
// index.js 中导出的 install 方法
function install (_Vue) {
if (Vue) {
console.warn(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
return
}
Vue = _Vue
override(Vue)
}
/**
* override.js
* 在 Vue2.0 以上的版本中主要是通过 Vue.mixin 实现在 beforeCreate 生命周期中执行 vuexInit 方法
* 在 Vue1.0 版本中用的是 _init ,由于本人没用过vue1.0版本,在这里也就不赘述了
*/
Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
function vuexInit () {
const options = this.$options
const { store, vuex } = options
// store injection
if (store) {
this.$store = store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
...
}
我们可以看到 install 方法对 vuex 多次使用的检测。
然后获取options的 store, 如果有 store 就将赋值给 this.$store ,也就是挂载到根组件实例上。 如果 options 中没有 store 且父组件有 store 属性,就将父组件的 store 也挂载在子组件的 $store 上,实现了 state 的向下流动。
vuexInit 后面的代码就不赘述了,主要目的是支持以下代码中的写法,这种写法在vue2.0版本中已经被摒除了
export default {
vuex: {
getters: {
count: state => state.count
},
actions: actions
}
}
index.js
那么我们回到index.js 文件中,去看 Store 类内部是如何实现的:
class Store {
constructor ({
state = {},
mutations = {},
modules = {},
plugins = [],
strict = false
} = {}) {
this._rootMutations = this._mutations = mutations
this._modules = modules
const dispatch = this.dispatch
// bind dispatch to self
this.dispatch = (...args) => {
dispatch.apply(this, args)
}
this._vm = new Vue({
data: {
state
}
})
this._setupModuleState(state, modules)
this._setupModuleMutations(modules)
get state () {
return this._vm.state
}
set state (v) {
throw new Error('[vuex] Use store.replaceState() to explicit replace store state.')
}
}
}
dispatch (type, ...payload) {
let silent = false
let isObjectStyleDispatch = false
// 兼容使用对象操作的参数
if (typeof type === 'object' && type.type && arguments.length === 1) {
isObjectStyleDispatch = true
payload = type
if (type.silent) silent = true
type = type.type
}
if (typeof type !== 'string') {
throw new Error(
`[vuex] Expects string as the type, but found ${typeof type}.`
)
}
const handler = this._mutations[type]
const state = this.state
if (handler) {
this._dispatching = true
// apply the mutation
if (Array.isArray(handler)) {
handler.forEach(h => {
isObjectStyleDispatch
? h(state, payload)
: h(state, ...payload)
})
} else {
isObjectStyleDispatch
? handler(state, payload)
: handler(state, ...payload)
}
this._dispatching = false
if (!silent) {
const mutation = isObjectStyleDispatch
? payload
: { type, payload }
this._subscribers.forEach(sub => sub(mutation, state))
}
} else {
console.warn(`[vuex] Unknown mutation: ${type}`)
}
}
/**
* Attach sub state tree of each module to the root tree.
*
* @param {Object} state
* @param {Object} modules
*/
_setupModuleState (state, modules) {
if (!isObject(modules)) return
Object.keys(modules).forEach(key => {
const module = modules[key]
// set this module's state
Vue.set(state, key, module.state || {})
// retrieve nested modules
this._setupModuleState(state[key], module.modules)
})
}
/**
* Bind mutations for each module to its sub tree and
* merge them all into one final mutations map.
*
* @param {Object} updatedModules
*/
_setupModuleMutations (updatedModules) {
const modules = this._modules
Object.keys(updatedModules).forEach(key => {
modules[key] = updatedModules[key]
})
const updatedMutations = this._createModuleMutations(modules, [])
this._mutations = mergeObjects([this._rootMutations, ...updatedMutations])
}
/**
* Helper method for _setupModuleMutations.
* The method retrieve nested sub modules and
* bind each mutations to its sub tree recursively.
*
* @param {Object} modules
* @param {Array<String>} nestedKeys
* @return {Array<Object>}
*/
_createModuleMutations (modules, nestedKeys) {
if (!isObject(modules)) return []
return Object.keys(modules).map(key => {
const module = modules[key]
const newNestedKeys = nestedKeys.concat(key)
// retrieve nested modules
const nestedMutations = this._createModuleMutations(module.modules, newNestedKeys)
if (!module || !module.mutations) {
return mergeObjects(nestedMutations)
}
// bind mutations to sub state tree
const mutations = {}
Object.keys(module.mutations).forEach(name => {
const original = module.mutations[name]
mutations[name] = (state, ...args) => {
original(getNestedState(state, newNestedKeys), ...args)
}
})
// merge mutations of this module and nested modules
return mergeObjects([
mutations,
...nestedMutations
])
})
}
new Store 先是初始化了this._rootMuattions、this._modules等变量,然后通过 new Vue 将 state 代理到 this._vm.state 中。
实现 dispatch 方法(也就是正式版本中的 commit 方法),再通过 this._setupModuleState(state, modules) 和 this._setupModuleMutations(modules) 递归实现 module 上的state 树合并到 store 的 root state树上,module上的 mutations 合并到 store的 mutations 上。
下图就是new Vuex.Store()的大概流程
小结:我们可以看到1.0分支的vuex还是比较简陋的,在 Store 类中并没有 vuex3.0+ 版本的 actions 的实现、module 也没有命名空间、没有实用的辅助函数等等,但是整体流程是十分清晰,简单易懂的。
dev版本的vuex(3.6.2)是如何实现的
废话不多说,我们直接看目录结构:
|-- src
| |-- module
| |-- module.js
| |-- module-collection.js
| |-- plugins
| |-- devtools.js
| |-- logger.js
| |-- helpers.js
| |-- index.cjs.js
| |-- index.js
| |-- index.mjs
| |-- minxin.js
| |-- store.js
| |-- util.js
|-- package.json
|-- ...
index.js
我们先从index.js文件看起
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
export {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
index 使用了 export default 和 export 两种方式导出方法。提供了多元化的引入方式
mixin.js
我们知道 install 方法也是定义在 store.js 的,
// store.js
import applyMixin from './mixin'
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
mixin.js 就跟1.0版本中 override.js 作用是一样的,都是为了能在 Vue 的上下文环境中使用this.$store ,实现方式也是大同小异,就不多说了
// mixin.js
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// vue 1.0 版本的向后兼容
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
// 跟1.0版本有区别的是,传入的 store 可以是 function ,只要返回的值是 store 对象
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
store.js
现在我们来看最核心的 store.js
export class Store {
constructor (options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
if (__DEV__) {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `store must be called with the new operator.`)
}
const {
plugins = [],
strict = false
} = options
// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue() // 使用 $watch 监听 state 的变化
this._makeLocalGettersCache = Object.create(null)
// bind commit and dispatch to self
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
// strict mode
this.strict = strict
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
// 初始化根 module 同时递归的注册所有子 module
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
// 初始化 store 实例,同时将 _wrappedgetter 注册为 computed 属性
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
}
在new Store()开始阶段,主要对设置初始变量的值,我们着重看this._modules = new ModuleCollection(options)方法,
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
// 注册根 module
console.log('rawRootModule', rawRootModule)
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
if (__DEV__) {
assertRawModule(path, rawModule)
}
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
// 获取当前 module 的 parent 的 store
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
// 注册嵌套 modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
这些代码的主要功能是将传入的 options 转换成一个 module 对象并赋值给 this.module
此时整个 Store 的属性为:
{
_actions: {}
_mutations: {},
_module: ModuleCollection,
_modulesNamespaceMap: {},
_wrappedGetters: [],
...
}
installModule 方法
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
// 如果 module 之前被注册过就会报错
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
//
store._modulesNamespaceMap[namespace] = module
}
// 非 root module 设置 state
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('.')}"`
)
}
}
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
// 遍历注册 mutations
module.forEachMutation((mutation, key) => {
// 以 shopping-cart 为例子就是 cart/${key}
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 遍历注册 actions
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 遍历注册 gettter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 遍历注册 module._children 里的模块
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
function registerMutation (store, type, handler, local) {
// 如果 store._mutations[type] 没有为null, 就初始化 this._mutations[type] 并赋值 []
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
function registerAction (store, type, handler, local) {
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)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
在 registerAction 的时候做了一些处理,一是对 action 方法的第一个参数的格式化,二是判断 action 方法是否返回的是一个 Promise 对象,如果不是就给该 actions 方法包上一层Promise
此时整个 Store 的属性为:
{
_actions: {
'actionType': fn
'moduleName/actionType': fn,
...
}
_mutations: {
'mutationType': fn,
'moduleName/mutationType': fn
},
_module: ModuleCollection,
_modulesNamespaceMap: {
'moduleName/': Module,
...
},
_wrappedGetters: {
'moduleName/getter': fn,
...
}
...
}
可以看到 _module 和 _module._children 里的module 都被合并到了 root module里
resetStoreVM 方法
// 可以通过 this.$store.state 访问到 this._vm._data.$$state
get state () {
return this._vm._data.$$state
}
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍历 wrapperGetters,并用computed 对象保存,使得可以通过 this.$store.getters.xxxx 访问到该 getter
// this.$store.getters.xxxx 访问到 this._vm.xxxx
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.
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent
Vue.config.silent = true
// 设置新的 store._vm 初始化,以及 getters 作为 computed 属性
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
// 禁止除 mutations handler 外修改 state
enableStrictMode(store)
}
if (oldVm) {
// 解除旧 vm 的引用,以及销毁旧的 vue 对象
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
commit 方法
commit (_type, _payload, _options) {
/**
* 检查 object 形式调用的 commit, mutation 也可以通过下面这种方式调用
* mutation({
* type: 'increment',
* amount: 10
* })
*/
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
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'
)
}
}
dispatch 方法
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)
})
})
}
我们可以看到所有改变 state 状态的都是通过 store._withCommit 方法:
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
在 vuex 中为了让状态的每次改变都很明确且可追踪,vuex 状态的所有的改变都必须在 store 的 mutations handler 中调用。
我们可以看到每次传入的修改 state 的方法被执行前都会将_committing 置为true,执行完成后再将_committing 恢复成原位。外部修改state虽然能生效,但是 _committing 变量是不会变化的,这时只要监听 state 的变化时判断 _committing 变量就能知道修改的合法性了。
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
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 })
}
到这里 vuex 的基本实现我们都大概清楚了,这些mapState, mapMutations, mapGetters, mapActions辅助函数的实现有兴趣的朋友可以自行翻看了解。
其他能学习到的知识点
利用 typeof 的安全防范机制
typeof 去检查一个没有声明过的变量结果会返回undefined而不是直接报错, 所以可以通过 typeof 去检查是否有其他模块声明的全局变量,在 devtool.js 我可以看到有以下的写法
// web 环境下 window 是全局变量,node 环境下的全局变量是 global
const target = typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}
如何防止 this 丢失
在这里我们先复习一下this的定义:
this是在执行上下文(执行环境)创建时确定的一个在执行过程中不可更改的变量。
this只在函数调用阶段确定,也就是执行上下文创建阶段进行赋值,保存在环境变量中。
实际上,this的最终指向是那个调用它的对象
在1.0分支的vuex中,我们能看到源码是这样去实现 dispatch 的:
const dispatch = this.dispatch
// bind dispatch to self
this.dispatch = (...args) => {
dispatch.apply(this, args)
}
如果我们不通过显示绑定(apply、call、bind)将 dispatch 方法的 this 绑定到 store 对象上的话,那么像下面例子中这样使用dispatch就会有问题:
let func = this.$store.dispatch
func(type, payload) // 严格模式下会指向undefined。非严格模式下,web浏览器环境指向window
// component.vue
export default {
mounted() {
let { dispatch } = this.$store
dispath(); // this 指向该组件
}
}
在vuex源码中还有很多地方使用到了该技巧,如下面等等
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
设计模式:发布-订阅者模式
在devtool.js中,我们可以看到有以下的代码:
const hook =
typeof window !== 'undefined' &&
window.__VUE_DEVTOOLS_GLOBAL_HOOK__
hook.emit('vuex:init', store)
hook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
devtool插件中应该用到了发布-订阅者模式,通过on去订阅事件,emit通知变化。Vue中的EventBus就是一个很典型的订阅发布者模式,在这里我们也简单实现以下:
class Vue {
constructor() {
this.subs = {}
}
$on(type, fn)() {
if(!this.subs[type]){
this.subs[type] = []
}
this.subs[type].push(fn)
}
$emit(type, ...args) {
if(this.subs[type]){
this.subs[type].forEach((fn) => {
fn(...args)
})
}
}
$off(type, fn) {
let fns = this.subs[type]
let index = fns.indexOf(fn)
if(index !== -1){
fns.splice(index, 1)
}
}
$once(type, fn) {
let wrapper = (...args) => {
fn(...args)
this.$off(type, wrapper)
}
this.$on(type, wrapper)
}
}