菜鸡手写vuex(一)

148 阅读2分钟

前言

这篇文章实现了vueX的基础功能,小白一看就会,不考虑modules模块嵌套、命名空间的场景。

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

分析

import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
    state: {          // 存放状态  相当于new Vue({data}) 并且这里存放的数据时响应式的
        age: 18,
    },
    mutations: {      // 从规范上来说,这是唯一能够修改state中数据的地方(主要是为了方便跟踪state的改变),并且只能书写同步代码
        changeAge(state, payload) {
            state.age += payload;
        }
    },
    actions: {        // 通过mutations间接修改state中的数据,可以书写异步代码
        asyncChangeAge(store, payload) {
            setTimeout(() => {
                store.commit("changeAge", payload);
            }, 1000);
        }
    },
    getters: {        // VueX的计算属性   相当于new Vue({computes}) 具有懒惰性,与compute不同的是,getters可以作用于多个组件
        otherAge(state) {
            return state.age + 10;
        }
    }
})

export default store;
import store from './store'

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

以上就是vueX的一个简单实用方式,接下来我们的对它的使用方式分析一下。首先我们会使用import引入vuex,接着使用vue.use(vuex),说明我们的vuex应该是一个插件形式的,且拥有一个install()方法。然后实例化了一个new Vuex.Store({})对象,并且提供的参数对象里有state、mutations、actions、getters属性,那么我们的Store应该是一个类或者构造函数。接着在vue的根实例上引入了刚才Store的实例化对象,这里其实是让根实例以及子组件都可以访问到Store的实例化对象,简称该对象为$store吧。 image.png

实现

主文件

import { Store, install } from "./store";
export default{
    Store,
    install,
}
export {
    Store,
    install,
}

主文件主要是集合其他文件。

install()

install方法里面主要是在beforeCreate钩子实现让所有子组件都可以访问到store对象。利用先父后子的渲染规则,让子组件去获取父组件的store对象。利用先父后子的渲染规则,让子组件去获取父组件的store属性,从而实现每个组件都可以访问到$store。

const install = (_Vue) => {
    Vue = _Vue;
    applyMixin(Vue);
}
const applyMixin = (Vue) => {
    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){
        // 子组件
        this.$store = options.parent.$store;
    }
}

state

class Store{
    constructor{
        ...
        
        let state = options.state;
        this._vm = new Vue({
            data: {
                $$state: state,
            },
        })
    }
    ...
    
    get state(){
        return this._vm._data.$$state;
    }
}

state里面存储的数据是响应式的,当数据发生改变时视图也会更新,这里主要是利用了vue的响应式系统。vuex在内部直接创建了一个vue实例,将state状态数据放在vue的data中,然后对Store类添加类属性访问器,在属性访问器里面代理访问存储data的state状态数据,从而实现依赖收集,当依赖发生改变就会去通知渲染watcher更新页面。
为什么在data里面声明的state前面带有$$符号呢?因为当属性名是类似'$xxx',那么它不会被代理到vue的实例上。

mutations

按照vuex的规范来说,修改state中数据的唯一方法是通过commit触发mutations里面定义的方法,这主要是为了方便vuex跟踪数据的变化,并且mutations里面只能写同步方法。当然你也可以简单粗暴的使用this.$store.state.xxx = "",它同样也不会报错,也会得到视图响应式更新,但是这是一种不规范的写法。

export const forEach = (obj = {}, fn) => {
    Object.keys(obj).forEach((key, index) => fn(obj[key], key));
}
class Store{
    constructor{
        ...
        
        this._mutations = {};
        orEach(options.mutations, (fn, type) => {
            this._mutations[type] = (payload) => fn.call(this, this.state, payload)
        })
    }
    ...
    
    commit = (type, payload) => {
        this._mutations[type](payload);
    }
}

采用发布订阅模式,将用户定义的mutation先保存起来,稍后调用commit时 就找订阅的mutation方法,执行订阅方法。

actions

actions是通过mutations间接修改state里面的数据,并且在actions里面可以书写异步方法。它的实现过程和mutations差不多。

class Store{
    constructor{
        ...
        
        this._actions = {};
        forEach(options.actions, (fn, type) => {
            this._actions[type] = (payload) => fn.call(this, this, payload)
        })
    }
    ...
    
    dispatch = (type, payload) => {
        this._actions[type](payload);
    }
}

同样是发布订阅模式,将用户定义的actions先保存起来,稍后调用dispatch时 就找订阅的actions方法,执行订阅方法。

getters

getters可以理解为vuex的计算属性,类似于vue的computed属性,getters同样是惰性求值,只有当它的依赖值发生改变才会重新计算新的值。getters和computed的区别在于,前者可以作用于多个组件,而后者只是作用于当前组件。getters的本质还是借用computed来实现的。

class Store{
    constructor{
        ...
        
        this.getters = {};
        const computed = {};
        forEach(options.getters, (fn, key) =>{
            computed[key] = () => {         // 借用计算属性来实现懒加载
                return fn(this.state);
            }
            Object.defineProperty(this.getters, key, {
                get: () => this._vm[key],
            })
        })
        
        this._vm = new Vue({
            data: {
                $$state: state,
            },
            computed,
        })
    }
    ...
    
}

结尾

至此,就已经实现vuex的基本功能了,有兴趣的朋友可以继续查看完整项目代码,代码地址:vue-vuex-mini

仓库代码已经实现了modules模块化、namespaced命名空间、plugins、动态注册模块,并且具有超详细的注释。

菜鸡手写vuex(一)
菜鸡手写vuex(二)