Vuex系列原理(剖析)

87 阅读3分钟

Vuex原理剖析

一.Vuex基本使用及用法

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

以下是一个表示“单向数据流”理念的简单示意:

image.png

这个状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

image.png

使用示例:

// store/index.js文件
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    age: 28
  },
  getters: {
    getAge(state) {
      return state.age + 10;
    }
  },
  mutations: {
    changeAge(state, payload) {
      state.age += payload
    }
  },
  actions: {
    changeAge({
      commit
    }, payload) {
      setTimeout(() => {
        commit('changeAge', payload);
      }, 1000);
    }
  }
})
// index.js
import Vue from "vue"  
import App from "./App.vue"  
import store from "./store"  
var vm = new Vue({  
    el'#root',  
    store,  
    render: h => h(App)  
})
<template>  
<div id="app">  
    <div>{{$store.state.age}}</div>  
</div>  
<button @click="$store.commit('changeAge',2)">changeAge</button>  
</template>

(1)在 store/index.js 中,导入vuex后,做了两件事:Vue.use(Vuex)new Vuex.Store(options)的options配置

(2)在index.js中,导入new Vuex.Store(options)生成的实例store,然后new Vue的时候作为选项参数传入。

(3)在app.vue中,也就是vm的子组件实例都挂载了一个$store属性(install方法),可以直接通过this.$store访问。

使用方式也可以参考官方文档: v3.vuex.vuejs.org/zh/guide/

这里可以将 state 类比为组件的状态
getters类比为组件的计算属性
mutations类比为组件中的方法(可以更改组件的状态)
actions用于进行异步操作将结果提交给mutation

<div id="app">
    age: {{this.$store.state.age}}
    <br />
    age01: {{this.$store.getters.getAge}}
    <br />
    <!-- dispatch对应的action -->
    <button @click="$store.dispatch('changeAge',3)">可异步add+3</button>
    <!-- commit 对应的mutation -->
    <button @click="$store.commit('changeAge',5)">同步增加5</button>
</div>

这个$store属性是通过根实例传入的

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

内部会将store属性挂载在每个实例上命名为$store,这样所有组件都可以操作同一个store属性

二.自己实现Vuex模块

Vue.use会调用vuex.install方法;new Vuex.store表示Vuex.Store是个类。

2.1 Store类和install方法

当我们在new Vue(options)的选项中传入store实例后,它每一个子组件上面都有一个$store属性。挂载全局$store就是在install方法完成,借助Vue.mixin()实现。

// src/vuex@/index.js

export let Vue

class Store {
    constructor(options) {
        
    }
}

const install = function(_Vue) {
    Vue = _Vue;
    Vue.mixin({ // 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
        beforeCreate() {
            if(this.$options.store) {
                this.$store = this.$options.store;
            }else if(this.$options.parent &&this.$options.parent.$store) {
            // //子组件$store属性指向父组件的$store属性, 这样每个组件的$store,并且都指向根实例的$store
                this.$store =this.$options.parent.$store
            }
        }
    })
}

export default {
    Store,
    install,
}

2.2 另外一种方式

或者以下方式,实现入口文件,默认导出Store类和install方法

import { Store, install } from './store';

export default {
    Store,
    install
}
export {
    Store,
    install
}

1.install方法

import applyMixin from './mixin'
let Vue;
export class Store {
    constructor(options){}
}

export const install = (_Vue) =>{
    Vue = _Vue;
    applyMixin(Vue);
}

当我们使用插件时默认会执行install方法并传入Vue的构造函数

2.mixin方法

const applyMixin = (Vue) => {
    Vue.mixin({
        beforeCreate: vuexInit
    })
}

function vuexInit() {
    const options = this.$options;
    if (options.store) { 
        // 给根实例增加$store属性
        this.$store = options.store;
    } else if (options.parent && options.parent.$store) {
        // 给组件增加$store属性
        this.$store = options.parent.$store;
    }
}
export default applyMixin

将store实例定义在所有的组件实例上

