本文章主要讲解的是 Vuex底层 是如何实现的,让大家真正的理解、读懂Vuex内部的实现机制,何谈读懂?自己动手将vuex底层手敲出来。首先,使用Vue中的响应式实现Vuex中的state
,其次,使用发布订阅来实现 mutations 和 actions,Object.defineProperty实现getters
,最后,再深究 modules 实现机制
。文章可能有些长,只要你细心阅读,一定会有所收获。
如果小伙伴们觉得对自己学习 vuex 有帮助,小编希望各位大佬能点个小 赞 哦!!!好了,话不多说,正式进入文章主题!!!
Vuex 概念
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式
。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
其实说白了 Vuex 就是一个存储状态的容器
,每个组件都可以通过 Vuex 获取共享组件的状态
,其实就是组件之间的通信过程。Vuex
的运作流程如下图所示:
Vuex 核心原理
Vuex
和Vue-Router
本质都是一样,都是Vue的一个插件,本质是一个对象
。Vuex
对象有两个属性,一个是install方法
,一个是Store类
。install方法
的作用就是将 store 实例挂载到每一个组件上
。Store
这个类中,包含commit、dispatch
等方法,在内部通过new Vue来借助vue的响应式来实现Vuex的内部的响应式
。
好了,知道 vuex内部是如何进行工作,我们正式来实现 Vuex
install方法
install方法
主要作用就是,使每个组件都挂载上$store实例
。使得每一个组件都可以通过this.$store
获取到 Vuex
里面的状态和方法。
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store/store.js'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
复制代码
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
const applyMinx = (Vue)=> {
// 每个组件都需要混入挂载$store属性
Vue.mixin({
beforeCreate:vuexInit
})
}
// 子组件的创建过程 先父后子
function vuexInit () {
const options = this.$options
// 判断当前组件是否为根组件 为根组件绑定一个$store
if(options.store) {
this.$store = options.store
} else if (options.parent && options.parent.$store) {
// 有父组件并且父组件有$store属性
// 非根组件 从父组件中获取$store 从而绑定到自己组件实例上
this.$store = options.parent.$store
}
}
复制代码
上面的代码中,使用Vue.mixin
混入,每一个组件在创建的过程中都会执行Vue.mixin里面配置的方法
,所以每一个组件都会执行一次 vuexInit方法
,每一个组件都有一个$options属性
,里面包含着组件的配置参数,因为我们在挂载store实例
的时候 只往根组件上挂载了
,所有只有根组件才有store这个属性,即$options.store
就可以作为我们判断的依据,有store属性
,则说明是根组件
,将store绑定到根组件并声明为$store
,如果没有store属性
,说明是子组件
,判断子组件有父组件并且父组件有$store属性
,则为子组件添加上$store属性
。
Store 类
import Vue from 'vue'
import Vuex from '../vuex/index.js'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 10
},
mutations: {
changeAge(state,num) {
state.age += num
}
},
getters: {
getAge(state) {
return state.age
}
},
modules: {},
actions: {
changeAge({commit},payload) {
setTimeout(()=>{
commit('getAge',payload)
},1000)
}
}
})
复制代码
import applyMinx from './mixin'
let Vue
class Store {
constructor(options) {
// 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
this.state = options.state
}
}
复制代码
// App.vue
<template>
<div id="app">
state: {{$store.state.age}}
<button @click="handle">点击+10</button>
</div>
</template>
<script>
export default {
name: 'App',
methods: {
handle() {
this.$store.state.age += 10
console.log(this.$store.state.age);
}
}
}
</script>
复制代码
上面的修改$store.state数据方式是错误的,修改store状态只能通过mutation修改,文章只是为了方便测试。
测试结果:
此时你发现,可以正常的渲染到页面上,虽然这样能够渲染到页面上,但是,此时这个数据不是响应式
的,也就是说,当我通过一个按钮点击修改store中的state的值时,此时是不会触发页面的重新渲染、数据更新的
,这也是Vuex和全局变量的区别
(面试考点)。Vuex是响应式的
,而全局变量不是响应式的
。既然说到Vuex和全局变量的区别,那么我们就简单说一下两者的区别:
Vuex 和 全局变量 的区别
- Vuex的状态存储是
响应式
。当 Vue 组件从store中读取状态的时候,若store
中的状态发生了变化,那么相应的组件也会相应地得到高效的更新
。同时不能直接修改store
中的状态,只能通过显示提交(commit)mutation来改变状态
。 - Vuex 由
统一的方法修改数据
,全局变量可以任意修改
。 - 全局变量多了会造成
命名空间污染
,vuex不会,同时vuex还解决了跨组件通信的问题
。
State
通过分析,我们知道上面的直接将 options.state
赋值给 this.state
的方法是不可取得,所以,我们需要将store中的状态设置为响应式
的,这里vuex就很巧妙的利用了vue的响应式原理
,将store中的状态存入到vue实例中作为data的一个属性
,以此来实现响应式
。代码如下:
import applyMinx from './mixin'
let Vue
class Store {
constructor(options) {
// 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
let state = options.state
this._vm = new Vue({
// 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
// 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
data() {
return {// $$ 内部状态
$$state: state
}
},
})
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时数据是响应式的
return this._vm._data.$$state
}
}
复制代码
上面代码中,使用了一个类属性访问器
,当访问Store类里面的属性
的时候,就会自动触发这个类属性访问器方法
,相当于做了一层代理,当组件访问 $store.state 属性
的时候,其实就是访问了 vue实例中的$$state
这个属性,因为,vue内部已经将data中的数据进行了数据劫持
,实现了响应式,当访问vue实例对象的时候,此时实例对象数据也会是响应式
的。
测试结果:
通过测试,我们发现,此时的数据是响应式
的了,当store状态发生改变的时候
,此时页面也会同时渲染新的值。
Getters
实现了state之后,我们继续来来实现 Vuex
的 getters 方法
。说到这里,不知道小伙伴们在使用 vuex 的 getters 的时候有没有注意到,为什么声明的 getters是一个方法
,但是当我们使用的时候却直接写属性值就可以获取到数据了
?
// 写的是方法
...
getters: {
getAge(state) {
return state.age
}
},
...
复制代码
// 获取的时候却是属性
<template>
<div id="app">
state: {{$store.state.age}}
getters: {{$store.getters.getAge}}
<button @click="handle">点击+10</button>
</div>
</template>
复制代码
写的是方法,获取的是属性,此时,你可能会想到的是Object.defineProperty
的 get方法
来进行代理,实现方法如下:
import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
constructor(options) {
// 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
let state = options.state
// 用存储store里的方法
this._getters = {}
// getters 写的方法 获取的时候是属性
// 遍历options.getters里面的key getAge是key 函数是属性value 获取方法里面的属性
Object.keys(options.getters).forEach(key => {
Object.defineProperty(this.getters,key,{
get:()=> {
return options.getters[key](this.state)
}
})
})
this._vm = new Vue({
// 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
// 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
data() {
return {// $$ 内部状态
$$state: state
}
},
})
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
}
复制代码
上面的代码中。我们在 Store类
上声明一个 _getters对象
来存储 store中的getters方法
,遍历store中的getters的key
,将其存放到_getters上
,然后使用Object.defineProperty
来进行代理,当访问getter属性
的是,此时就会触发get方法
就会自动执行options.getters对应属性的函数
。然会将其返回。
测试结果:
测试你会发现,getters 可以显示到页面中,同时也实现了 响应式
,这是因为在获取getters属性
时会触发get
,而get执行的函数包含了this.state
,所以state实现了响应式
,间接的 getters 也实现了响应式
。
但是,这里的getters是没有缓存效果
的,当依赖的值没有发生变化,此时也会执行 getters 里面的方法的。测试如下:
...
state: {
age: 10,
a: 1
},
getters: {
getAge(state) {
console.log('getAge执行了');
return state.age
}
},
...
复制代码
<template>
<div id="app">
state.age: {{$store.state.age}}
satte.a:{{$store.state.a}}
getters: {{$store.getters.getAge}}
<button @click="handle">点击a+10</button>
</div>
</template>
<script>
export default {
name: 'App',
methods: {
handle() {
// 只修改a的值
this.$store.state.a += 10
}
}
}
</script>
复制代码
age的值并没有改变,此时getters的getAge方法却一直被执行,这是因为 state的数据发生了变化
,会更新页面,并且在页面中一直获取getters的getAge的值
,此时会一直触发get方法
,重新执行 getAge方法
,这不是我们想要看到的效果。所以,我们需要对getters做缓存
。那么如何做缓存呢?回顾一些vue的computed中是不是有缓存的效果,vuex就巧妙的使用vue的computed的缓存特性
,来解决vuex getters的缓存
问题,所以这我们常说的,vuex
的getters属性
就相当于vue
中的computed属性
,vuex的state属性就相当于vue中的data,其实vuex内部通过实例一个vue
,然后getters是借助vue的computed来实现缓存
的,而state是借助vue中的data响应式来实现响应式
的。(可能面试会问到,为什么我们经常说vuex的state、getters就是vue中的data和computed,你是如何理解的)
实现 getters 缓存
import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
constructor(options) {
// console.log(options);
// 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
let state = options.state
// Object.create和{}区别:
// Object.create(null)原型链指向null {}原型链指向Object
// 将用户定义的getters存储起来
this.getters = Object.create(null)
//缓存 利用vue的computed属性 计算属性会将自己的属性放到实例上
const computed = {}
// getters 写的方法 获取的时候是属性
// 遍历options.getters里面的key getAge是key 函数是属性value 获取方法里面的属性
Object.keys(options.getters).forEach(key => {
// 缓存 通过vue计算属性实现懒加载
computed[key] = () => {
return options.getters[key](this.state)
}
Object.defineProperty(this.getters,key,{
get:()=> {
return this._vm[key]
}
})
})
this._vm = new Vue({
// 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
// 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
data() {
return {// $$ 内部状态
$$state: state
}
},
computed // 计算属性会将自己的属性放到实例上 this._vm.a其实就是获取计算属性的a
})
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
}
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
export {
Store,
install
}
复制代码
vue中的 computed属性会将自己身上的属性放到vue实例上
,可以直接通过实例.属性获取computed的值
。
测试结果:
完美,我们实现了getters的缓存效果。
Mutations
完成了getters的功能之后,我们继续来完善mutations的方法,在 vuex 内部通过发布订阅模式
,将用户定义的 mutations
和 actions
存储起来,当触发 commit
的时候就找订阅 mutations方法
,当触发 dispatch
的时候就找订阅 actions方法
。
import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
constructor(options) {
// 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
let state = options.state
// Object.create和{}区别:
// Object.create(null)原型链指向null {}原型链指向Object
// 将用户定义的getters存储起来
this.getters = Object.create(null)
//缓存 利用vue的computed属性 计算属性会将自己的属性放到实例上
const computed = {}
// getters 写的方法 获取的时候是属性
// 遍历options.getters里面的key getAge是key 函数是属性value 获取方法里面的属性
Object.keys(options.getters).forEach(key => {
// 缓存 通过vue计算属性实现懒加载
computed[key] = () => {
return options.getters[key](this.state)
}
Object.defineProperty(this.getters,key,{
get:()=> {
return this._vm[key]
}
})
})
this._vm = new Vue({
// 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
// 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
data() {
return {// $$ 内部状态
$$state: state
}
},
computed // 计算属性会将自己的属性放到实例上 this._vm.a其实就是获取计算属性的a
})
// 发布订阅模式 将用户定义的mutations和actions存储起来 当调用commit就找订阅mutations方法
// 调用dispatch就找订阅actions方法
// mutations方法
this._mutations = Object.create(null)
Object.keys(options.mutations).forEach(key => {
// 发布订阅模式
this._mutations[key] = (playload) => {
// 第一个参数指向state
// call 确保this指向永远都是当前store实例
options.mutations[key].call(this,this.state,playload)
}
})
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
commit = (type,playload) => {
// 触发commit会触发_mutations里面的方法
this._mutations[type](playload)
}
}
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
export {
Store,
install
}
复制代码
// APP.vue
<template>
<div id="app">
state.age: {{$store.state.age}}
satte.a:{{$store.state.a}}
getters: {{$store.getters.getAge}}
<button @click="handle">点击a+10</button>
<button @click="$store.commit('changeAge',5)">mustation方法</button>
</div>
</template>
复制代码
...
mutations: {
changeAge(state,num) {
state.age += num
}
},
...
复制代码
Actions
actions方法
和 mutations方法
是一样的思路,都是通过 发布订阅
将其存储起来,触发 dispatch
的时候再找订阅的 actions方法
。
import applyMinx from './mixin'
// import forEach from './utils.js'
let Vue
class Store {
constructor(options) {
// 不能直接 this.state 这样虽然可以在组件上通过$store.state能渲染数据但是数据不是响应式 需要通过new vue来实现响应式
let state = options.state
// Object.create和{}区别:
// Object.create(null)原型链指向null {}原型链指向Object
// 将用户定义的getters存储起来
this.getters = Object.create(null)
//缓存 利用vue的computed属性 计算属性会将自己的属性放到实例上
const computed = {}
// getters 写的方法 获取的时候是属性
// 遍历options.getters里面的key getAge是key 函数是属性value 获取方法里面的属性
Object.keys(options.getters).forEach(key => {
// 缓存 通过vue计算属性实现懒加载
computed[key] = () => {
return options.getters[key](this.state)
}
Object.defineProperty(this.getters,key,{
get:()=> {
return this._vm[key]
}
})
})
this._vm = new Vue({
// 在vue中定义数据,属性名有规定 如果属性名是$xxx命名 ,则不会被代理到vue实例上 存在_data中
// 如果不想通过实例this._vm.state获取 就使用$命名 通过this._vm_data获取
data() {
return {// $$ 内部状态
$$state: state
}
},
computed // 计算属性会将自己的属性放到实例上 this._vm.a其实就是获取计算属性的a
})
// 发布订阅模式 将用户定义的mutations和actions存储起来 当调用commit就找订阅mutations方法
// 调用dispatch就找订阅actions方法
// mutations方法
this._mutations = Object.create(null)
Object.keys(options.mutations).forEach(key => {
// 发布订阅模式
this._mutations[key] = (playload) => {
// 第一个参数指向state
// call 确保this指向永远都是当前store实例
options.mutations[key].call(this,this.state,playload)
}
})
// actions方法
this._actions = Object.create(null)
Object.keys(options.actions).forEach(key => {
this._actions[key] = (playload) => {
//第一个参数为this 因为这里我们经常解构出{commit}
options.actions[key].call(this, this, playload)
}
})
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
commit = (type,playload) => {
// 触发commit会触发_mutations里面的方法
this._mutations[type](playload)
}
dispatch = (type,playload) => {
this._actions[type](playload)
}
}
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
export {
Store,
install
}
复制代码
//App.vue
<template>
<div id="app">
state.age: {{$store.state.age}}
satte.a:{{$store.state.a}}
getters: {{$store.getters.getAge}}
<button @click="handle">点击a+10</button>
<button @click="$store.commit('changeAge',5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
复制代码
...
actions: {
changeAge({commit},playload) {
setTimeout(()=>{
commit('getAge',playload)
},1000)
}
}
...
复制代码
我们看到,actions 的变化在1s之后就执行了,这跟我们在 store 中写的定时器执行效果符合,此时就完成了 actions 功能
的实现。
此时,vuex 的主要实现机制就完成了,但是我们还没有实现 modules 的功能,但是 vuex 主要的逻辑已经跑通了。已经将 vuex 理解的70%了,下面我们继续来探究 modules 是如何是实现的
modules基本用法
在手写 modules 之前,我们先来了解一下在 vuex 中 modules 的基本用法
。
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 10,
a: 1
},
mutations: {
changeAge(state,num) {
state.age += num
}
},
getters: {
getAge(state) {
console.log('getAge执行了');
return state.age
}
},
actions: {
getAge({commit},playload) {
setTimeout(()=>{
commit('getAge',playload)
},1000)
}
},
modules: {
b: {
state: {
c:100
},
mutations: {
changeAge() {
console.log('b中的c更新了')
}
}
},
d: {
state: {
e:100
},
mutations: {
changeAge() {
console.log('d中的e更新了')
}
}
}
}
})
复制代码
在modules中添加,两个子模块,b模板和d模块,并且里面的 mutations方法
是和 根模块的mutations
重名的,看一下测试效果:
此时你会发现,默认情况下,当mutations重名的时候,模块之间没有作用域的
,此时函数并不会覆盖
,而是会依次将函数存放到一个数组里。当触发commit
的时候会依次执行数组里面的函数
。
此时,向d模块中又添加一个子模块e,你会发现此时的模块名字与d模块中的state下的e重名了。测试结果:
// APP.vue
<template>
<div id="app">
state.age: {{$store.state.age}}
getters: {{$store.getters.getAge}}
b模块的c {{$store.state.b.c}}
d模块的e {{$store.state.d.e}}
<button @click="handle">点击a+10</button>
<button @click="$store.commit('changeAge',5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
复制代码
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 10,
},
mutations: {
changeAge(state,num) {
state.age += num
}
},
getters: {
getAge(state) {
console.log('getAge执行了');
return state.age
}
},
actions: {
getAge({commit},playload) {
setTimeout(()=>{
commit('getAge',playload)
},1000)
}
},
modules: {
b: {
state: {
c:100
},
mutations: {
changeAge() {
console.log('b中的c更新了')
}
}
},
d: {
state: {
e:300
},
mutations: {
changeAge() {
console.log('d中的e更新了')
}
},
modules: {
e: {
state: {
g:500
},
mutations: {}
}
}
}
}
})
复制代码
此时发现,当模块名和状态名重名的时候,$store.state.d.e获取的不是d模块状态的e,而是获取的是d模下面的子模块状态。所以,建议模块名不要与状态名相同
。
向d模块添加一个getters属性,此时你会发现
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 10,
},
mutations: {
changeAge(state,num) {
state.age += num
}
},
getters: {
getAge(state) {
console.log('getAge执行了');
return state.age
}
},
actions: {
getAge({commit},playload) {
setTimeout(()=>{
commit('getAge',playload)
},1000)
}
},
modules: {
b: {
state: {
c:100
},
mutations: {
changeAge() {
console.log('b中的c更新了')
}
}
},
d: {
state: {
e:300
},
// 新增代码
getters: {
getD(state) {
return state.e += 50
}
},
mutations: {
changeAge() {
console.log('d中的e更新了')
}
},
modules: {
}
}
}
})
复制代码
// APP.vue
<template>
<div id="app">
state.age: {{$store.state.age}}
getters: {{$store.getters.getAge}}
b模块的c {{$store.state.b.c}}
d模块的e {{$store.state.d.e}}
getD: {{$store.getters.d.getD}}
<button @click="handle">点击a+10</button>
<button @click="$store.commit('changeAge',5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
复制代码
如果你直接通过模块.属性获取 getters 的值是无法获取的。我们前面说过,computed计算属性会将自己的属性放到实例上
,而且 computed
和 getters
的是一样的,也就是说,getters的属性也会直接将自己的属性直接挂载到实例上
,所以,默认情况下
我们是可以直接通过getters.属性获取
getters的值,而不需要写模块名。
上面我写法中,我们都没有加入namespaced命名空间属性,下面我们加入namespaced看一下代码会发生怎样的变化。
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 10,
},
mutations: {
changeAge(state,num) {
state.age += num
}
},
getters: {
getAge(state) {
console.log('getAge执行了');
return state.age
}
},
actions: {
getAge({commit},playload) {
setTimeout(()=>{
commit('changeAge',playload)
},1000)
}
},
modules: {
b: {
// 新增代码
namespaced: true,
state: {
c:100
},
mutations: {
changeAge() {
console.log('b中的c更新了')
}
}
},
d: {
state: {
e:300
},
getters: {
getD(state) {
return state.e += 50
}
},
mutations: {
changeAge() {
console.log('d中的e更新了')
}
},
modules: {
}
}
}
})
复制代码
使用规则:模块名/状态或方法或getters
// App.vue
<template>
<div id="app">
state.age: {{$store.state.age}}
getters: {{$store.getters.getAge}}
b模块的c {{$store.state.b.c}}
d模块的e {{$store.state.d.e}}
getD: {{$store.getters.getD}}
<button @click="handle">点击a+10</button>
<button @click="$store.commit('b/changeAge',5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
复制代码
跟之前全部都会执行的结果完成不同,当前只会执行有namespaced:true模块的mutations方法,你会发现,当为模块添加了 namespaced 为ture
之后,此时,会将模块的getters、state、mutations、actions都封装到当中作用域下
。
那么,此时我又向子模块的子模块添加命名空间,比如:我在d模块下的f模块添加了命名空间,获取值的时候是通过 d/f/changeAge获取吗?我们来测试一下看看效果:
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 10,
},
mutations: {
changeAge(state,num) {
state.age += num
}
},
getters: {
getAge(state) {
console.log('getAge执行了');
return state.age
}
},
actions: {
getAge({commit},playload) {
setTimeout(()=>{
commit('changeAge',playload)
},1000)
}
},
modules: {
b: {
state: {
c:100
},
mutations: {
changeAge() {
console.log('b中的c更新了')
}
}
},
d: {
state: {
e:300
},
getters: {
getD(state) {
return state.e += 50
}
},
mutations: {
changeAge() {
console.log('d中的e更新了')
}
},
modules: {
// 新增代码
f: {
namespaced: true,
state: {
g:700
},
mutations: {
changeAge() {
console.log('d下f中的g更新了')
}
}
}
}
}
}
})
复制代码
//App.vue
<template>
<div id="app">
state.age: {{$store.state.age}}
getters: {{$store.getters.getAge}}
b模块的c {{$store.state.b.c}}
d模块的e {{$store.state.d.e}}
getD: {{$store.getters.getD}}
<button @click="handle">点击a+10</button>
<button @click="$store.commit('d/f/changeAge',5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
复制代码
测试发现提示找不到该方法,通过d/f/changeAge是无法获取到mutation
的,此时有namespaced的子模块会往上一层父级查找,看看是否有namespaced属性,如果没有就以当前模块为一个作用域,如果有就嵌套,所以,可以直接通过 f/changeAge
的方法获取 mutations
里的方法。修改之后测试结果:
此时去除f模块的namepaced,为d模块添加namespace,
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 10,
},
mutations: {
changeAge(state,num) {
state.age += num
}
},
getters: {
getAge(state) {
console.log('getAge执行了');
return state.age
}
},
actions: {
getAge({commit},playload) {
setTimeout(()=>{
commit('changeAge',playload)
},1000)
}
},
modules: {
b: {
state: {
c:100
},
mutations: {
changeAge() {
console.log('b中的c更新了')
}
}
},
d: {
namespaced: true,
state: {
e:300
},
getters: {
getD(state) {
return state.e += 50
}
},
mutations: {
changeAge() {
console.log('d中的e更新了')
}
},
modules: {
f: {
state: {
g:700
},
mutations: {
changeAge() {
console.log('d下f中的g更新了')
}
}
}
}
}
}
})
复制代码
//App.vue
<template>
<div id="app">
state.age: {{$store.state.age}}
getters: {{$store.getters.getAge}}
b模块的c {{$store.state.b.c}}
d模块的e {{$store.state.d.e}}
getD: {{$store.getters.getD}}
<button @click="handle">点击a+10</button>
<button @click="$store.commit('d/changeAge',5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
复制代码
此时,通过 d/changeAge触发
,你会发现,执行了d的e更新了,也执行了d下的f模块g也更新了,其实这也不难理解,当模块有 namespaced
的时候,此时会为当前模块封装一个作用域
,在d模块下的子模块如果没有 namespaced属性
,则剩下的子模块全部都归属当前声明 namespaced 模块管理
。
上面说了modules的基本用法,大概总结如下:
默认情况下模块,没有作用域问题
。状态名不要和模块名相同
。默认情况下计算属性,直接可以通过getters取值
。如果增加namespaced:true 会将这个模块的属性,都封装到这个作用域下
。默认会找当前模块上是否有 namespaced,再往上查找父级是否有namespaced属性,如果有则一同算上,封装成一个作用域,没有则以当前模块为一个作用域
。
好了,知道 modules 的基本用法之后,我们现在正式手写 modules
Modules
在上面的 state
、getters
、mutations
、actions
中,我们只做成了根模块,并没有考虑到 modules
,所以,之前写的代码中是不能嵌套递归的,我们重新写一个Store类
,但是前面的工作我们也不是白费的,有了前面的知识铺垫,我们知道了内部机制之后,我们就可以很轻松的写出一个完整的Store类
,也可以很容易的理解 vuex 内部核心
。
moduleCollection类
这个类主要用来收集模块,然后将参数模块格式化为模块嵌套的树形结构,方便我们后续的操作。
//根模块
this.root = { // 模块的配置 包含当前模块的getters、mutations、actions、state
_raw:xxx,
// 子模块
_children: {
a: {
_raw: xxx,
_children: {}
state: xxx.state
},
b: {
_raw: xxx,
_children: {},
state: xxx.state
}
},
state: xxx.state
}
复制代码
好了知道将用户传入的参数格式化成以上的树形结构之后,现在着手来现实 moduleCollection
这个类。
import forEach from "./utils";
export default class ModlueCollection {
constructor(options) {
// 注册模块 []表示路径 递归注册模块
this.register([],options)
}
register(path,rootModule) {
let newModlue = {
_raw: rootModule,
_children: {},
state: rootModule.state
}
// 判断如果数组长度为了,则为根模块
if(path.length == 0) {
this.root = newModlue
}else {
// 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
let parent = path.slice(0,-1).reduce((pre, next) => {
// 逐级找父节点
return pre._children[next]
},this.root)
parent._children[path[path.length-1]] = newModlue
}
// 注册子模块
if(rootModule.modules) {
forEach(rootModule.modules,(moduleValue, moduleName)=>{
// 递归组成子模块 将模块名字和模块值传入
this.register([...path,moduleName],moduleValue)
})
}
}
}
复制代码
通过测试,我们将传入的参数整理成了嵌套的树形结构。上面的代码还是有点冗余,下面我们将代码提取出来每一个modules创建一个类。
// modules.js
export default class module {
constructor(rootModule) {
this._raw = rootModule,
this._children = {}
this.state = rootModule.state
}
// 找模块的子模块
getChild(key) {
return this._children[key]
}
// 向模块追加子模块
addChild(key, module) {
this._children[key] = module
}
}
复制代码
// module-collection.js
import forEach from "./utils";
import Module from './module.js'
export default class ModlueCollection {
constructor(options) {
// 注册模块 []表示路径 递归注册模块
this.register([],options)
}
register(path,rootModule) {
let newModlue = new Module(rootModule)
// {
// _raw: rootModule,
// _children: {}
// state: rootModule.state
// }
// 判断如果数组长度为了,则为根模块
if(path.length == 0) {
this.root = newModlue
}else {
// 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
let parent = path.slice(0,-1).reduce((pre, next) => {
// 逐级找父节点
// return pre._children[next]
return pre.getChild(next)
},this.root)
// parent._children[path[path.length-1]] = newModlue
parent.addChild(path[path.length-1], newModlue)
}
// 注册子模块
if(rootModule.modules) {
forEach(rootModule.modules,(moduleValue, moduleName)=>{
// 递归组成子模块 将模块名字和模块值传入
this.register([...path,moduleName],moduleValue)
})
}
}
}
复制代码
抽离了代码之后,效果依旧不变,但是看到思路就比较清晰了,比如 getchild
就是找子模块,addChild
就是向子模块追加模块,现在我们在每个模块的 Module 中添加每个模块的方法了。 上面将用户传入的参数递归变成树形结构之后,此时,又回到了前面我们所写的Store类中,我们需要在Store上注册所有模块
的 state
、mutations
、actions
、getters
方法。
自定义 forEachValue 方法
/**
*
* @param {*} obj 传入的对象
* @param {*} callback 回调函数 两个参数分别对应解构出对象的value 和 key
*/
const forEachValue = (obj = {}, callback) => {
Object.keys(obj).forEach(key => {
// 第一个参数是值,第二个参数是键
callback(obj[key],key)
})
}
export default forEachValue
复制代码
封装这个方法的目的就是为了方便我们获取对象的键值对
。
installModlue
installModlue方法
的作用:将创建的树形结构上的状态、方法安装到Store实例上
,才可以通过$store方式获取到对应的数据。
// module.js
import forEachValue from "./utils"
export default class module {
constructor(rootModule) {
this._raw = rootModule,
this._children = {}
this.state = rootModule.state
}
// 找模块的子模块
getChild(key) {
return this._children[key]
}
// 向模块追加子模块
addChild(key, module) {
this._children[key] = module
}
// 遍历当前模块的mutations
forEachMutations(fn) {
if(this._raw.mutations) {
forEachValue(this._raw.mutations,fn)
}
}
// 遍历当前模块的actions
forEachActions(fn) {
if(this._raw.actions) {
forEachValue(this._raw.actions,fn)
}
}
// 遍历当前模块的getters
forEachGetters(fn) {
if(this._raw.getters) {
forEachValue(this._raw.getters,fn)
}
}
// 遍历当前模块的child
forEachChild(fn) {
forEachValue(this._children,fn)
}
}
复制代码
安装所有模块的目的就是将当前模块的方法、状态都安装到当前的 Store实例身上
,前面我们已经声明了一个 module类
来创建每一个子模块,刚好,我们就可以在这个类中使用 自定义forEachValue
遍历当前类的所有 mutations
、actions
、getters
、还有子模块
,将参数作为 forEachValue函数
的callback
,方便我们在Store类通过传入的函数中进行安装。
重写 Store 类
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
let Vue
function installModule(store, rootState, path, module) {
// 收集所有模块的状态
if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
let parent = path.slice(0,-1).reduce((pre, next) => {
return pre[next]
}, rootState)
// 将属性设置为响应式 可以新增属性
// 如果本身对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
Vue.set(parent,path[path.length -1 ],module.state)
}
module.forEachMutations((mutations, type)=>{
// 收集所有模块的mutations 存放到 实例的store._mutations上
// 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
store._mutations[type] = (store._mutations[type] || [])
store._mutations[type].push((payload) => {
// 函数包装 包装传参是灵活的
// 使this 永远指向实例 当前模块状态 入参数
mutations.call(store, module.state, payload)
})
})
module.forEachActions((actions, type)=>{
store._actions[type] = (store._actions[type] || [])
store._actions[type].push((payload) => {
actions.call(store, store, payload)
})
})
module.forEachGetters((getters, key)=>{
// 同名计算属性会覆盖 所以不用存储
store._wrappedGetters[key] = () => {
return getters(module.state)
}
})
module.forEachChild((child, key)=>{
installModule(store, rootState, path.concat(key), child)
})
}
class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
console.log(this._modules);
this._mutations = Object.create(null) // 存放所有模块的mutation
this._actions = Object.create(null) // 存放所有模块的actions
this._wrappedGetters = Object.create(null) // 存放所有模块的getters
// 安装所有模块到Store实例上
let state = this._modules.root.state
// this当前实例、根状态、路径、根模块
installModule(this, state, [], this._modules.root)
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
commit = (type,playload) => {
// 触发commit会触发_mutations里面的方法
this._mutations[type](playload)
}
//
dispatch = (type,playload) => {
this._actions[type](playload)
}
}
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
export {
Store,
install
}
复制代码
经过上面的分析,我们已经树形结构的全部方法和状态都安装到了Store实例身上了,下面我们为数据增加响应式的效果。
resetStoreVm
resetStoreVm方法 作用:将状态放到Vue实例身上
,以此来现实响应式
,实现getters的缓存
。
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
// 收集所有模块的状态
if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
let parent = path.slice(0,-1).reduce((pre, next) => {
return pre[next]
}, rootState)
// 将属性设置为响应式 可以新增属性
// 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
Vue.set(parent,path[path.length -1 ],module.state)
}
module.forEachMutations((mutations, type)=>{
// 收集所有模块的mutations 存放到 实例的store._mutations上
// 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
store._mutations[type] = (store._mutations[type] || [])
store._mutations[type].push((payload) => {
// 函数包装 包装传参是灵活的
// 使this 永远指向实例 当前模块状态 入参数
mutations.call(store, module.state, payload)
})
})
module.forEachActions((actions, type)=>{
store._actions[type] = (store._actions[type] || [])
store._actions[type].push((payload) => {
actions.call(store, store, payload)
})
})
module.forEachGetters((getters, key)=>{
// 同名计算属性会覆盖 所以不用存储
store._wrappedGetters[key] = () => {
return getters(module.state)
}
})
module.forEachChild((child, key)=>{
installModule(store, rootState, path.concat(key), child)
})
}
function resetStoreVm (store, state) {
const wrappedGetters = store._wrappedGetters
const computed = {}
store.getters = Object.create(null)
// 通过使用vue的computed实现缓存 懒加载
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = ()=> {
return fn()
}
// 代理
Object.defineProperty(store.getters, key, {
get: ()=> store._vm[key]
})
})
// 将状态实现响应式
store._vm = new Vue({
data() {
return {
$$state: state
}
},
computed
})
}
class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
console.log(this._modules);
this._mutations = Object.create(null) // 存放所有模块的mutation
this._actions = Object.create(null) // 存放所有模块的actions
this._wrappedGetters = Object.create(null) // 存放所有模块的getters
// 注册所有模块到Store实例上
// this当前实例、根状态、路径、根模块
let state = this._modules.root.state
installModule(this, state, [], this._modules.root)
//实现状态响应式
resetStoreVm(this,state)
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
commit = (type,payload) => {
// 触发commit会触发_mutations里面的方法
this._mutations[type].forEach(fn => fn(payload))
}
//
dispatch = (type,payload) => {
this._actions[type].forEach(fn => fn(payload))
}
}
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
export {
Store,
install
}
复制代码
将状态放到vue实例身上之后,我们来看一下代码运行的结果: 运行效果跟一开始的情况一模一样,虽然实现了 modules 的嵌套,但是,还没有实现 namespaced命名空间
的功能。下面继来完善modules的namespaced
的模块。
namespaced
import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
constructor(options) {
// 注册模块 []表示路径 递归注册模块
this.register([],options)
}
register(path,rootModule) {
let newModlue = new Module(rootModule)
// {
// _raw: rootModule,
// _children: {}
// state: rootModule.state
// }
// 判断如果数组长度为了,则为根模块
if(path.length == 0) {
this.root = newModlue
}else {
// 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
let parent = path.slice(0,-1).reduce((pre, next) => {
// 逐级找父节点
// return pre._children[next]
return pre.getChild(next)
},this.root)
// parent._children[path[path.length-1]] = newModlue
parent.addChild(path[path.length-1], newModlue)
}
// 注册子模块
if(rootModule.modules) {
forEachValue(rootModule.modules,(moduleValue, moduleName)=>{
// 递归组成子模块 将模块名字和模块值传入
this.register([...path,moduleName],moduleValue)
})
}
}
// 新增代码
getNamespaced(path) { //获取命名空间
let root = this.root
return path.reduce((pre, next)=>{
//[b,c]
// 获取子模块 查看是否有namespaced属性
root = root.getChild(next)
// 有namespace属性就拼接上
return pre + (root.namespaced ? next + '/' :'')
}, '')
}
}
复制代码
function installModule(store, rootState, path, module) {
// 新增代码 获取命名空间
let namespaced = store._modules.getNamespaced(path)
console.log(namespaced);
// 收集所有模块的状态
if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
let parent = path.slice(0,-1).reduce((pre, next) => {
return pre[next]
}, rootState)
// 将属性设置为响应式 可以新增属性
// 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
Vue.set(parent,path[path.length -1 ],module.state)
}
module.forEachMutations((mutations, type)=>{
// 收集所有模块的mutations 存放到 实例的store._mutations上
// 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
store._mutations[namespaced + type].push((payload) => {
// 函数包装 包装传参是灵活的
// 使this 永远指向实例 当前模块状态 入参数
mutations.call(store, module.state, payload)
})
})
module.forEachActions((actions, type)=>{
store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
store._actions[namespaced + type].push((payload) => {
actions.call(store, store, payload)
})
})
module.forEachGetters((getters, key)=>{
// 同名计算属性会覆盖 所以不用存储
store._wrappedGetters[key] = () => {
return getters(module.state)
}
})
module.forEachChild((child, key)=>{
installModule(store, rootState, path.concat(key), child)
})
}
复制代码
import forEachValue from "./utils"
export default class module {
constructor(rootModule) {
this._raw = rootModule,
this._children = {}
this.state = rootModule.state
}
// 新增代码 属性访问器
get namespaced () {
return this._raw.namespaced
}
// 找模块的子模块
getChild(key) {
return this._children[key]
}
// 向模块追加子模块
addChild(key, module) {
this._children[key] = module
}
// 遍历当前模块的mutations
forEachMutations(fn) {
if(this._raw.mutations) {
forEachValue(this._raw.mutations,fn)
}
}
// 遍历当前模块的actions
forEachActions(fn) {
if(this._raw.actions) {
forEachValue(this._raw.actions,fn)
}
}
// 遍历当前模块的getters
forEachGetters(fn) {
if(this._raw.getters) {
forEachValue(this._raw.getters,fn)
}
}
// 遍历当前模块的child
forEachChild(fn) {
forEachValue(this._children,fn)
}
}
复制代码
在实现,namespaced的时候,就是在模块安装的时候遍历每个模块是否有namespaced属性
,然后再将路径拼接起来。
registerModule
Vuex 和 Vue-Router一样都支持动态的添加模块和路由,所以,我们需要在store中添加动态添加模块的 registerModule
方法。
// module-collection.js
import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
constructor(options) {
// 注册模块 []表示路径 递归注册模块
this.register([],options)
}
register(path,rootModule) {
let newModlue = new Module(rootModule)
// {
// _raw: rootModule,
// _children: {}
// state: rootModule.state
// }
// 判断如果数组长度为了,则为根模块
//映射当前module实例 为了动态添加模块
rootModule.rawModule = newModlue
if(path.length == 0) {
this.root = newModlue
}else {
// 说明path有长度 比如 [a,b] 将此模块存入到根模块的children属性中
let parent = path.slice(0,-1).reduce((pre, next) => {
// 逐级找父节点
// return pre._children[next]
return pre.getChild(next)
},this.root)
// parent._children[path[path.length-1]] = newModlue
parent.addChild(path[path.length-1], newModlue)
}
// 注册子模块
if(rootModule.modules) {
forEachValue(rootModule.modules,(moduleValue, moduleName)=>{
// 递归组成子模块 将模块名字和模块值传入
this.register([...path,moduleName],moduleValue)
})
}
}
...
}
复制代码
新增 rootModule.rawModule = newModlue
原因:在new Modlue
模块的之后,我们又为当前模块实例添加了一个属性映射到自己身上
,这是为了在动态注册模块
时,我们需要将注册好的模块安装到 Store实例
身上,安装需要 Module
实例中的遍历 mutations
、 actions
、 getters
的方法,但是添加的模块中并没有 Module实例
,所以需要在注册模块的时候为新增模块添加一个属性,映射
到当前模块实例。
// store.js
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
// 获取命名空间
let namespaced = store._modules.getNamespaced(path)
console.log(namespaced);
// 收集所有模块的状态
if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
let parent = path.slice(0,-1).reduce((pre, next) => {
return pre[next]
}, rootState)
// 将属性设置为响应式 可以新增属性
// 如果本身对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
Vue.set(parent,path[path.length -1 ],module.state)
}
module.forEachMutations((mutations, type)=>{
// 收集所有模块的mutations 存放到 实例的store._mutations上
// 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
store._mutations[namespaced + type].push((payload) => {
// 函数包装 包装传参是灵活的
// 使this 永远指向实例 当前模块状态 入参数
mutations.call(store, module.state, payload)
})
})
module.forEachActions((actions, type)=>{
store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
store._actions[namespaced + type].push((payload) => {
actions.call(store, store, payload)
})
})
module.forEachGetters((getters, key)=>{
// 同名计算属性会覆盖 所以不用存储
store._wrappedGetters[key] = () => {
return getters(module.state)
}
})
module.forEachChild((child, key)=>{
installModule(store, rootState, path.concat(key), child)
})
}
function resetStoreVm (store, state) {
// 存储上一个实例
let oldVm = state._vm
const wrappedGetters = store._wrappedGetters
const computed = {}
store.getters = Object.create(null)
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = ()=> {
return fn()
}
// 代理
Object.defineProperty(store.getters, key, {
get: ()=> store._vm[key]
})
})
// 将状态实现响应式
store._vm = new Vue({
data() {
return {
$$state: state
}
},
computed
})
// 判断是否有上一个vue实例 如果存在则下一次dom更新的时候销毁实例
if(oldVm) {
Vue.nextTick(()=> oldVm.$destoryed())
}
}
class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
console.log(this._modules);
this._mutations = Object.create(null) // 存放所有模块的mutation
this._actions = Object.create(null) // 存放所有模块的actions
this._wrappedGetters = Object.create(null) // 存放所有模块的getters
// 注册所有模块到Store实例上
// this当前实例、根状态、路径、根模块
let state = this._modules.root.state
installModule(this, state, [], this._modules.root)
// console.log(this._module);
//实现状态响应式
resetStoreVm(this,state)
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
commit = (type,payload) => {
// 触发commit会触发_mutations里面的方法
this._mutations[type].forEach(fn => fn(payload))
}
//
dispatch = (type,payload) => {
this._actions[type].forEach(fn => fn(payload))
}
// 新增代码
registerModule (path,rawModule) {
// 封装成一个数组
if(!Array.isArray(path)) path = [path]
// 注册模块
this._modules.register(path,rawModule)
// 安装模块 注册模块需要使用到模块实例上的方法 在模块收集的地方做了一个模块映射
installModule(this,this.state,path,rawModule.rawModule)
//重置getters
resetStoreVm(this, this.state)
}
}
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
export {
Store,
install
}
复制代码
安装好模块之后,此时添加模块的mutations
、actions
、state
都可以向其他模块一样正常使用了,但是getters的方法目前还不可以使用,这是因为我们是在 resetStoreVm方法
对 getters
进行处理的,所以我们还需要执行一次 resetStoreVm函数
,才可以使用 添加模块的getters
。但是,前面我们设置模块 状态响应式
的时候 执行了resetStoreVm方法
创建了一个 vue实例
,添加模块的时候 又执行resetStoreVm
,此时就有 两个vue实例
,我们需要通过 nextTick方法
在 下一次DOM更新
的时候执行 上一个实例的 $destoryed()方法
将上一个vue实例销毁掉
。
测试结果:
// store/store.js
...
store.registerModule(['h'],{
namespaced:true,
state: {
l: 9
},
getters: {
getL(state) {
return state.l
}
}
})
...
复制代码
测试发现,根模块就动态的添加了一个h模块。完成了动态注册模块。
plugins 状态持久化
vuex内部也支持插件的使用,在创建Store的时候可以注册一些插件。
import Vue from 'vue'
// import Vuex from '../vuex/index.js'
import Vuex from 'vuex'
Vue.use(Vuex)
function persists(store) {
}
const store = new Vuex.Store({
plugins: [
persists
],
state: {
age: 10,
},
})
export default store
复制代码
当执行插件的时候,内部会提供一个Store参数,就是一个Store实例,这样可以借助实例写出想要的插件。 所以现在自定义的vuex也要实现插件的功能。
class Store{
constructor(options){
....
// 存储插件
this._subscriber = []
// 遍历插件并执行
options.plugins.forEach(fn => fn(this))
....
}
// 发布订阅
subscriber(fn) {
//当状态发生变化的时候自动触发
this._subscriber.push(fn)
}
// 更新状态
replaceState(newState) {
this._vm._data.$$state = newState
}
....
// 省略后面代码
}
复制代码
import Vue from 'vue'
import Vuex from '../vuex/index.js'
// import Vuex from 'vuex'
Vue.use(Vuex)
function persists(store) {
let local = localStorage.getItem('VUEX')
if (local) {
// 每次页面刷新 从localStorage中获取最新的值 展示到页面
store.replaceState(JSON.parse(local))
}
store.subscriber((mutations,state)=>{
// 将状态存储在localStorage中
localStorage.setItem('VUEX',JSON.stringify(state))
})
}
const store = new Vuex.Store({
plugins: [
persists
],
state: {
age: 10,
},
...
// 省略后面代码
})
export default store
复制代码
// store.js
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
// 新增代码 获取最新值
function getState(store, path) {
return path.reduce((pre, next) => {
return pre[next]
},store.state)
}
function installModule(store, rootState, path, module) {
// 获取命名空间
let namespaced = store._modules.getNamespaced(path)
// 收集所有模块的状态
if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
let parent = path.slice(0,-1).reduce((pre, next) => {
return pre[next]
}, rootState)
// 将属性设置为响应式 可以新增属性
// 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
Vue.set(parent,path[path.length -1 ],module.state)
}
module.forEachMutations((mutations, type)=>{
// 收集所有模块的mutations 存放到 实例的store._mutations上
// 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
store._mutations[namespaced + type].push((payload) => {
// 函数包装 包装传参是灵活的
// 使this 永远指向实例 当前模块状态 入参数
// 实现插件 ,这里需要更新state的值 内部可能会替换状态 这里如果一直使用module.state 可能是旧的值 所以需要获取最新的值
mutations.call(store, getState(store, path), payload) //状态更改触发subscriber
// 新增代码
// 第一个参数是一个对象 记录mutations 和 触发commit名称 第二个参数是最新的状态值
store._subscriber.forEach(sub => sub({mutations,type},store.state))
})
})
module.forEachActions((actions, type)=>{
store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
store._actions[namespaced + type].push((payload) => {
actions.call(store, store, payload)
})
})
module.forEachGetters((getters, key)=>{
// 同名计算属性会覆盖 所以不用存储
store._wrappedGetters[key] = () => {
return getters(getState(store, path))
}
})
module.forEachChild((child, key)=>{
installModule(store, rootState, path.concat(key), child)
})
}
复制代码
通过上面我们自己实现了一个持久化状态的一个插件,每一页面刷新都是读取localStorage中的最新值。
区分 actions 和 mutations
vuex中在严格模式下只允许通过mutations修改状态,其他情况修改状态是不允许的并且会报错。下面,来实现这个功能:
import applyMinx from './mixin'
import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
// 获取最新值
function getState(store, path) {
return path.reduce((pre, next) => {
return pre[next]
},store.state)
}
function installModule(store, rootState, path, module) {
// 获取命名空间
let namespaced = store._modules.getNamespaced(path)
console.log(namespaced);
// 收集所有模块的状态
if(path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
let parent = path.slice(0,-1).reduce((pre, next) => {
return pre[next]
}, rootState)
// 将属性设置为响应式 可以新增属性
// 如果本事对象不是响应式的话会直接赋值,如果是响应式此时新增的属性也是响应式
store._WithCommitting(()=>{
Vue.set(parent,path[path.length -1 ],module.state)
})
}
module.forEachMutations((mutations, type)=>{
// 收集所有模块的mutations 存放到 实例的store._mutations上
// 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
store._mutations[namespaced + type].push((payload) => {
// 函数包装 包装传参是灵活的
// 使this 永远指向实例 当前模块状态 入参数
// 实现插件 ,这里需要更新state的值 内部可能会替换状态 这里如果一直使用module.state 可能是旧的值
store._WithCommitting(()=>{
mutations.call(store, getState(store, path), payload) //状态更改触发subscriber
})
// 第一个参数是一个对象 记录mutations 和 触发commit名称 第二个参数是最新的状态值
store._subscriber.forEach(sub => sub({mutations,type},store.state))
})
})
module.forEachActions((actions, type)=>{
store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
store._actions[namespaced + type].push((payload) => {
actions.call(store, store, payload)
})
})
module.forEachGetters((getters, key)=>{
// 同名计算属性会覆盖 所以不用存储
store._wrappedGetters[key] = () => {
return getters(getState(store, path))
}
})
module.forEachChild((child, key)=>{
installModule(store, rootState, path.concat(key), child)
})
}
function resetStoreVm (store, state) {
let oldVm = state._vm
const wrappedGetters = store._wrappedGetters
const computed = {}
store.getters = Object.create(null)
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = ()=> {
return fn()
}
// 代理
Object.defineProperty(store.getters, key, {
get: ()=> store._vm[key]
})
})
// 将状态实现响应式
store._vm = new Vue({
data() {
return {
$$state: state
}
},
computed
})
if(oldVm) {
Vue.nextTick(()=> oldVm.$destoryed())
}
// 开启严格模式
if (store.strict) {
store._vm.$watch(()=>store._vm._data.$$state,()=>{
console.assert(store._committing,'只能通过mutations更改状态')
},{deep:true,sync:true}) // 同步只要状态一变化会立即执行
}
}
class Store {
constructor(options) {
this._modules = new ModuleCollection(options)
console.log(this._modules);
this._mutations = Object.create(null) // 存放所有模块的mutation
this._actions = Object.create(null) // 存放所有模块的actions
this._wrappedGetters = Object.create(null) // 存放所有模块的getters
// 注册所有模块到Store实例上
// this当前实例、根状态、路径、根模块
let state = this._modules.root.state
installModule(this, state, [], this._modules.root)
// console.log(this._module);
//实现状态响应式
resetStoreVm(this,state)
this.strict = options.strict
// 插件
this._subscriber = []
// 遍历插件并执行
options.plugins.forEach(fn => fn(this))
//同步Watcher
this._committing = false
}
// 类属性访问器 当用户通过实例获取实例属性state时,会触发这个方法 相对于代理
get state() {
// 访问state相当于取this._vm._data.$$state,当访问this._vm._data.$$state的时候,它是通过new vue 生成的数据
// 此时会是响应式的
return this._vm._data.$$state
}
// 新增代码 同步Watcher 只能通过mutations更改状态
_WithCommitting(fn) {
let committing = this._committing
this._committing = true //切片 在函数调用前 标识_committing为true
fn()
this._committing = committing
}
commit = (type,payload) => {
// 触发commit会触发_mutations里面的方法
this._mutations[type].forEach(fn => fn(payload))
}
//
dispatch = (type,payload) => {
this._actions[type].forEach(fn => fn(payload))
}
registerModule (path,rawModule) {
// 封装成一个数组
if(!Array.isArray(path)) path = [path]
// 注册模块
this._modules.register(path,rawModule)
// 安装模块 注册模块需要使用到模块实例上的方法 在模块收集的地方做了一个模块映射
installModule(this,this.state,path,rawModule.rawModule)
//重置getters
resetStoreVm(this, this.state)
}
// 发布订阅
subscriber(fn) {
//当状态发生变化的时候自动触发
this._subscriber.push(fn)
}
// 更新状态
replaceState(newState) {
this._WithCommitting(()=>{
this._vm._data.$$state = newState
})
}
}
const install = (_Vue) => {
Vue = _Vue
applyMinx(Vue)
}
export {
Store,
install
}
复制代码
设置了严格模式下,需要在Store内部设置一个 _committing标识符
,如果是 mutations
修改状态,就将 _committing标识符
改为 true
,其他全部修改状态情况置为 false
。这里使用了 _WithCommitting函数
将 _committing标识
变更,在 resetStoreVm
内部判断是否开启严格模式,开启严格模式就使用 $watch同步深度监听
状态的变化,根据设置的 _committing标识
来判定用户是否是通过 mutations
修改状态。
辅助函数 helper
上面vuex的源码的主干部分已经完成了,下面写一下vuex中的四个辅助函数实现原理。
//App.vue
<template>
<div id="app">
state.age: {{age}}
getters: {{$store.getters.getAge}}
b模块的c {{$store.state.b.c}}
d模块的e {{$store.state.d.e}}
<button @click="$store.state.age+=10"></button>
<button @click="$store.commit('changeAge',5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
<script>
const mapState = (arrayList) => {
const obj = {}
for(let i = 0; i < arrayList.length; i++) {
obj[arrayList[i]] = function() {
console.log(this);
return this.$store.state[arrayList[i]]
}
}
return obj
}
export default {
name: 'App',
computed: {
...mapState(['age']),
// age(){
// return this.$store.state.age
// }
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
复制代码
使用...mapState(['age']) 其实就是相当于 age(){return this.$store.state.age}
使用...mapGetters([getAge]) 相当于 getAge(){return this.$store.getters.getAge}
复制代码
我们知道传入的是一个数组,返回的是一个对象,所以代码如下:
const mapState = (arrayList) => {
const obj = {}
for(let i = 0; i < arrayList.length; i++) {
obj[arrayList[i]] = function() {
console.log(this);
return this.$store.state[arrayList[i]]
}
}
return obj
}
复制代码
运行结果如下:跟原来没区别,剩下的三种辅助函数都可以通过这种思路来书写。 辅助函数的代码如下:
// helper.js
const mapState = (arrayList) => {
const obj = {}
for (let i = 0; i < arrayList.length; i++) {
obj[arrayList[i]] = function () {
console.log(this);
return this.$store.state[arrayList[i]]
}
}
return obj
}
const mapGetters = (arrayList) => {
const obj = {}
for (let i = 0; i < arrayList.length; i++) {
obj[arrayList[i]] = function () {
return this.$store.getters.getAge
}
}
return obj
}
const mapMutations = (arrayList) => {
const obj = {}
for (let i = 0; i < arrayList.length; i++) {
obj[arrayList[i]] = function (payload) {
return this.$store.commit(arrayList[i], payload)
}
}
return obj
}
const mapActions = (arrayList) => {
const obj = {}
for (let i = 0; i < arrayList.length; i++) {
obj[arrayList[i]] = function (payload) {
return this.$store.dispatch(arrayList[i], payload)
}
}
return obj
}
export {
mapState,
mapGetters,
mapActions,
mapMutations
}
复制代码
// App.vue
<template>
<div id="app">
state.age: {{age}}
getters: {{getAge}}
b模块的c {{$store.state.b.c}}
d模块的e {{$store.state.d.e}}
<button @click="changeAge(5)">mustation方法</button>
<button @click="$store.dispatch('changeAge',10)">actions方法</button>
</div>
</template>
<script>
import {mapState,mapGetters,mapMutations} from './vuex/index.js'
export default {
name: 'App',
computed: {
...mapState(['age']),
...mapGetters(['getAge'])
},
methods: {
...mapMutations(['changeAge']),
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
复制代码
测试结果如下:
我个人对Vuex的理解
vuex
是个状态管理模式,通过这个状态机我们可以实现组件之间的通信
,因为在 install方法
内通过 vue的 mixin方法
将 Store实例
注入到每个组件内,在 resetStoreVm方法
内部主要是借助 vue本身的响应式原理
来实现状态的响应式,借助computed
来实现 getters的缓存
效果,然后通过 发布订阅的思想
将用户定义的 mutations
和 actions
方法存储起来,当用户触发 commit
、dispatch
的时候就去 订阅mutations
和 actions
找出对应的方法。如果存在 模块嵌套
的情况下,首先,vuex内部会通过一个 moduleCollection类
将所有模块 格式化
为一个 树形结构
,其次,通过 installModule方法
将格式化好的树形结构 安装
到 Store实例上
,最后,通过 resetStoreVm方法
借助 vue内部响应式的原理
将所有模块的状态都置为 响应式
。vuex是 响应式
的同时还支持 插件
的使用,Store内部有 subscribe方法
和 replaceState方法
,通过这两个方法我们实现 vuex的状态的持久化
,以上说明了 vuex是高度依赖 vue响应式
和 插件系统的
。以上就是我个人对vuex的理解。
如果对 Vue响应式原理
的不是很理解的小伙伴可以去之前发布的文章阅读 Vue响应式原理
综上,vuex源码
的主干部分基本已经在上面全部撸完了。vuex 的核心
基本已经实现了,希望对细心阅读的小伙伴们探究 vuex 核心机制
有所帮助,如有错误,敬请各位大佬在评论区指正。相关的源码已经上传到gitee,感兴趣的小伙伴可以去获取源码 戳我获取源码!!!!!。