likes
comments
collection
share

Vue系列深入教程(二)-- 实现基本的vuex

作者站长头像
站长
· 阅读数 15

大家好,我是小黑。

本人打算写一个和vue全家桶相关的系列教程,从vue2到vue3,这系列不会有太多讲解vue全家桶的使用,所以需要对vue及其全家桶比较熟练。

系列文章

  1. Vue系列深入教程(一)-- 实现基本的vue-router
  2. Vue系列深入教程(二)-- 实现基本的vuex

建议系列文章从头读起,不要跳着读。

本文使用vue2基于hash实现简单的vuex,因为是vue2写的,所以实现vuex的版本也是3版本。

系列教程代码menorepo仓库地址gitee.com/jimpp/front…

本节代码在vue-series-demo/my-vuex下

Vuex使用回顾

1. npm install vuex; (这个必须的)

2. 在main.js中

import Vuex from 'vuex'
Vue.use(Vuex)

3. 添加store.js

const store = {
    state: {
       count: 1
    },
    getters: {
       doubleCount(state) {
            return state.count * 2
       }
    },
    mutations: {
      setCount(state, count) {
        state.count = count
      }
    },
    actions: {
      setCount({commit}, count) {
          commit('setCount', count)
      }
    }
}
export default store

4. 初始化store

import staticStore from './store.js'
const store = new Vuex.Store(staticStore);

5. 初始化vue实例的时候注入store

new Vue({
    el: '#app',
    store, // 在这里注入了
    render: c => c(App),
})

6. 注入成功后,所有组件都可以访问$store对象,能够访问store实例

7. 在.vue文件中

<template>
    <div>
        <button @click="$store.commit('setCount', 4)">mutations</button>    
        <button @click="$store.dispatch('setCount', 2)">action</button>    
        <div>count: {{$store.state.count}}</div>
        <div>double:{{$store.getters.doubleCount}}</div>
    </div>
</template>

8. 点击两个按钮可以看到界面发生变化

vuex的实现思路

store的特点

  1.  Vuex必定是一个包含Store class导出的类,因为可以通过new Vuex.store实例化
  2.  每个实例都会有state, getters, mutations, actions等属性
  3.  state可以直接读取,但是不能直接通过store.state = xxx赋值
  4.  每个实例都会有commit, dispatch方法
  5. state所有数据是响应式的
  6. 实例化后,原型会挂载$store对象
  7. Vue.use方法的原理,不懂的同学看上一篇文章

关键就在于实现响应式的state,state可以有两种方法实现,但是其实都是基于vue框架的

state第一种实现

通过new 一个 vue实例实现

const state = new Vue({
 data: {
   intervalStore: {}
 }
});

这样state中的属性必定是响应式的

state第二种实现

通过Vue.observable(object), 不知道的同学请看上一篇文章

const state = Vue.observable({
   instarvalStore: {}
})

这样state中的属性也是是响应式的

环境准备

本章不会帖太多环境代码,大部分跟上一章一样,只贴关键的代码

目录结构

依旧是使用vite

Vue系列深入教程(二)-- 实现基本的vuex

package.json

{ 
    "name": "my-vuex", 
    "version": "1.0.0", 
    "description": "", 
    "main": "index.js", 
    "scripts": { 
        "dev": "vite" 
    }, 
    "keywords": [], 
    "author": "", 
    "license": "ISC", 
    "dependencies": { 
        "vue": "2.7.9" 
    }, 
    "devDependencies": { 
        "vite": "3.0.7", 
        "vue-template-compiler": "2.7.9", 
        "vite-plugin-vue2": "1.9.3" 
    }
}

app.vue

<template>  
    <div>    
        <button @click="$store.commit('setCount', 4)">mutations</button>    
        <button @click="$store.dispatch('setCount', 2)">action</button>    
        <div>count: {{ $store.state.count }}</div>    
        <div>double:{{ $store.getters.doubleCount }}</div>  
    </div>
</template>

store.js

const store = {
    state: {
       count: 1
    },
    getters: {
       doubleCount(state, getter) {
          console.log("getter", getter)
          return state.count * 2
       },
       countAddOne(state) {
          return state.count + 1
       }
    },
    mutations: {
      setCount(state, count) {
        state.count = count
      }
    },
    actions: {
      setCount({ state, commit, dispatch }, count) {         console.log("测试state", state);         dispatch("testAction", "测试action");         setTimeout(() => {           console.log("测试commit");           commit("setCount", count);         }, 2000);       },       testAction(context, payload) {         console.log("in testActions", context, payload);       },    }
}
export default store
  1. action有两个,在setCount中调用testAction
  2. setCount中输出state,提交commit
  3. getters有两个,输出参数getter