3.实现state

export class Store {
    constructor(options){
        let state = options.state;
        // vuex完全依赖vue的响应式原理
        this._vm = new Vue({
            data:{
                $$state:state,
            }
        });
    }
    get state(){ //访问器
        return this._vm._data.$$state
    }
}

将用户传入的数据定义在vue的实例上 (这个就是vuex核心)产生一个单独的vue实例进行通信,这里要注意的是定义$开头的变量不会被代理到实例上

定义forEachValue函数

// 参数为一个obj对象 和回调函数, 内部遍历obj执行fn函数
export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(key, obj[key]))
}

4.实现 getters

this.getters = {};
const computed = {}
forEachValue(options.getters, (key, value) => {
    computed[key] = () => {
        return value(this.state); // this.state为getters定义的属性的参数,  value 为obj对象中的值,也就是具体定义的函数🌟
    }
    Object.defineProperty(this.getters,key,{ 
    //将key(也就是getters上用户定义的属性 比如add)挂载到this._vm上
        get:()=> this._vm[key]
    })
});
this._vm = new Vue({ 
    data: {
        $$state: state,
    },
    computed // 利用计算属性实现缓存
});

forEachValue为自定义的一个方法, 第一个参数options.getters为用户定义的对象属性, 第二个参数为一个回调函数, 

5.实现mutations

export class Store {
    constructor(options) {
        this.mutations = {};
        forEachValue(options.mutations, (key, value) => {
            this.mutations[key] = (payload) => value.call(this, this.state, payload)// value 为obj对象中的值,也就是具体定义的函数🌟
            /*定义this.mutations[key] 具体的方法赋值 也就是用户使用的increment函数
            mutations:{
                increment(state, payload) {
                  state.count += payload.acc;
            }
            */
        });
    }
    commit = (type, payload) => { // 用户执行的commit函数
        this.mutations[type](payload);
    }
}

6.实现actions

export class Store {
    constructor(options) {
        this.actions = {};
        forEachValue(options.actions, (key, value) => {
            this.actions[key] = (payload) => value.call(this, this,payload); // 和mutations,具体不同看下面
        });
    }
    dispatch = (type, payload) => {
        this.actions[type](payload);
    }
}

map辅助函数

使用方式

computed:{
    store_count() {
      return this.$store.state.count
    },
    ...mapGetters([ // 可以传入数组
      'doneTodos',
      'doneTodosFalse'
    ]),
    ...mapState([
      'count',
    ])
}

1.mapState辅助函数 实现

arrList: 用户传入的数组
const mapState = arrList => {
  let obj = {};
  for (let i = 0; i < arrList.length; i++) {
    let stateName = arrList[i]; //每一项, eg: 'count',
    obj[stateName] = function() { // 将obj[count]挂载到computed上, 
      return this.$store.state[stateName]; //  返回的是src/store/index.js的Store里面定义的内容
    };
  }
  return obj;
};

2.mapGetters实现

// 同mapState辅助函数思路
const mapGetters = arrList => {
  let obj = {};
  for (let i = 0; i < arrList.length; i++) {
    let getterName = arrList[i]
    obj[getterName] = function() {
      return this.$store.getters[getterName];
    };  
  }
  return obj;
};

3.mapMutations实现

// 同mapState辅助函数思路
const mapMutations = mutationList=>{
    let obj = {};
    for (let i = 0; i < mutationList.length; i++) {
        let type = mutationList[i]
        obj[type] = function(payload){
            this.$store.commit(type,payload);
        }
    }
    return obj
}

4.mapActions实现

// 同mapState辅助函数思路
const mapActions = actionList=>{
    let obj = {};
    for (let i = 0; i < actionList.length; i++) {
        let type = actionList[i]
        obj[type] = function(payload){
            this.$store.dispatch(type,payload);
        }
    }
    return obj
}

实现模块 Module 机制

1.格式化用户数据

