MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue Vuex state、getters、mutations与actions的功能解析

2022-02-173.8k 阅读

Vuex 基础概念

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

在 Vuex 中,核心概念包括 state、getters、mutations 和 actions。理解这些概念对于有效地使用 Vuex 管理应用状态至关重要。

State

什么是 State

State 是 Vuex 中的状态存储,它就像一个全局的对象,用于存储应用中多个组件共享的状态数据。在 Vue 组件中,我们可以通过 $store.state 来访问 Vuex 中的 state。

State 的使用场景

想象一个电商应用,购物车的商品列表就是一个适合存储在 state 中的数据。多个组件,比如商品详情页、购物车页面等都可能需要访问和修改购物车的商品列表,这时将购物车数据存储在 state 中就非常合适。

代码示例

首先,我们创建一个简单的 Vuex 实例并定义 state。

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0
  }
});

export default store;

在 Vue 组件中访问 state:

<template>
  <div>
    <p>The count is: {{ count }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count;
    }
  }
};
</script>

在这个例子中,count 是 state 中的一个属性,通过 this.$store.state.count 可以在组件中访问到它。我们使用计算属性 count 来简化访问。

State 的响应式原理

Vuex 的 state 利用了 Vue 的响应式系统。当 state 中的数据发生变化时,依赖它的组件会自动更新。这是因为 Vue 在初始化 state 时,会使用 Object.defineProperty 将 state 中的属性转换为响应式数据。

模块中的 State

在大型应用中,我们可能需要将 state 按照功能模块进行划分。例如,一个博客应用可能有用户模块、文章模块等。我们可以这样定义模块中的 state:

// modules/user.js
const state = {
  userInfo: {
    name: '',
    age: 0
  }
};

export default state;

然后在主 store 中引入模块:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    user
  }
});

export default store;

在组件中访问模块中的 state:

<template>
  <div>
    <p>User name: {{ user.userInfo.name }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    user() {
      return this.$store.state.user;
    }
  }
};
</script>

Getters

什么是 Getters

Getters 相当于 Vue 组件中的计算属性。它用于从 state 中派生出一些状态,比如对 state 中的数据进行过滤、计算等操作。Getters 的值会根据它的依赖被缓存起来,只有当它的依赖值发生了改变才会重新计算。

Getters 的使用场景

继续以电商应用为例,假设 state 中存储了所有商品的列表,而在某些场景下,我们只需要显示有库存的商品。这时就可以使用 getters 来过滤出有库存的商品列表。

代码示例

在 store 中定义 getters:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    products: [
      { id: 1, name: 'Product 1', stock: 10 },
      { id: 2, name: 'Product 2', stock: 0 },
      { id: 3, name: 'Product 3', stock: 5 }
    ]
  },
  getters: {
    availableProducts(state) {
      return state.products.filter(product => product.stock > 0);
    }
  }
});

export default store;

在组件中使用 getters:

<template>
  <div>
    <ul>
      <li v-for="product in availableProducts" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  computed: {
    availableProducts() {
      return this.$store.getters.availableProducts;
    }
  }
};
</script>

在这个例子中,availableProducts 是一个 getters,它从 state.products 中过滤出了库存大于 0 的商品。在组件中通过 this.$store.getters.availableProducts 来访问这个 getters。

Getters 接受其他 Getters 作为参数

Getters 不仅可以依赖 state,还可以依赖其他 getters。例如,我们有一个计算商品总价的 getters,并且只想计算有库存商品的总价:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    products: [
      { id: 1, name: 'Product 1', stock: 10, price: 100 },
      { id: 2, name: 'Product 2', stock: 0, price: 200 },
      { id: 3, name: 'Product 3', stock: 5, price: 150 }
    ]
  },
  getters: {
    availableProducts(state) {
      return state.products.filter(product => product.stock > 0);
    },
    totalPriceOfAvailableProducts(state, getters) {
      return getters.availableProducts.reduce((total, product) => total + product.price * product.stock, 0);
    }
  }
});

export default store;

在组件中使用:

<template>
  <div>
    <p>Total price of available products: {{ totalPrice }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    totalPrice() {
      return this.$store.getters.totalPriceOfAvailableProducts;
    }
  }
};
</script>

这里 totalPriceOfAvailableProducts 依赖于 availableProducts getters。

Getters 在模块中的使用

在模块中定义 getters 时,需要注意命名空间。如果模块开启了命名空间,访问模块内的 getters 需要带上模块名。