index.js

import Vue from "vue";
import Vuex from "./Vuex/index";
import Store from "./store";
import App from "./App.vue";

Vue.use(Vuex);

const store = new Vuex.Store(Store);
const app = new Vue({  
    store,  
    render: (h) => h(App),
}).$mount("#app");

Vuex/index.js

class Store {  
    static VueInstance = null;
}
function install(app, options = {}) {  
    Store.VueInstance = app;
}

export default {  Store,  install,};

解释一下Vuex/index.js 的雏形,Vue.use调用install方法,然后另外又导出了class Store,所以在入口文件中可以new Vuex.Store实例化

实现Vuex

准备好环境后,运行npm run dev,发现报错

Vue系列深入教程(二)-- 实现基本的vuex

很明显模板中用上了$store, 但是还未注入找不到就报错了,先注入一下,修改Vuex/index.js

class Store {  
    static VueInstance = null;  
    constructor(option) {    
        const { state = {}, getters = {}, mutations = {}, actions = {} } = option; // +++    
        const app = Store.VueInstance; // +++     
        app.mixin({ // +++      
            beforeCreate() {        
                if (this.$options.store) {          
                    app.prototype.$store = this.$options.store;        
                }      
            },    
        });  
}}

function install(app, options = {}) {  
    Store.VueInstance = app;
}

上述代码通过app.mixin向vue的原型注入$store,不知道app怎么来的同学请看上一篇文章

在入口文件把app输出一下,再次刷新界面,还是报错

Vue系列深入教程(二)-- 实现基本的vuex

Vue系列深入教程(二)-- 实现基本的vuex

此时虽然有了store,但是读取不到store,但是读取不到store,但是读取不到store.state.count 和 store.getters.count,先实现state吧,把store.getters.count,先实现state吧,把store.getters.count,先实现state吧,把store.getters.count那个div先注释

实现State

这里取上述第一种思路去实现state,要注意state不能直接修改,但是可以直接取值,所以这里使用代理模式去实现state

修改contrusctor,添加state的get 和 set方法

constructor(option) {    
    const { state = {}, getters = {}, mutations = {}, actions = {} } = option;    
    const app = Store.VueInstance;    
    this.vInstance = new app({ // +++      
        data: {        
            state      
        },    
    });
    // ...  
}  
get state() { // +++    
    return this.vInstance;  
}  
set state(val) { // +++    
    console.error("can not set state directly");  
}
  1. this.vInstance实现了一个小型vue,作为响应式的state
  2. 添加了state的set方法,直接设置state会报错
  3. 添加了state的get方法,代理this.vInstance

刷新浏览器可以看到

Vue系列深入教程(二)-- 实现基本的vuex

可以通过this.vInstance.state.count获得state中的count值,再次修改代码让state get方法指向this.vInstance.state

get state() {    
    return this.vInstance.state; // +++
}

刷新界面,state.count读取成功

Vue系列深入教程(二)-- 实现基本的vuex

在入口文件加上

store.state = 3;

Vue系列深入教程(二)-- 实现基本的vuex

直接修改state会报错,state的实现成功了,但是此时点击两个按钮都会报错,因为commit和dispatch方法还未实现

实现mutions和commit方法

思路

  1. 通过commit方法可以同步修改state中的某个值,commit有两个参数,第一个是对应在mutations中的方法名字,第二个是传给这个方法用的值payload
  2. 而mutations中的方法也有两个参数,第一个是state,第二个是commit时传进来的参数payload

实现

constructor(option) {
    // ...    
    this.vInstance = new app({      
        data: {        
            state,      
        },    
    });
    this.mutations = mutations // +++    
    this.commit = (muationName, payload) => { // +++      
        const method = mutations[muationName];      
        if (!method) {        
            console.error("no match mutation");      
        }      
        method.call(this, this.state, payload);    
    };
    // ...  
}

增加了实例的commit方法,增加了this.mutations

  1.  根据第一个参数找到对应在mutations中的方法,没找到报错
  2. 方法的this修正指向store实例,传入this.state和payload

刷新界面,测试一下commit方法

