Vuex的使用及原理实现

249 阅读5分钟

Vuex使用及原理

1.概念

在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。

什么是状态管理

开发对于应用程序的各种数据如何保存,我们称之为状态管理

2.何时使用

  • 多个组件共享数据时

image-20220116133804723

3.搭建vuex环境

1.安装Vuex

npm install vuex@next

2.创建文件参加传入配置项

创建文件:src/store/index.js

import { createStore } from "vuex"; //引入Vuex

//创建并暴露store
export default createStore({
    state: {},
    //准备mutations对象——修改state中的数据
    mutations: {},
    //准备actions对象——响应组件中用户的动作
    actions: {},
    //准备state对象——保存具体的数据
    modules: {},
});

3.在main.js中使用store

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

createApp(App).use(store).use(router).mount("#app");

4.使用须知

每一个Vuex应用的核心就是store(仓库)

store本质上是一个容器,它包含着你的应用中大部分的状态(state),状态存储是响应式的

若store中的状态发生变化,那么对应的组件依赖的store的状态也会被更新

4.基本使用

options API使用Vuex

1.dispatch 方法,派发给action一个叫change的方法

methods: {
    handleClick() {
      //dispatch 和 actions 做关联
      this.$store.dispatch("change", "hello world");
    }
  }

2.action里面的change方法感知并执行将方法提交给mutations

actions: {
    change(store, str) {
        setTimeout(() => {
            // commit 和 mutation 做关联
            store.commit("change", str)
        }, 2000)
    }
}

3.mutation 感知到提交的change改变,执行 change 方法改变数据

state() {
    return {
        name: “yunmu",
    };
},
mutations: {
    // mutation 里面只允许写同步代码,不允许写异步代码
    // 这是因为devtool工具会记录mutation的前一状态和后一状态的快照的日记
    // 但是在mutation中执行异步操作,就无法追踪到数据的变化
    change(state, payload) {
        state.name = payload;
    }
},

组件中读取vuex中的数据:$store.state.name

组件中修改vuex中的数据:$store.dispatch('action中的方法名',数据)$store.commit('mutations中的方法名',数据)

备注:若没有网络请求或其他业务逻辑,组件中也可以越过actions,即不写dispatch,直接编写commit

Action 通常是异步的,那么如何知道 action 什么时候结束呢?

我们可以让action返回Promise

// store
getHomedata(context) {
    return new Promise((resolve, reject) => {
        axios
            .get("http://123.207.32.32:8000/home/multidata")
            .then((res) => {
            context.commit("addBannerData", res.data.data.banner.list);
            resolve({ name: "coderwhy", age: 18 });
        })
            .catch((err) => {
            reject(err);
        });
    });
},


setup() {
    const store = useStore();

    onMounted(() => {
      // store.dispatch返回对应action里的promise
      const promise = store.dispatch("getHomeMultidata");
      promise
        .then((res) => {
          console.log(res);
        })
        .catch((err) => {
          console.log(err);
        });
    });
  },

Composition API使用Vuex

<script>
import { useStore } from "vuex";
export default {
    setup() {
        const store = useStore();
        const handleClick = () => {
            store.dispatch("change", "hello world");
            //store.commit("change", "hello world");
        	}
    	}
	}
</script>

5.getters的使用

1.概念:当state中的数据需要经过加工后再使用时,可以使用getters加工。

2.在store.js中追加getters配置

import { createStore } from "vuex";

const store = createStore({
  state() {
    return {
      name: "yunmu"
      books: [
        { name: "深入Vuejs", price: 200, count: 3 },
        { name: "深入Webpack", price: 240, count: 5 },
        { name: "深入React", price: 130, count: 1 },
        { name: "深入Node", price: 220, count: 2 },
      ],
    };
  },
  getters: {
    totalPrice(state, getters) {
      let totalPrice = 0;
      for (const book of state.books) {
        totalPrice += book.count * book.price;
      }
      return totalPrice + getters.myName;
    },
    myName(state) {
      return state.name
    },
  },
});

export default store;

3.组件中读取数据:$store.getters.totalPrice

注意:getters中的函数本身可以返回函数,使用的时候调用函数即可

6.派发风格

action和mutation都可以使用对象形式进行派发

this.$store.commit({
    type: "increment",
    name: "yunmu",
    age: 18,
});

this.$store.dispatch({
    type: "increment",
    name: "yunmu",
    age: 18,
});

7.模块化+命名空间

目的:解决单一状态树过于臃肿的问题,让代码更好维护,让多种数据分类更加明确。

store

const countAbout = {
  namespaced:true,//开启命名空间
  state:{...},
  mutations: { ... },
  actions: { ... 
 	incrementAction({ commit, dispatch, state, rootState, getters,rootGetters}) {}
   },
  getters: {
    bigSum(state){
       return state.sum * 10
    }
    doubleHomeCounter(state, getters, rootState, rootGetters) {
        // 对根状态提交
        commit("increment", null, { root: true });
     }
  }
}

const personAbout = {
  namespaced:true,//开启命名空间
  state:{ ...,  },
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    countAbout,
    personAbout,
  },
});