// modules/product.js
const state = {
  products: [
    { id: 1, name: 'Product 1', stock: 10, price: 100 },
    { id: 2, name: 'Product 2', stock: 0, price: 200 },
    { id: 3, name: 'Product 3', stock: 5, price: 150 }
  ]
};

const getters = {
  availableProducts(state) {
    return state.products.filter(product => product.stock > 0);
  }
};

export default {
  state,
  getters
};

在主 store 中引入模块:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import product from './modules/product';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    product: {
      namespaced: true,
      ...product
    }
  }
});

export default store;

在组件中访问模块内的 getters:

<template>
  <div>
    <ul>
      <li v-for="product in availableProducts" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  computed: {
    availableProducts() {
      return this.$store.getters['product/availableProducts'];
    }
  }
};
</script>

Mutations

什么是 Mutations

Mutations 是 Vuex 中唯一允许修改 state 的地方。它是一个同步函数,通过提交 mutation 来修改 state,这样可以保证 state 的变化是可追踪和可预测的。

Mutations 的使用场景

在电商应用中,当用户添加商品到购物车时,就需要通过 mutation 来修改购物车的商品列表(即 state 中的购物车数据)。

代码示例

在 store 中定义 mutation:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  }
});

export default store;

在组件中提交 mutation:

<template>
  <div>
    <p>The count is: {{ count }}</p>
    <button @click="incrementCount">Increment</button>
  </div>
</template>

<script>
export default {
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    incrementCount() {
      this.$store.commit('increment');
    }
  }
};
</script>

在这个例子中,increment 是一个 mutation,通过 this.$store.commit('increment') 在组件中提交这个 mutation 来修改 state.count

Mutation 传递参数

Mutation 可以接受额外的参数,称为 payload(负载)。例如,在购物车应用中,添加商品到购物车时,需要传递商品的信息作为参数。

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    cart: []
  },
  mutations: {
    addToCart(state, product) {
      state.cart.push(product);
    }
  }
});

export default store;

在组件中提交 mutation 并传递参数:

<template>
  <div>
    <button @click="addToCart(product)">Add to Cart</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      product: { id: 1, name: 'Sample Product' }
    };
  },
  methods: {
    addToCart(product) {
      this.$store.commit('addToCart', product);
    }
  }
};
</script>

这里 product 就是 payload,传递给 addToCart mutation 来更新购物车。

多个 Mutation 调用

在实际应用中,可能需要在一个组件中调用多个 mutation。例如,在一个用户登录的场景中,可能需要更新用户信息并设置登录状态。

// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    user: null,
    isLoggedIn: false
  },
  mutations: {
    setUser(state, userData) {
      state.user = userData;
    },
    setLoggedIn(state, status) {
      state.isLoggedIn = status;
    }
  }
});

export default store;

在组件中调用多个 mutation:

<template>
  <div>
    <button @click="login">Login</button>
  </div>
</template>

<script>
export default {
  methods: {
    login() {
      const userData = { name: 'John Doe', age: 30 };
      this.$store.commit('setUser', userData);
      this.$store.commit('setLoggedIn', true);
    }
  }
};
</script>

Mutation 在模块中的使用

在模块中定义 mutation 时,如果模块开启了命名空间,提交 mutation 需要带上模块名。

// modules/user.js
const state = {
  user: null
};

const mutations = {
  setUser(state, userData) {
    state.user = userData;
  }
};

export default {
  state,
  mutations
};

在主 store 中引入模块:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    user: {
      namespaced: true,
      ...user
    }
  }
});

export default store;

在组件中提交模块内的 mutation:

<template>
  <div>
    <button @click="setUser">Set User</button>
  </div>
</template>

<script>
export default {
  methods: {
    setUser() {
      const userData = { name: 'Jane Smith', age: 25 };
      this.$store.commit('user/setUser', userData);
    }
  }
};
</script>

Actions

什么是 Actions

Actions 类似于 Mutations,不同的是 Actions 可以包含异步操作,比如调用 API。Actions 提交的是 mutation,而不是直接修改 state。

Actions 的使用场景

在电商应用中,当需要从服务器获取商品列表时,就需要使用 actions 来处理异步操作,获取数据后再通过 mutation 更新 state。

代码示例

假设我们使用 axios 来进行 API 调用,首先安装 axios:

npm install axios

