Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。若是你已经灵活运用,可是依然好奇它底层实现逻辑,不妨一探究竟。javascript
咱们知道开发 Vue 插件,安装的时候须要执行 Vue.use(Vuex)vue
import Vue from 'vue'
import Vuex from '../vuex'
Vue.use(Vuex)
复制代码
经过查看 Vue API Vue-use 开发文档,咱们知道安装 Vue.js 插件。若是插件是一个对象,必须提供
install
方法。若是插件是一个函数,它会被做为 install 方法。install 方法调用时,会将 Vue 做为参数传入。该方法须要在调用new Vue()
以前被调用。当 install 方法被同一个插件屡次调用,插件将只会被安装一次。java
为了更好了的去理解源码意思,这里写了一个简单的测试实例。git
import Vue from 'vue'
import Vuex from '../vuex'
Vue.use(Vuex)
export default new Vuex.Store({
plugins: [],
state: {
time: 1,
userInfo: {
avatar: '',
account_name: '',
name: ''
},
},
getters: {
getTime (state) {
console.log('1212',state)
return state.time
}
},
mutations: {
updateTime(state, payload){
state.time = payload
}
},
actions: {
operateGrou({ commit }) {
// commit('updateTime', 100)
return Promise.resolve().then(()=>{
return {
rows: [1,2,3]
}
})
}
},
modules: {
report: {
namespaced: true,
state: {
title: '',
},
getters: {
getTitle (state) {
return state.title
}
},
mutations: {
updateTitle(state, payload){
state.title = payload
}
},
actions: {
operateGrou({ commit }) {
commit('updateTitle', 100)
return Promise.resolve().then(()=>{
return {
rows: [1,2,2,3]
}
})
}
},
modules: {
reportChild: {
namespaced: true,
state: {
titleChild: '',
},
mutations: {
updateTitle(state, payload){
state.title = payload
}
},
actions: {
operateGrou({ commit }) {
commit('updateTitle', 100)
return Promise.resolve().then(()=>{
return {
rows: [1,2,2,3]
}
})
}
},
}
}
},
part: {
namespaced: true,
state: {
title: '',
},
mutations: {
updateTitle(state, payload){
state.title = payload
},
updateTitle1(state, payload){
state.title = payload
}
},
actions: {
operateGrou({ commit }) {
commit('updateTitle', 100)
return Promise.resolve().then(()=>{
return {
rows: [1,2,2,3]
}
})
}
},
modules: {
partChild: {
namespaced: true,
state: {
titleChild: '',
},
getters: {
getTitleChild (state) {
return state.titleChild
}
},
mutations: {
updateTitle(state, payload){
state.titleChild = payload
}
},
actions: {
operateGrou({ commit }) {
commit('updateTitle', 1000)
return Promise.resolve().then(()=>{
return {
rows: [1,2,2,3]
}
})
}
},
modules: {
partChildChild: {
namespaced: true,
state: {
titleChild: '',
},
getters: {
getTitleChild (state) {
return state.titleChild
}
},
mutations: {
updateTitle(state, payload){
state.titleChild = payload
}
},
actions: {
operateGrou({ commit }) {
commit('updateTitle', 1000)
return Promise.resolve().then(()=>{
return {
rows: [1,2,2,3]
}
})
}
},
}
}
}
}
}
}
})
复制代码
用 Graphviz 图来表示一下父子节点的关系,方便理解github
在调用 Vuex 的时候会找其 install 方法,并把组件实例传递到 install 方法的参数中。vuex
let Vue;
class Store {
}
const install = _Vue => {
Vue = _Vue;
Vue.mixin({
beforeCreate(){
console.log(this.$options.name);
}
})
};
export default {
Store,
install
}
复制代码
到这里说一下 Vuex 实现的思想,在 Vuex 的 install 方法中,能够获取到 Vue 实例。
咱们在每一个 Vue 实例上添加 $store 属性,可让每一个属性访问到 Vuex 数据信息;
咱们在每一个 Vue 实例的 data 属性上添加上 state,这样 state 就是响应式的;
收集咱们传入 new Vuex.Store(options) 即 options 中全部的 mutaions、actions、getters;
接着当咱们 dispatch 的时候去匹配到 Store 类中存放的 actions 方法,而后去执行;
当咱们 commit 的时候去匹配到 Store 类中存放的 mutations 方法,而后去执行;
这其实就是一个发布订阅模式,先存起来,后边用到再取再执行。好了解这些,咱们开始真正的源码分析;后端
为了更好理解,咱们打印出 Vue 实例,能够看到注入了 $store,见下图。
api
具体实现关键点数组
const install = (_Vue) => {
Vue = _Vue
Vue.mixin({
beforeCreate(){
// 咱们能够看下面 main.js 默认只有咱们的根实例上有 store,故 this.$options.store 有值是根结点
if(this.$options.store) {
this.$store = this.$options.store // 根结点赋值
} else {
this.$store = this.$parent && this.$parent.$store // 每一个实例都会有父亲。故一层层给实例赋值
}
}
})
}
复制代码
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
复制代码
响应式核心就是挂载到实例 data 上,让 Vue 内部运用 Object.defineProperty 实现响应式。app
class Store{
constructor (options) { // 咱们知道 options 是用户传入 new Vuex.Store(options) 参数
this.vm = new Vue({
data: {
state: options.state
}
})
}
}
复制代码
来看下 用户传入的 mutations 变成了什么,数据采用 最上面的测试实例代码。
咱们能够看到 mutations 是一个对象,里面放了函数名,值是数组,将相同函数名对应的函数存放到数组中。
const setMoutations = (data, path = []) => {
const mutations = data.mutations
Object.keys(mutations).map(item => {
this.mutations[item] = this.mutations[item] || [] // 以前的旧值
this.mutations[item].push(mutations[item]) // 存起来
})
const otherModules = data.modules || {} // 有子 modules 则递归
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setMoutations(otherModules[item], path.concat(item))
})
}
}
setMoutations(options) // 这里 options 是用户传入的 new Vuex.Store(options) 的参数
复制代码
class Store{
commit = (mutationName, payload) => {
this.mutations[mutationName].map(fn => {
fn(this.state, payload)
})
}
}
复制代码
const setAction = (data, path = []) => {
const actions = data.actions
Object.keys(actions).map(item => {
this.actions[item] = this.actions[item] || []
this.actions[item].push(actions[item])
})
const otherModules = data.modules || {}
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setAction(otherModules[item], path.concat(item))
})
}
}
setAction(options)
复制代码
class Store{
dispatch = (acitonName, payload) => {
this.actions[acitonName].map(fn => {
fn(this, payload) // this.$store.dispatch('operateGrou')
})
}
}
复制代码
const setGetter = (data, path = []) => {
const getter = data.getters || {}
const namespace = data.namespaced
Object.keys(getter).map(item => {
// 跟 Vue 计算属性底层实现相似,当从 store.getters.doneTodos 取值的时候,实际会执行 这个方法。
Object.defineProperty(this.getter, item, {
get:() => {
return options.state.getters[item](this.state)
}
})
})
const otherModules = data.modules || {}
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setGetter(otherModules[item], path.concat(item))
})
}
}
setGetter(options)
复制代码
上面讨论的是没有 namespaced 的状况,加上 namespaced 有什么区别呢,见下图。
瞬间拨云见日了,日常写上面基本上都要加上 namespaced,防止命名冲突,方法重复屡次执行。
如今就算每一个 modules 的方法命同样,也默认回加上这个方法别包围的全部父结点的 key。
下面对 mutations actions getters 扩展一下,让他们支持 namespaced。核心就是 path 变量
// 核心点在 path
const setMoutations = (data, path = []) => {
const mutations = data.mutations
const namespace = data.namespaced
Object.keys(mutations).map(item => {
let key = item
if (namespace) {
key = path.join('/').concat('/'+item) // 将全部父亲用 斜杠 相关联
}
this.mutations[key] = this.mutations[key] || []
this.mutations[key].push(mutations[item])
})
const otherModules = data.modules || {}
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setMoutations(otherModules[item], path.concat(item)) // path.concat 不会修改 path 原来的值
})
}
}
setMoutations(options)
复制代码
actions 与 mutations 是同样的
const setAction = (data, path = []) => {
const actions = data.actions
const namespace = data.namespaced
Object.keys(actions).map(item => {
let key = item
if (namespace) {
key = path.join('/').concat('/'+item)
}
this.actions[key] = this.actions[key] || []
// this.actions[key].push(actions[item])
this.actions[key].push((payload) => {
actions[item](this, payload);
})
})
const otherModules = data.modules || {}
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setAction(otherModules[item], path.concat(item))
})
}
}
setAction(options)
复制代码
const setGetter = (data, path = []) => {
const getter = data.getters || {}
const namespace = data.namespaced
Object.keys(getter).map(item => {
let key = item
if (namespace) {
key = path.join('/').concat('/'+item)
}
Object.defineProperty(this.getter, key, {
get: () => {
return getter[item](this.state)
}
})
})
const otherModules = data.modules || {}
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setGetter(otherModules[item], path.concat(item))
})
}
}
setGetter(options)
复制代码
咱们能够总结来看,namespaces 加与不加的区别实际就下图;
看到这,小伙伴怀疑了,actions 与 mutations,具体实现是同样的,那为何要说 actions 能够异步执行,mutations,不能异步执行呢?下面我来贴一下核心代码。
class Store{
constructor () {
////..... 省略
if(this.strict){// 严格模式下才给报错提示
this.vm.$watch(()=>{
return this.vm.state // 咱们知道 commit 是会出发 state 值修改的
},function () {
// 此处监听 state 修改,由于在执行 commit 的时候 this._committing 是true 的,你若放了异步方法,this._committing 就会往下执行 变成 false
console.assert(this._committing,'您异步调用了!') // 断言 this._committing 为false, 给报错提示
},{deep:true,sync:true});
}
}
_withCommit(fn){
const committing = this._committing; // 保留false
this._committing = true; // 调用 mutation以前, this._committing 更改值是 true
fn(); // 保证 执行的时候 this._committing 是 true
this._committing = committing // 结束后重置为 false
}
commit = (mutationName, payload) => {
console.log('1212',mutationName)
this._withCommit(()=>{
this.mutations[mutationName] && this.mutations[mutationName].map(fn => {
fn(this.state, payload)
})
})
}
}
复制代码
咱们常常在 Vuex 中这样使用
import {
mapState,
mapGetters
} from 'vuex'
computed: {
isAfterSale () {
return this.$route.meta.isAfterSale
},
...mapGetters({
messageState: 'message/getMessageState'
}),
...mapGetters({
messageNum: 'message/getMessageNum'
}),
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
},
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
复制代码
export const mapState = (stateArr) => { // {age:fn}
let obj = {};
stateArr.forEach(stateName => {
obj[stateName] = function () {
return this.$store.state[stateName]
}
});
return obj;
}
复制代码
export function mapGetters(gettersArr) {
let obj = {};
gettersArr.forEach(getterName => {
obj[getterName] = function () {
return this.$store.getters[getterName];
}
});
return obj
}
复制代码
export function mapMutations(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function (...args) {
this.$store.commit(value, ...args)
}
})
return res;
}
复制代码
export function mapActions(obj) {
let res = {};
Object.entries(obj).forEach(([key, value]) => {
res[key] = function (...args) {
this.$store.dispatch(value, ...args)
}
})
return res;
}
复制代码
Vuex 的 store 接受
plugins
选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 做为惟一参数:
实际上 具体实现是发布订阅着模式,经过store.subscribe 将须要执行的函数保存到 store subs 中,
当 state 值发生改变时,this.subs(fn=>fn()) 执行。
const vuePersists = store => {
let local = localStorage.getItem('VuexStore');
if(local){
store.replaceState(JSON.parse(local)); // 本地有则赋值
}
store.subscribe((mutation,state)=>{
localStorage.setItem('VuexStore',JSON.stringify(state)); // state 发生变化执行
});
}
const store = new Vuex.Store({
// ...
plugins: [vuePersists]
})
复制代码
class Store{
constructor () {
this.subs = []
const setMoutations = (data, path = []) => {
const mutations = data.mutations
const namespace = data.namespaced
Object.keys(mutations).map(mutationName => {
let namespace = mutationName
if (namespace) {
namespace = path.join('/').concat('/'+mutationName)
}
this.mutations[namespace] = this.mutations[namespace] || []
this.mutations[namespace].push((payload)=>{ // 以前是直接 push
mutations[item](options.state, payload)
this.subs.forEach(fn => fn({ // state 发生改变 则发布通知给插件
type: namespace,
payload: payload
}, options.state));
})
})
const otherModules = data.modules || {}
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setMoutations(otherModules[item], path.concat(item)) // path.concat 不会修改 path 原来的值
})
}
}
setMoutations(options)
}
subscribe(fn) {
this.subs.push(fn);
}
}
复制代码
state 还没处理呢,别忘记咱们 用户传入的 state 只是分module 传的,最终都要挂载到 state 中,见初始值和下图图片。其实是数据格式转化,相信跟后端对接多的同窗,考验处理数据格式的能力了。是的递归跑不了了。
{
state: {
time: 1,
userInfo: {
avatar: '',
account_name: '',
name: ''
},
},
modules: {
report: {
state: {
title: '',
},
},
part: {
state: {
title: '',
},
modules: {
partChild: {
state: {
titleChild: '',
},
}
}
},
}
}
复制代码
能够看到核心方法仍是 path, path.slice 来获取每次递归的父结点。
const setState = (data, path = []) => {
if (path.length > 0) {
let parentModule = path.slice(0, -1).reduce((next, prev)=>{
return next[prev]
}, options.state)
Vue.set(parentModule, path[path.length - 1], data.state); // 为了 State 每一个属性添加 get set 方法
// parentModule[path[path.length - 1]] = data.state // 这样修改 Vue 是不会监听的
}
const otherModules = data.modules || {}
if (Object.keys(otherModules).length > 0){
Object.keys(otherModules).map(item => {
setState(otherModules[item], path.concat(item))
})
}
}
setState(options)
复制代码
原谅我,最重要的一块放到最后才说。上面全部方法都是基于用户传的 options (new Vuex.Store(options)) 来实现的。可是用户输入的咱们怎能轻易详细,咱们仍是要对模块进行进一步格式处理,转化成咱们须要的数据。转化成见下图。
咱们能够分析出收集模块,实际也是递归,转化成固定格式数据 _children、state、rawModule。
直接看源码把 核心代码是 register 方法,实际也是数据格式的转化。
通篇看下来,仍是须要本身手敲一下,在实践的过程当中,才能发现问题(this 指向、父子结点判断、异步方法保存提示的巧妙)。当你明白具体实现,那每次使用就垂手可得了,每一步使用都知道干了什么的感受真好。