前言
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。他维护着一个对象,所有的属性都是相应式的。更新任意属性都触发依赖组件发相应式更新。Vuex中只有commit 方式可以修改状态,是一个单项数据流模式。
使用
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
new Vue({
el: '#app',
store,
})
// 想要修改
methods: {
increment() {
this.$store.commit('increment') // 必须要触发commit修改state
console.log(this.$store.state.count)
}
}
代码结构
│ helpers.js // 提供mapSate,mapMutations,mapActions,mapGetters 等操作api函数
│ index.cjs.js
│ index.js // 入口文件
│ index.mjs
│ mixin.js // 混入方法,提供将store 仓库挂载到每个vue实例的方法
│ store.js // 整个代码核心,创建store对象
│ test.txt
│ util.js // 工具类
│
├─module // 模块化
│ module-collection.js //创建模块树
│ module.js
│
└─plugins // 插件
devtool.js // 开发环境工具插件,针对chorme安装 vue-tool 插件
logger.js // state状态改变日志查询
代码分析
首先我们想要使用vuex必须要先安装一下vue.use(vuex) vue会执行一个install方法。我们来看一下install方法是怎么实现的。
代码调试
首先我们来看一下examples这个文件夹打开时候会看到一个webpack配置文件webpack.config.js,在这个配置文件中添加devtool
devtool: 'source-map',
然后执行dev打包 打开examples准备的例子,如:http://localhost:8080/chat/
安装
src\store.js
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
// 安装vuex
applyMixin(Vue)
}
// 在Store这个类中有这么一段代码
// 通过script标签这种方式引入会自动安装
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.`)
}
在vuex和vuerouter都中采用了通过传递参数来或者vue实例没有通过引入的方式,这样的好处是打包的时候不会把vue打包进去使得整个体积变大
applyMixin
src\mixin.js
export default function (Vue) {
// 获取vue版本
const version = Number(Vue.version.split('.')[0])
// 在vue2版本中使用混入的方法
// 全局混入beforeCreate
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 兼容vue1版本
// 获取保存 _init方法 在vue中初始化要走这个方法
//重写init方法,配置项传入vuexinit然后再执行刚才保存的_init方法。
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
function vuexInit () {
// 在vue中执行this是vue实例
const options = this.$options
// 这个就是为什么在new vue({store})要传入stopre 会检查当前组件或则根组件有没有这个选项然后挂载store实例 给this.$store
if (options.store) { //根组件
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) { //向上传递$store,保证任何组件都可以访问到
this.$store = options.parent.$store
}
}
}
根据vue1、vue2不同的版本,vue1使用重写_init方法(注:_init方法是vue是初始化执行的方法),vue2使用全局混入beforeCreate生命周期执行vuexInit,然后挂载store实例,子组件引用父组件的store层层递归,能让我们在任意组件上都可以访问到store实例,并且store实例只实例化了一次。
初始化
接下来我们看一下store的创建
src\store.js
//最后传递给vue的是一个store的实例
//由于导出的时候是export default {Store}
//所以我们实例化的时候要实例Vuex的Store
//let store = new Vuex.Store({})
export class Store {
constructor (options = {}) {
const {
plugins = [], // 初始化插件,这个选项每次暴露出mutation ,接收唯一的参数store
strict = false // 是否开启严格模式,开始了严格模式只要不是commit触发状态发生改变就抛出错误
} = options
}
// 做一系列的初始化 所有都是私有属性
this._committing = false //是否在提交状态
this._actions = Object.create(null) // actions集合
this._actionSubscribers = [] //action订阅者集合
this._mutations = Object.create(null) // mutations集合
this._wrappedGetters = Object.create(null) // getters集合
this._modules = new ModuleCollection(options) // modules集合
this._modulesNamespaceMap = Object.create(null) // 命名空间集合
this._subscribers = [] // 监听队列,当执行commit时会执行队列中的函数
this._watcherVM = new Vue() // vue实例,vue组件用于watch监听变化 类似eventbus
this._makeLocalGettersCache = Object.create(null)
}
下面我们来看一下this._modules
src\module\module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
// 接收整个option作为参数 这个option就是我们实例化的时候的参数
// {store:xxx,mutaions:xxx,actions:xxx.modules:xxx}
// 执行注册函数 注册根模块
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
if (__DEV__) {
assertRawModule(path, rawModule)
}
// 初始化Module
const newModule = new Module(rawModule, runtime)
// 对于第一次初始化 ModuleCollection 时
// 会走第一个 if 条件,因为当前是 root
if (path.length === 0) {
this.root = newModule
} else {
//子元素把关系添加到父元素,形式关系树
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 递归注册
/*
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
state: { ... },
modules: {
a: moduleA,
b: moduleB
}
})
*/
// 把每个modules都注册一边
// 递归注册
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
src\module\module.js
export default class Module {
constructor (rawModule, runtime) {
//缓存运行时的标志
this.runtime = runtime
//创建一个空对象来保存子模块
// 缓存children
this._children = Object.create(null)
// 缓存初始化的rawModule
this._rawModule = rawModule
const rawState = rawModule.state
// 赋值state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
//下面是 获取namespaced、对_children增删改查、跟新rawModule、遍历_children、getter、action、mutation
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)
}
}
}
ModuleCollection主要将初始化传入的配置项构造为一个module对象,并递归调用注册,形成父子级
下面回到store.js初始化完毕之后开始安装模块
src\store.js
//
installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
// 判断是否是根modules
const isRoot = !path.length
// 获取namespace
// 对于 modules: {a: moduleA} 来说
// namespace = 'a/
const namespace = store._modules.getNamespace(path)
// 查重、向modulesNamespaceMap集合添加module key为namespace
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
// 设置 state
// 不是根组件 没有设置preserveState:是否要保留state
if (!isRoot && !hot) {
// 获取父级state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 得到moduleName
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)
})
}
// 重写dispatch和commit
const local = module.context = makeLocalContext(store, namespace, path)
// 注册模块中的 mutation、action 和 getter
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 这里会生成一个 _wrappedGetters 属性
// 用于缓存 getter,便于下次使用
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 递归安装模块
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
安装完成之后会调用resetStoreVM,我们来看下这个方法干了些什么
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// 设置getters
store.getters = {}
// 初始化getter缓存
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍历 _wrappedGetters
forEachValue(wrappedGetters, (fn, key) => {
// 给 computed 添加属性
computed[key] = partial(fn, store)
// 重写gettrs的get方法 store.getters.xx 实际上是访问的store._vm[xx]
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true
})
})
const silent = Vue.config.silent
// 暂时将Vue设为静默模式,避免报出用户加载的某些插件触发的警告
Vue.config.silent = true
// 设置新的storeVm,将当前初始化的state以及getters作为computed属性
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 恢复Vue的模式
Vue.config.silent = silent
// 如果开启了严格模式禁止commit以外的方式修改state
// 使用了store._vm中的watch方法深度监听
if (store.strict) {
enableStrictMode(store)
}
// 若不是初始化过程执行的该方法,将旧的组件state设置为null,强制更新所有监听者(watchers),待更新生效,DOM更新完成后,执行vm组件的destroy方法进行销毁,减少内存的占用
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
Api
commit
vuex中要想改变state只能调用commit方法
get state () {
return this._vm._data.$$state
}
set state (v) {
if (__DEV__) {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
// 直接获取state返回我们刚才创建的vue实例中的$$state 实现了相应式,如果要修改就抛出错误
// 下面我们来看一下commit方法
// 接收三个参数:mutation 的类型、参数和另外配置项
commit (_type, _payload, _options) {
// 检查参数,对commit多种形式传参经行处理
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 在_mutations集合中找到具体调用的方法
const entry = this._mutations[type]
// 如果没有找到抛出错误
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 设置_committing 回调完成之前是true 完成之后是false
// handle 就是 wrappedMutationHandler 函数
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 先拷贝一下,执行订阅函数
this._subscribers
.slice()
.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
如果想要使用异步方式去改变state,就需要调用dispatch,然后再通过调用commit来实现异步修改状态
dispatch (_type, _payload) {
// 和commit一样检查参数 然后冲actions中找到具体的方法
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()
.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)
}
}
// 因为dispatch支持异步,所以使用Promise.all来执行异步函数并且判断所有异步函数是否都已经执行完成,所以在文件最开始判断了当前环境必须支持promise就是这个原因
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)
})
})
}
mapState
src\helpers.js
export const mapState =
// 如果有namespace查看没有有/ 没有加上
// 如果没有namespace当作一个匿名 参数转化一下
normalizeNamespace((namespace, states) => {
const res = {}
// 检测states格式
if (__DEV__ && !isValidMap(states)) {
console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
}
// 格式化states 形成[{ key, val: key }]这样形式的数组
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
// 如果有命名空间则去模块中去找
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
// 最后返回结果
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
mapGetters的逻辑跟mapState类似 在mapActions和mapMutations中会返回执行dispatch或者commit的结果
总结
- state内部支持模块配置和模块嵌套如何实现的?
答:在store类中有makeLocalContext方法所有的module都会有一个local context,根据path进行匹配 - 执行dispatch触发action的时候,只需要传入type,payload,action中第一个参数store从哪获取?
答:store初始化的时候调用了registerAction然后在处理handler的时候传入
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
// 传入这些参数
- 如果区分是state是外部修改的还是通过调用commit修改的?
答:在执行commit方法的时候,会调用this._withCommit(fn),这个时候会设置_committing为true,当回调执行完毕之后再次修改为原来的值。enableStrictMode这个方法会创建一个watch,监听state如果_committing不是true修改的会抛出错误。
文章有不对的地方请大佬们指正
最后麻烦大家点一个小赞
附上几个之前写的小文链接