开启命名空间后,组件中读取state数据:

//方式一:自己直接读取
this.$store.state.personAbout.list;
//方式二:借助mapState读取:
...mapState("countAbout", ["sum", "school", "subject"]);
// 方式三:借助createNamespacedHelpers映射出该模块的mapState等
const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers("countAbout")

开启命名空间后,组件中读取getters数据:

//方式一:自己直接读取
this.$store.getters["personAbout/firstPersonName"];
//方式二:借助mapGetters读取:
...mapGetters("countAbout", ["bigSum"]);

开启命名空间后,组件中调用dispatch

//方式一:自己直接dispatch
this.$store.dispatch("personAbout/addPersonWang", person);
//方式二:借助mapActions:
...mapActions("countAbout", { incrementOdd: "jiaOdd", incrementWait: "jiaWait" });

开启命名空间后,组件中调用commit

//方式一:自己直接commit
this.$store.commit("personAbout/ADD_PERSON", person);
//方式二:借助mapMutations:
...mapMutations("countAbout", { increment: "JIA", decrement: "JIAN" });

8.四个map方法的使用

1.**mapState方法:**用于帮助我们映射state中的数据为计算属性

// store
state() {
    return {
      counter: 100,
      name: "yunmu",
 }

computed: {
    //借助mapState生成计算属性:yunmu、age(对象写法)
      ...mapState({
          sCounter: (state) => state.counter,
          sName: (state) => state.name,
    	}),
         
    //借助mapState生成计算属性:yunmu、age(数组写法)
    ...mapState(["counter", "name"]),
},

2.mapGetters方法:用于帮助我们映射getters中的数据为计算属性

// store
getters: {
    nameInfo(state) {
      return `name: ${state.name}`;
    },
    ageInfo(state) {
      return `age: ${state.age}`;
    },
}



computed: {
    //借助mapGetters生成计算属性:bigSum(对象写法)
  	...mapGetters({
       sNameInfo: "nameInfo",
       sAgeInfo: "ageInfo",
    }),

    //借助mapGetters生成计算属性:bigSum(数组写法)
    ...mapGetters(["nameInfo", "ageInfo"]),
},

3.**mapActions方法:**用于帮助我们生成与actions对话的方法,即:包含$store.dispatch(xxx)的函数

methods:{
    //靠mapActions生成:incrementOdd、incrementWait(对象形式)
    ...mapActions({incrementOdd: "jiaOdd",incrementWait: "jiaWait"})

    //靠mapActions生成:incrementOdd、incrementWait(数组形式)
        ...mapActions(["jiaOdd", "jiaWait"])
}

// setup里使用
import { mapActions } from "vuex";
setup() {
    const actions1 = mapActions(["incrementAction", "decrementAction"]);
    const actions2 = mapActions({
        add: "incrementAction",
        sub: "decrementAction",
    });

    return {
        ...actions1,
        ...actions2,
    };
},

4.**mapMutations方法:**用于帮助我们生成与mutations对话的方法,即:包含$store.commit(xxx)的函数

methods:{
    //靠mapActions生成:increment、decrement(对象形式)
    ...mapMutations({increment: "JIA",decrement: "JIAN"}),
    
    //靠mapMutations生成:JIA、JIAN(对象形式)
    ...mapMutations(["JIA", "JIAN"]),
}

// setup里使用
import { mapMutations } from "vuex";
setup() {
    const storeMutations1 = mapMutations(["increment", "decrement"]);
    const storeMutations2 = mapMutations({
          add: "increment",
        }),
    return {
        ...storeMutations1,
        ...storeMutations2,
    };
}

备注:mapActions与mapMutations使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则参数是事件对象。

setup使用map的两个映射方法mapStatemapGetters需要封装才能使用

hooks/useState.js

import { computed } from "vue";
import { mapState, useStore } from "vuex";

export function useState(mapper) {
  // 拿到store独享
  const store = useStore();

  // 获取到对应的对象的functions: {name: function, age: function}
  const storeStateFns = mapState(mapper);

  // 对数据进行转换
  const storeState = {};
  Object.keys(storeStateFns).forEach((fnKey) => {
    const fn = storeStateFns[fnKey].bind({ $store: store });
    storeState[fnKey] = computed(fn);
  });

  return storeState;
}

hooks/useGetters

import { computed } from "vue";
import { mapGetters, useStore } from "vuex";

export function useGetters(mapper) {
  // 拿到store独享
  const store = useStore();

  // 获取到对应的对象的functions: {name: function, age: function}
  const storeStateFns = mapGetters(mapper);

  // 对数据进行转换
  const storeState = {};
  Object.keys(storeStateFns).forEach((fnKey) => {
    const fn = storeStateFns[fnKey].bind({ $store: store });
    storeState[fnKey] = computed(fn);
  });

  return storeState;
}

代码逻辑重复进行封装

hooks/useMapper.js

import { computed } from "vue";
import { useStore } from "vuex";

export function useMapper(mapper, mapFn) {
  // 拿到store独享
  const store = useStore();

  // 获取到对应的对象的functions: {name: function, age: function}
  const storeStateFns = mapFn(mapper);

  // 对数据进行转换
  const storeState = {};
  Object.keys(storeStateFns).forEach((fnKey) => {
    const fn = storeStateFns[fnKey].bind({ $store: store });
    storeState[fnKey] = computed(fn);
  });

  return storeState;
}

hooks/useState.js

import { mapState } from "vuex";
import { useMapper } from "./useMapper";

export function useState(mapper) {
  return useMapper(mapper, mapState);
}

hooks/useGetters

import { mapGetters, createNamespacedHelpers } from "vuex";
import { useMapper } from "./useMapper";

export function useState(mapper) {
  return useMapper(mapper, mapGetters);
}

进一步对其完善支持模块化

hooks/useState.js

import { mapState, createNamespacedHelpers } from "vuex";
import { useMapper } from "./useMapper";

export function useState(moduleName, mapper) {
  let mapperFn = mapState;
  if (typeof moduleName === "string" && moduleName.length > 0) {
    mapperFn = createNamespacedHelpers(moduleName).mapState;
  } else {
    mapper = moduleName;
  }

  return useMapper(mapper, mapperFn);
}

hooks/useGetters

import { mapGetters, createNamespacedHelpers } from "vuex";
import { useMapper } from "./useMapper";

export function useGetters(moduleName, mapper) {
  let mapperFn = mapGetters;
  if (typeof moduleName === "string" && moduleName.length > 0) {
    mapperFn = createNamespacedHelpers(moduleName).mapGetters;
  } else {
    mapper = moduleName;
  }

  return useMapper(mapper, mapperFn);
}

9.Vuex 模拟实现

回顾基础示例,自己模拟实现一个 Vuex 实现同样的功能

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    count: 0,
    msg: "Hello World",
  },
  getters: {
    reverseMsg(state) {
      return state.msg.split("").reverse().join("");
    },
  },
  mutations: {
    increate(state, payload) {
      state.count += payload.num;
    },
  },
  actions: {
    increate(context, payload) {
      setTimeout(() => {
        context.commit("increate", { num: 5 });
      }, 2000);
    },
  },
});