Vue系列深入教程(二)-- 实现基本的vuex

commit编写成功

实现actions和dispatch

mutations中的方法不支持异步操作,actions支持异步操作,而提交actions就需要dispatch方法

来看看官网对actions的介绍

Vue系列深入教程(二)-- 实现基本的vuex

思路

  1. dispatch有两个参数,第一个是对应actions中的方法名字,第二个是传给这个方法用的值payload
  2. 而actions中的方法也有两个参数,第一个参数是一个对象,具体看上图,第二个是dispatch时传进来的参数payload

实现

constructor(option) {    
    // ...    
    this.actions = actions;    
    this.dispatch = (actionName, payload) => {      
        const method = actions[actionName];      
        if (!method) {        
            console.error("no match mutation");      
        }      
        method.call(this,{
            state: this.state,
            commit: this.commit,
            dispatch: this.dispatch,        
        },payload);    
    };
    // ...  
}

增加了实例的dispatch方法,增加了this.actions

  1. 根据第一个参数找到对应在actions中的方法,没找到报错
  2. 方法的this修正指向store实例,第一个参数是一个对象,拥有state,commit,dispatch是哪个参数(其实不止三个,这里暂时需要这三个够了)
  3. 传入payload

刷新界面,测试一下dispatch方法

Vue系列深入教程(二)-- 实现基本的vuex

Vue系列深入教程(二)-- 实现基本的vuex

点击action按钮2s后,count值变为2,并且能访问到state和触发其它action

实现getters

看看官网对getters的介绍

Vue系列深入教程(二)-- 实现基本的vuex

思路

  1. 初始化的时候需要遍历传入的getters中的所有key,访问store.getters其实是访问传入的getters,所以这里又是代理,代理中把this.state,this.getters传入作为参数
  2. getters依赖state,state发生变化则getters应该立即变化,state不变则getters不应该变化,是不是跟vue的计算属性有点像?

实现

constructor(option) {    
    const { state = {}, getters = {}, mutations = {}, actions = {} } = option;    
    // ...    
    this.getters = {}; // 添加实例的getters属性    
    Object.keys(getters).forEach((k) => { // 遍历option的getters      
        Object.defineProperty(this.getters, k, { // 添加代理,访问实例的getters相当于访问option的getters        
            get: () => {          
                return getters[k](this.state, this.getters); // 传入state和getters,使用的时候可以获取到        
            },      
        });    
    });
    // ...  
}

刷新界面

Vue系列深入教程(二)-- 实现基本的vuex

getters实现成功?

getters问题

下面改一下app.vue

<template>  
    <div>    
        <button @click="$store.commit('setCount', 4)">mutations</button>
        <button @click="$store.dispatch('setCount', 2)">action</button>
        <div>count: {{ $store.state.count }}</div>
        <div>double1:{{ $store.getters.doubleCount }}</div>
        <div>double2:{{ $store.getters.doubleCount }}</div>
        <div>double3:{{ $store.getters.doubleCount }}</div>
    </div>
</template>

Vue系列深入教程(二)-- 实现基本的vuex

刷新界面,getter输出了3次

想想computed的效果,当依赖的值没发生变化的时候,不会重新触发,所以getters还未实现成功

解决方案

文中已经提了多次computed,所以轮到它出场了,修改代码如下

constructor(option) {    
    const { state = {}, getters = {}, mutations = {}, actions = {} } = option;    
    // ...    this.getters = {};    
    const computed = {}; // +++    
    Object.keys(getters).forEach((k) => {
        computed[k] = () => getters[k](this.state, this.getters); // +++      
        Object.defineProperty(this.getters, k, {
            get: () => {
                return this.vInstance[k]; // +++
            },
        });
    });
    this.vInstance = new app({
        data: {
            state,
        }, 
        computed, // +++    
    });
}
  1. 添加一个对象computed,拥有getters中所有的方法并传入state和getters
  2. 对象computed同时也传入到this.vInstance中作为计算属性,因为vue的计算属性的效果就是依赖值不发生变化,就不会重新生成计算属性
  3. 代理指向改成 this.vInstance, 因为vue本身就把计算属性代理到vue实例上,此时访问this.vInstance就可以访问到传入的getters的所有属性

刷新浏览器

Vue系列深入教程(二)-- 实现基本的vuex

模板中多次调用同一个getters不会再重新生成,而是像computed一样缓存下来了