import ModuleCollection from './module/module-collection'
this._modules = new ModuleCollection(options);
import { forEachValue } from '../util'
export default class ModuleCollection {
    constructor(options) {
        this.register([], options)
    }
    register(path, rootModule) {
        let newModule = {
            _raw: rootModule,
            _children: {},
            state: rootModule.state
        };
        if (path.length == 0) {
            this.root = newModule;
        } else {
            let parent = path.slice(0,-1).reduce((memo,current)=>{
                return memo._children[current];
            },this.root);
            parent._children[path[path.length-1]] = newModule;
        }
        if (rootModule.modules) {
            forEachValue(rootModule.modules, (module, moduleName) => {
                this.register(path.concat(moduleName), module);
            })
        }
    }
}

2.抽离模块类

export default class Module{
    constructor(rawModule){
        this._children = {};
        this._rawModule = rawModule;
        this.state = rawModule.state
    }
    getChild(key){
        return this._children[key]
    }
    addChild(key,module){
        this._children[key] = module
    }
}

后续我们对模块扩展会更加的方便

3.安装模块

this._actions = {};
this._mutations = {};
this._wrappedGetters = {}
// 安装模块
installModule(this, state, [], this._modules.root);

在模块类中提供遍历方法

export default class Module {
    constructor(rawModule) {
        this._children = {};
        this._rawModule = rawModule;
        this.state = rawModule.state
    }
    getChild(key) {
        return this._children[key]
    }
    addChild(key, module) {
        this._children[key] = module
    }
    forEachMutation(fn) {
        if (this._rawModule.mutations) {
            forEachValue(this._rawModule.mutations, fn)
        }
    }
    forEachAction(fn) {
        if (this._rawModule.actions) {
            forEachValue(this._rawModule.actions, fn)
        }
    }
    forEachGetter(fn) {
        if (this._rawModule.getters) {
            forEachValue(this._rawModule.getters, fn)
        }
    }
    forEachChild(fn) {
        forEachValue(this._children, fn);
    }
}

对模块进行安装操作

function installModule(store, rootState, path, module) {
    if (path.length > 0) {
        let parent = path.slice(0, -1).reduce((memo, current) => {
            return memo[current];
        }, rootState);
        Vue.set(parent, path[path.length - 1], module.state);
    }
    module.forEachMutation((mutation, key) => {
        store._mutations[key] = (store._mutations[key] || []);
        store._mutations[key].push((payload) => {
            mutation.call(store, module.state, payload);
        });
    });
    module.forEachAction((action, key) => {
        store._actions[key] = (store._actions[key] || []);
        store._actions[key].push(function(payload) {
            action.call(store, this, payload);
        });
    });
    module.forEachGetter((getter, key) => {
        store._wrappedGetters[key] = function() {
            return getter(module.state);
        }
    });
    module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child)
    })
}

dispatch和 action方法进行重写

commit = (type, payload) => {
    this._mutations[type].forEach(fn => fn.call(this, payload));
}
dispatch = (type, payload) => {
    this._actions[type].forEach(fn => fn.call(this, payload));
}

4.定义状态和计算属性

resetStoreVM(this, state);
function resetStoreVM(store, state) {
    const computed = {};
    store.getters = {};
    const wrappedGetters = store._wrappedGetters
    forEachValue(wrappedGetters, (fn, key) => {
        computed[key] = () => {
            return fn(store.state);
        }
        Object.defineProperty(store.getters, key, {
            get: () => store._vm[key]
        })
    });
    store._vm = new Vue({
        data: {
            $$state: state,
        },
        computed
    });
}

5.实现命名空间

import { forEachValue } from '../util';
import Module from './module';
export default class ModuleCollection {
    getNamespace(path) {
        let module = this.root
        return path.reduce((namespace, key) => {
            module = module.getChild(key);
            console.log(module)
            return namespace + (module.namespaced ? key + '/' : '')
        }, '');
    }
}
export default class Module {
    get namespaced(){
        return !!this._rawModule.namespaced;
    }
}

在绑定属性是增加命名空间即可