在 store 中定义 action:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    products: []
  },
  mutations: {
    setProducts(state, products) {
      state.products = products;
    }
  },
  actions: {
    async fetchProducts({ commit }) {
      try {
        const response = await axios.get('/api/products');
        commit('setProducts', response.data);
      } catch (error) {
        console.error('Error fetching products:', error);
      }
    }
  }
});

export default store;

在组件中调用 action:

<template>
  <div>
    <button @click="fetchProducts">Fetch Products</button>
    <ul>
      <li v-for="product in products" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  computed: {
    products() {
      return this.$store.state.products;
    }
  },
  methods: {
    fetchProducts() {
      this.$store.dispatch('fetchProducts');
    }
  }
};
</script>

在这个例子中,fetchProducts 是一个 action,它通过 axios.get 异步获取商品数据,然后通过 commit 提交 setProducts mutation 来更新 state.products。在组件中通过 this.$store.dispatch('fetchProducts') 来调用这个 action。

Action 传递参数

Action 也可以接受参数,与 mutation 类似。例如,我们可能需要根据分类来获取商品列表,这时可以传递分类参数给 action。

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    products: []
  },
  mutations: {
    setProducts(state, products) {
      state.products = products;
    }
  },
  actions: {
    async fetchProductsByCategory({ commit }, category) {
      try {
        const response = await axios.get(`/api/products?category=${category}`);
        commit('setProducts', response.data);
      } catch (error) {
        console.error('Error fetching products:', error);
      }
    }
  }
});

export default store;

在组件中调用 action 并传递参数:

<template>
  <div>
    <button @click="fetchProducts('electronics')">Fetch Electronics Products</button>
    <ul>
      <li v-for="product in products" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  computed: {
    products() {
      return this.$store.state.products;
    }
  },
  methods: {
    fetchProducts(category) {
      this.$store.dispatch('fetchProductsByCategory', category);
    }
  }
};
</script>

这里 category 就是传递给 fetchProductsByCategory action 的参数。

组合 Action

在复杂应用中,可能需要多个 action 协同工作。例如,在用户注册场景中,可能需要先验证用户名是否可用,然后再进行注册。

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    user: null
  },
  mutations: {
    setUser(state, userData) {
      state.user = userData;
    }
  },
  actions: {
    async checkUsernameAvailability({ commit }, username) {
      try {
        const response = await axios.get(`/api/check-username?username=${username}`);
        return response.data.available;
      } catch (error) {
        console.error('Error checking username availability:', error);
        return false;
      }
    },
    async registerUser({ commit, dispatch }, userData) {
      const isAvailable = await dispatch('checkUsernameAvailability', userData.username);
      if (isAvailable) {
        try {
          const response = await axios.post('/api/register', userData);
          commit('setUser', response.data);
        } catch (error) {
          console.error('Error registering user:', error);
        }
      } else {
        console.error('Username is not available');
      }
    }
  }
});

export default store;

在组件中调用组合 action:

<template>
  <div>
    <button @click="register">Register</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      userData: { username: 'testuser', password: 'testpass' }
    };
  },
  methods: {
    register() {
      this.$store.dispatch('registerUser', this.userData);
    }
  }
};
</script>

这里 registerUser action 依赖于 checkUsernameAvailability action,先检查用户名可用性,再进行注册操作。

Action 在模块中的使用

在模块中定义 action 时,如果模块开启了命名空间,调用 action 需要带上模块名。

// modules/user.js
import axios from 'axios';

const state = {
  user: null
};

const mutations = {
  setUser(state, userData) {
    state.user = userData;
  }
};

const actions = {
  async login({ commit }, credentials) {
    try {
      const response = await axios.post('/api/login', credentials);
      commit('setUser', response.data);
    } catch (error) {
      console.error('Error logging in:', error);
    }
  }
};

export default {
  state,
  mutations,
  actions
};

在主 store 中引入模块:

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    user: {
      namespaced: true,
      ...user
    }
  }
});

export default store;

在组件中调用模块内的 action:

<template>
  <div>
    <button @click="login">Login</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      credentials: { username: 'testuser', password: 'testpass' }
    };
  },
  methods: {
    login() {
      this.$store.dispatch('user/login', this.credentials);
    }
  }
};
</script>

通过上述对 Vuex 中 state、getters、mutations 和 actions 的详细解析以及丰富的代码示例,希望读者能够深入理解并熟练运用这些概念来管理 Vue 应用的状态,构建出更加健壮和可维护的前端应用。