首先要确认版本,vue2搭配vuex3,vue3搭配vuex4,我们这篇文章说的的vuex3。
手写源码 :gitee.com/rootegg/fro…
手写源码视频:vuex手写视频
学习视频:vuex源码分析
Vuex就是Vue的一个实例,可认为跟其他组件一样
相信看这篇文章的同学,都是对vuex有使用过一段时间,想深入了解原理,下面几点是看了源码后总结的:
-
1、vuex利用mixin混入beforeCreate生命周期实现$store挂载
-
2、vuex中state利用new Vue创建实例实现双向绑定
-
3、getter利用Object.defineProperty实现响应式,利用Vue的computed实现缓存
-
4、子模块modules默认是透明的没有命名空间限制
-
5、子模块透明模式下,state是需要有子模块层级调用,getter重名会报错且只返回最后一个的值,mutation重名是都会执行,action重名是都会执行,有命名空间后,getters\mutations\actions都按命名空间层级调用
-
6、设置strict严格模式下,只能mutation里修改state状态,外部直接修改state状态会报错错,因为有监听state深入且同步变化
-
7、action是异步,因为执行了Promise.all方法后,返回了Promise对象;mutation是同步的,直接forEach同步执行的
-
8、其他实例函数有 replaceState,watch,replaceState,subscribeAction,registerModule,unregisterModule,hasModule,hotUpdate
-
9、其他辅助函数有 mapState,mapGetters,mapMutations,mapActions,createNamespacedHelpers
后面逐条分析上面总结。
vuex使用过程
import Vue from 'vue'
import Vuex from 'vuex'
// 必须先use
Vue.use(Vuex)
// 实例化Store
const store = new Vuex.Store({
state:{},
getters:{},
mutations:{},
actions:{
actions: {
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation', null, { root: true }) // -> 'someMutation'
}
},
},
strict: true, // 【可选】开启严格模式,生产环境建议关闭
plugins: [], // 【可选】插件
modules:{ // 【可选】子模块
post:{
namespaced: true,
actions: {
someAction: {
root: true, // 子模块注册全局 action
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
},
})
// store属性给根vue实例
new Vue({
store
})
mixin混入beforeCreate实现$store挂载
从上面看到,第一步是 Vue.use(Vuex),里面是执行了什么呢?
use方法实际上就是执行了install方法,所以Vuex会导出install方法
-
1、给Vue构造函数混入了beforeCreate方法,那么我们项目里面每个vue文件都会执行这个beforeCreate方法,
特别注意:很多人搞混了认为我们整个项目就一个Vue实例就是入口main.js里面的new Vue,实际上我们每个组件,每个子组件都是一个new Vue的实例,所以你的项目里有成千上万个new Vue实例 -
2、每个组件在实例化之前都会来执行 beforeCreate 方法
-
3、上面我们在根组件山挂载了 store 对象,所以根组件会来执行
this.$store = typeof options.store ==='function' ? options.store() : options.store这句,根上就有了 $store 属性了 -
4、子组件在实例化的时候会执行
this.$store = options.parent.$store这句,因为子组件上面没有options.store会判断为false,所以就去从 parent 上级组件中获取$store属性,所以整个项目里成千上万的vue实例都是用的同一个 $store 对象 -
5、
特别的install方法可以改成一句代码,不用这么复杂,这里是不是可以用原型链方式修改Vue.prototype.$store = options.store呢? -
6、 在vue4版本中使用的provide/inject方式,就没有用beforeCreate方式了
思考:可以改为原型链方式Vue.prototype方式吗,对客户端渲染没问题。如果是服务端渲染时,那就会导致各用户store对象交叉感染,所以我想原型链方式是不行的,所以不能用原型链
export function install (_Vue) {
Vue.mixin({ beforeCreate: vuexInit })
}
function vuexInit () {
const options = this.$options
// store injection
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
}
}
state和getters实现原理
state利用new Vue创建实例实现双向绑定
getter利用Object.defineProperty实现响应式,用computed计算属性做缓存
// 获取配置的 state, getters对象
const { state, getters } = options
// getters利用Vue计算属性做缓存
const computed = {}
// getter利用define.Property实现响应式
forEachValue(getters, (fn, key) => {
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// state利用new Vue创建实例实现双向绑定
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 返回state属性可供 this.$store.state使用
get state () {
return this._vm._data.$$state
}
-
1、很明显了,state是通过new Vue实例化的data属性,所以双向绑定就成了
-
2、getter是处理方式,是
Object.defineProperty和computed
难以理解的是getter是处理方式,为什么是
Object.defineProperty和computed呢?
这里做个说明:对于为什么用Object.defineProperty呢,为什么不简单的写成下面直接属性呢
forEachValue(getters, (fn, key) => {
computed[key] = partial(fn, store)
store.getters[key] = store._vm[key]
})
因为关键在 enumerable: true 以及 Object.defineProperty 没写的默认值 configurable: false和writable: false
如果采用store.getters[key] = store._vm[key]这种方式,那么属性修改,重设,删除 都是允许的,我们就可以在外面修改getter熟属性了
然后用vuex的Object.defineProperty写法,那属性修改,重设,删除 都是被禁止的,外面不能修改getter属性,即下面这三种操作都会自动报错
this.$store.getters.getA = 123
Object.defineProperty(this.$store.getters,'getA',{
get:()=>return 123
})
delete this.$store.getters.getA
第一点为什么用Object.defineProperty就说清楚了,为了属性不被修改
第二点为什么用computed呢,因为是做缓存,如果不做缓存,我们会写成如下:
forEachValue(getters, (fn, key) => {
Object.defineProperty(store.getters, key, {
get: () => partial(fn, store),
enumerable: true // for local getters
})
})
那么如partial调用,每次页面都会执行get方法,执行partial里面的逻辑,相比用了computed计算属性就不用每次重复计算了,只会在getters中以来的state变化后才会重新计算逻辑。
strict严格模式为什么只能mutation里修改state状态
如果strict设置为true,就表示开启严格模式,默认不开启。我们先看下源码实现:
// 严格模式下才执行enableStrictMode
if (store.strict) {
enableStrictMode(store)
}
// enableStrictMode里面监听了state变化,deep: true, sync: true
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 })
}
// 断言false就报错
function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
从源码就可以看到,严格模式下会监听state深层次变化,如果state变了,此时就可能是commit改变的或者外部直接修改的state
watch的state变化,如果_committing判断到是false,就报错了
我们看看_committing是什么,因为执行mutations是commit调用的,内部封装了commit会执行_withCommit函数,先_committing置为true,再实际执行我们的mutations方法去修改state,所以上面assert(store._committing)在commit中就会为true,而外部直接修改state的情况下,没有设置_committing为true,所以严格模式下就报错了,以此达到了目的
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
为什么actions是异步而mutations是同步
我们回答 actions 和 mutations 的区别时候,都会说两点:
1、数据是从dispatch调用actions中函数,commit执行mutations函数
2、actions是异步函数,mutations是同步函数
看看源码实现过程,自然就理解了:
-
dispatch 调用Promise.all 返回 Promise,actions是数组,在多子模块下,透明模式下的actions是都挂载根
-
commit是同步forEach遍历执行,mutations是数组,在多子模块下,透明模式下的mutations是都挂载根节点下
// dispatch 调用Promise.all 返回 Promise,actions是数组,在多子模块下,透明模式下的actions是都挂载根节点下
dispatch (_type, _payload) {
const action = { type, payload }
const entry = this._actions[type]
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
resolve(res)
})
})
}
// commit是同步forEach遍历执行,mutations是数组,在多子模块下,透明模式下的mutations是都挂载根节点下
commit (_type, _payload, _options) {
const mutation = { type, payload }
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
}
modules子模块和namespaced命名空间
把结论说一下:
-
子模块modules默认是透明的没有命名空间限制
-
子模块透明模式下,state是需要有子模块层级调用,getter重名会报错且只返回最后一个的值,mutation重名是都会执行,action重名是都会执行
-
有命名空间后,getters\mutations\actions都按命名空间层级调用
-
registerModule 函数动态注册子模块
这个地方,只用看registerModule函数是怎么实现的:
function registerModule (path, rawModule, options = {}) {
// path 可以是字符串,或者路劲数组
if (typeof path === 'string') path = [path]
// rawModule 原始格式重新格式化,做成 new Module 类实例
this._modules.register(path, rawModule)
// 这里是重点,里面子模块初始化state,getters,mutations,actions
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// 重新执行new Vue,绑定state和getters
resetStoreVM(this, this.state)
}
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
const local = module.context = makeLocalContext(store, namespace, path)
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)
})
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)
})
}
const namespacedType = namespace + key 这句就是计算命名空间的,可以看到取的时候加上了命名空间
看下 registerMutation ,看到type类型后面根的数组函数,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)
})
}
plugins 插件
源码中就一句
plugins.forEach(plugin => plugin(this))
所以插件就是一个函数,store实例就是参数,通常用法都是订阅
function createLogger(store){
// 订阅mutation完成后调用
store.subscribe((mutation, state) => {
console.log(mutation)
console.log(state)
})
// 订阅mutation完成后调用
store.subscribeAction((action, state) => {
console.log(action.type)
console.log(action.payload)
})
}