function installModule(store, rootState, path, module) {
    let namespace = store._modules.getNamespace(path);
    if (path.length > 0) {
        let parent = path.slice(0, -1).reduce((memo, current) => {
            return memo[current];
        }, rootState);
        Vue.set(parent, path[path.length - 1], module.state);
    }
    module.forEachMutation((mutation, key) => {
        store._mutations[namespace + key] = (store._mutations[namespace + key] || []);
        store._mutations[namespace + key].push((payload) => {
            mutation.call(store, module.state, payload);
        });
    });
    module.forEachAction((action, key) => {
        store._actions[namespace + key] = (store._actions[namespace + key] || []);
        store._actions[namespace + key].push(function(payload) {
            action.call(store, this, payload);
        });
    });
    module.forEachGetter((getter, key) => {
        store._wrappedGetters[namespace + key] = function() {
            return getter(module.state);
        }
    });
    module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child)
    })
}

6.注册模块

registerModule(path,rawModule){
    if(typeof path == 'string') path = [path];
    this._modules.register(path, rawModule);
    installModule(this, this.state, path, rawModule.rawModule);
    // 重新设置state, 更新getters
    resetStoreVM(this,this.state);
} 

实现模块的注册,就是将当前模块注册到_modules中

function resetStoreVM(store, state) {
+   let oldVm = store._vm;
    const computed = {};
    store.getters = {};
    const wrappedGetters = store._wrappedGetters
    forEachValue(wrappedGetters, (fn, key) => {
        computed[key] = () => {
            return fn(store.state);
        }
        Object.defineProperty(store.getters, key, {
            get: () => store._vm[key]
        })
    });
    store._vm = new Vue({
        data: {
            $$state: state,
        },
        computed
    });
    if(oldVm){
+      Vue.nextTick(() => oldVm.$destroy())
    }
}

销毁上次创建的实例

四.插件机制

1.使用方式

function persists(store) { // 每次去服务器上拉去最新的 session、local
    let local = localStorage.getItem('VUEX:state');
    if (local) {
        store.replaceState(JSON.parse(local)); // 会用local替换掉所有的状态
    }
    store.subscribe((mutation, state) => {
        // 这里需要做一个节流  throttle lodash
        localStorage.setItem('VUEX:state', JSON.stringify(state));
    });
}
plugins: [
    persists
]

这里我们要实现subscribe、replaceState方法

// 执行插件
options.plugins.forEach(plugin => plugin(this));
subscribe(fn){
	this._subscribers.push(fn);
}
replaceState(state){
	this._vm._data.$$state = state;
}

2.获取最新状态

function getState(store, path) {
    let local = path.reduce((newState, current) => {
        return newState[current]; 
    }, store.state);
    return local
}
module.forEachMutation((mutation, key) => {
    store._mutations[namespace + key] = (store._mutations[namespace + key] || []);
    store._mutations[namespace + key].push((payload) => {
    mutation.call(store, getState(store,path), payload);
    });
});

调用mutation时传入最新状态

六.区分mutation和action

this._committing = false;
 _withCommitting(fn) {
    let committing = this._committing;
    this._committing = true; // 在函数调用前 表示_committing为true
    fn();
    this._committing = committing;
}
if (store.strict) {
    // 只要状态一变化会立即执行,在状态变化后同步执行
    store._vm.$watch(() => store._vm._data.$$state, () => {
        console.assert(store._committing, '在mutation之外更改了状态')
    }, { deep: true, sync: true });
}

严格模式下增加同步watcher,监控状态变化

store._withCommitting(() => {
    mutation.call(store, getState(store, path), payload); // 这里更改状态
})

只有通过mutation更改状态,断言才能通过

replaceState(newState) { // 用最新的状态替换掉
    this._withCommitting(() => {
        this._vm._data.$$state = newState;
    })
}
store._withCommitting(() => {
    Vue.set(parent, path[path.length - 1], module.state);
})

内部更改状态属于正常更新,所以也需要用_withCommitting进行包裹