vue2.x源码刚粗略阅读完,说实话模板编译深层的实在是一时看不完,其他的差不多了解了,想着既然都看了vue了那把全家桶都看了吧,vuex拉了源码,感觉这代码量看着就舒服多了,先把这个解决了,后续再看vue-router,vuex阅读的是3.6.2版本。
0.目录结构
代码是真不多,核心代码就更少了,感觉很适合刚学会看源码的小白,最主要的是store.js中的Store构造函数
1.入口
index.js
老规矩,先看咱们使用的是import拿到的是什么东西
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'
// 默认导出
export default {
// store构造函数
Store,
// Vue.use()的时候执行函数
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
// 按需导出
export {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
2.Vue.use(Vuex)
先看一下Vuex官网给出的开始方式
import Vue from 'vue'
import Vuex from 'vuex'
// 注册插件
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
通过阅读Vue源码,或者文档我们知道调用Vue.use()时
- 参数是函数,直接执行他
- 参数是对象的时候,执行对象中
install方法
install
// src/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 = _Vue
applyMixin(Vue)
}
mixin
对Vue1.x和2.x以上的版本做了判断,可能是1.x没有Vue.mixin这个静态方法?这里不做深究,主要讨论2.x及以上版本
- 混入了一个
beforeCreate的钩子函数 - 函数将会在
new Vue(options)时拿到中的options参数中的store并挂载到了vm实例上vm.$store
// src/mixin.js
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
function vuexInit () {
const options = this.$options
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
}
}
}
3.new Store()
Store类
constructor
- 初始化内部使用的一些变量
- 用
call重新封装了dispatch、commit方法,使得调用的时候强制this指向当前实例 - 通过参数对象生成一个模块树,返回收集模块时的对象,该对象将暴露一个
root属性 - 初始化
modules - 判断是否
strict,在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误 resetStoreVM:让state和getter响应式- 注册插件
devtools开关
export class Store {
constructor (options = {}) {
// ...
const {
plugins = [],
strict = false
} = options
// 初始化一些内部使用的变量
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
// 该创建的对象包含一个root属性,指向了模块树的根
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// 这里封装了dispatch和commit方法,使得this强制指向store
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)
}
// 严格模式
this.strict = strict
// 拿到根节点的state,这一步构建好了一棵树
const state = this._modules.root.state
// 将树递归遍历,挂载到store的私有属性,可以点运算符访问
installModule(this, state, [], this._modules.root)
// 使得数据响应式
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
// 开启devtools开关
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
}
ModuleCollection类
我们知道store是可以用模块化来创建的,如何通过输入的options对象来创建模块化的store呢,首先应该有一个Module类,然后需要有一个收集的过程,就是通过这个Modulecollection类
构造函数
constructor: 调用register从options根开始递归注册 方法get(path): path是一个数组,记录着变量访问的key值,这里巧妙的运用了Array.prpototype.reduce方法来迭代寻找值getNamespace:通过模块路径生成一个用/分隔的命名空间updateregister: 注册模块,实际上是一个生成树的过程,通过原始对象不断递归遍历,new Module生成节点,再将其添加到父节点上unregisterisRegistered
// src/moduel/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
// 根options
this.register([], rawRootModule, false)
}
get (path) {
// this.root实际上是整个参数元素,通过path中存在的key值来迭代找到path所在模块对象
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
update (rawRootModule) {
update([], this.root, rawRootModule)
}
register (path, rawModule, runtime = true) {
// ...
// 通过当前参数对象来新建一个module实例
const newModule = new Module(rawModule, runtime)
// 路径数组是空,说明是根模块
if (path.length === 0) {
this.root = newModule
} else {
// 找到父模块
const parent = this.get(path.slice(0, -1))
// 将当前模块继承父模块
parent.addChild(path[path.length - 1], newModule)
}
// 如果当前模块下还存在嵌套模块
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 这里的key实际上是每一个modules的名字,将遍历到的模块名添加到path中作为模块路径的一部分
// rawChildModule 拿到的是modules[key]这个对象,然后递归
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
const child = parent.getChild(key)
// ...
if (!child.runtime) {
return
}
parent.removeChild(key)
}
isRegistered (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (parent) {
return parent.hasChild(key)
}
return false
}
}
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
Module类
相当于树中的一个节点,通过声明对象参数,来创建一个模块树的结点,代码非常简洁易懂,且该文件也只包含这些代码
构造函数
constructor:初始化参数runtime_children_rawModule_state属性
namespaced: 是否有单独的命名空间 方法addChildremoveChildgetChildhasChildupdateforEachChildforEachGetterforEachActionforEachMutation
// src/moduel/modules.js
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// 子模块
this._children = Object.create(null)
// 原始对象
this._rawModule = rawModule
const rawState = rawModule.state
// 保存state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
get namespaced () {
return !!this._rawModule.namespaced
}
addChild (key, module) {
this._children[key] = module
}
removeChild (key) {
delete this._children[key]
}
getChild (key) {
return this._children[key]
}
hasChild (key) {
return key in this._children
}
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
forEachChild (fn) {
forEachValue(this._children, fn)
}
forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
installModuel
- 可以通过点运算符来访问
state - 将输入的参数挂载到
store上,并统一格式
function installModule (store, rootState, path, module, hot) {
// 是否是根木块
const isRoot = !path.length
// 是否拥有命名空间
const namespace = store._modules.getNamespace(path)
// 注册命名空间
if (module.namespaced) {
// 将来我们可以通过命名空间来找到模块
store._modulesNamespaceMap[namespace] = module
}
// 设置state的访问方式
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 可以通过点运算符来访问模块
Vue.set(parentState, moduleName, module.state)
})
}
// 为下面的注册创建一个临时的上下文环境,这个环境中可以省略命名空间
// 某种程度上说是一种封装,防止下面注册的时候都要写命名空间
const local = module.context = makeLocalContext(store, namespace, path)
// 注册Mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 注册Action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 注册Getter
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 对child模块递归调用该函数
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
makeLocalContext
返回一个上下文对象包含
dispatchcommitgetterstate
dispatch和commit经过了封装,内部调用的时候会自动加上命名空间,所以书写的时候可以省略
function makeLocalContext (store, namespace, path) {
// 是否定义在全局还是有自己的命名空间
const noNamespace = namespace === ''
// 定义一个在当前命名空间下的store,可以自动加上命名空间
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)
}
}
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
registerMutation、registerAction、registerGetter
将输入的各种数据类型统一格式并注册
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)
})
}
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)
// action的返回值必须是promise
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
}
})
}
function registerGetter (store, type, rawGetter, local) {
// ...
store._wrappedGetters[type] = function wrappedGetter (store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
resetStoreVM
让store里的数据变成和vue一样响应式
- 创建一个新的
vue实例,挂载到store._vm getter变成computedstate变成data中的一个属性
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
// 对外暴露getter的访问
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
const silent = Vue.config.silent
Vue.config.silent = true
// 创建一个vue实例挂载到store._vm上
// getter -> computed
// state -> data
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// ..
}
// src/utils.js
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
小结
回顾一下我们用到的store的API是什么时候被挂载的
state:在installModule中Vue.set(parentState, moduleName, module.state)getter:在resetStoreVM中创建了vue实例挂载到store._vm再用definePrototype代理store.getterinstallModule中同时注册了mutation、action但是这两种我们并不是直接调用,而是通过commit和dispatch