实现思路

  • 实现 install 方法

    • Vuex 是 Vue 的一个插件,所以和模拟 VueRouter 类似,先实现 Vue 插件约定的 install 方 法
  • 实现 Store 类

    • 实现构造函数,接收 options
    • state 的响应化处理
    • getter 的实现
    • commit、dispatch 方法

完整源码

Myvuex/index.js

let _Vue = null

class Store {
  constructor (options) {
    const {
      state = {},
      getters = {},
      mutations = {},
      actions = {}
    } = options
    this.state = _Vue.observable(state)
    this.getters = Object.create(null)
    Object.keys(getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => getters[key](state)
      })
    })
    this._mutaions = mutations
    this._actions = actions
  }

  commit (type, payload) {
    this._mutaions[type](this.state, payload)
  }

  dispatch (type, payload) {
    this._actions[type](this, payload)
  }
}

function install (Vue) {
  _Vue = Vue
  _Vue.mixin({
    beforeCreate () {
      if (this.$options.store) {
        _Vue.prototype.$store = this.$options.store
      }
    }
  })
}

export default {
  Store,
  install
}

将store/index.js中的vuex的导入替换成myvuex

import Vuex from "../myvuex"
// 注册插件
Vue.use(Vuex)

10.购物车案例

Vue/vuex-cart-demo · 云牧/exampleCode - 码云 - 开源中国 (gitee.com)

1. 模板

地址:github.com/goddlts/vue…

用到了ElementUI、Vuex、Vue-Router

项目根目录下的server.js文件是一个node服务,为了模拟项目接口。

页面组件和路由已经完成了,我们需要使用Vuex完成数据的交互。

三个组件:

  • 商品列表组件
  • 购物车列表组件
  • 我的购物车组件(弹出窗口)

2. 商品列表组件

  • 展示商品列表
  • 添加购物车

3. 我的购物车组件

  • 购买商品列表
  • 统计购物车中的商品数量和价格
  • 购物车上的商品数量
  • 删除按钮

4. 购物车组件

  • 展示购物车列表
  • 全选功能
  • 增减商品功能和统计当前商品的小计
  • 删除商品
  • 统计选中商品和价格

5. Vuex插件介绍

  • Vuex的插件就是一个函数
  • 这个函数接受一个store参数

这个参数可以订阅一个函数,让这个函数在所有的mutation结束之后执行。

const myPlugin = store => {
  // 当store初始化后调用
  store.subscribe((mutation, state) => {
    // 每次mutation之后调用
    // mutation的格式为{ type, payload }
  })
}

Store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import products from './modules/products'
import cart from './modules/cart'
Vue.use(Vuex)

const myPlugin = store => {
  store.subscribe((mutation, state) => {
    if (mutation.type.startsWith('cart/')) {
      window.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))
    }
  })
}

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    products,
    cart
  },
  plugins: [myPlugin]
})