VueX基础使用

0 阅读9分钟

状态管理工具

状态管理工具出现的背景

  • 组件间数据共享的问题
  • 单向数据流:
    • 多个视图依赖于同一个状态。传参方法:props,emit。对于多层嵌套的组件会非常繁琐,同时无法解决兄弟组件间的状态共享问题
    • 来自不同视图的行为需要变更同一个状态。事件方法:emit, on。可以采用父子组件直接引用或者通过事件来变更和同步状态的拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
  • 全局对象
    • 比如 window,也可以全局获取和修改,但是全局对象会污染全局环境,且多个组件可能都需要修改它,这样不利于维护。而且没办法让组件知道数据的变化,导致需要手动刷新组件。
// 在所有组件外层套一个父组件,所有子组件都可以共享子组件内的状态
const Count = {
	// 状态
	data() {
		return {
			count: 0,
		};
	},
	// 视图
	template: `
		<div>
			<button @click="count++">+1</button>
			{{count}}
		</div>
	`,
	methods: {
		increment() {},
	},
};

createApp(Count).mount("#app");
  • 状态管理工具(vuex, redux, mobx)

Vuex

什么是状态管理工具

  • 状态管理工具是一种集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

什么是 Vuex

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

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
  • Vuex 是集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  • Vuex 是响应式的,当数据变化时,视图会重新渲染。

核心思想

  • 集中式状态管理 Vuex 将所有组件的共享状态抽取出来,集中存储在一个 store 中,使状态的变更变得可追踪,可预测,并可统一管理

  • 状态响应式的实现原理 Vuex 内部使用 Vue 的响应式系统(Vue2 使用 Object.defineProperty,Vue3 使用 Proxy)来让state中的对象变为响应式的。组件从store中获取状态后,一旦state中的数据发生变化,那么组件也会自动更新。

Vuex 的基本使用

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

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

vuex

在 vuex 中,数据的流动是单向的,遵循State -> View → Action/Mutations → State 的循环

组件触发事件 -→ Action(处理异步逻辑) -→ Mutation(修改状态) --> State(存储数据) --> View(渲染 UI)

View(组件) │ dispatch(异步)/commit(同步) ↓ actions → mutations → state(被更新) ↑ getters(派生状态) │ View(再次获取新状态)

vuexUse

组件 dispatch → actions → commit → mutations → 修改 state(响应式)→ 组件自动更新

  • State: 存放共享数据的地方
  • Getters: 类似于计算属性,用于获取 state 中的数据
  • Mutations: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。每个 mutation 都有一个字符串的事件类型 (type) 和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
  • Actions: 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。

VueX 的核心概念

  • Vuex 的核心概念包括:state、getters、mutations、actions、modules。
  • Vuex 的状态存储是响应式的。当 vue 组件从 store 中读取状态的时候,如果 store 中的状态发生变化,那么响应的组件也会触发更更新。
  • Vuex 的状态存储是响应式的。当 vue 组件从 store 中读取状态的时候,如果 store 中的状态发生变化,那么响应的组件也会触发更更新。
  • 不能直接修改 store 中的状态,而是需要通过 mutation 来进行状态的修改,这是改变 store 状态的唯一方式(commit)。方便跟踪每一个状态的变化,从而让我们能够更好地理解我们的应用发生了什么变化,并且更好地进行调试。
import { createApp } from "vue";
import { createStore } from "vuex";

// 创建一个 store 实例
const store = createStore({
	state() {
		return {
			count: 0,
		};
	},
	mutations: {
		increment(state) {
			state.count++;
		},
	},
});

const app = createApp("#app");

// 将 store 实例作为插件注入到根组件
app.use(store);
  1. 接下来,可以在任何组建中通过store.xxx获取状态对象,并通过commit触发变更
store.commit("increment");
console.log(store.state.count); // -> 1

// 访问 store 实例
methods:{
	increment(){
		this.$store.commit("increment")
		console.info(this.$store.state.count) // -> 1
	}
}
  1. 我们可以直接从store中获取状态,但是,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,以一个全局 store 对象管理应用的所有状态,而不管状态是定义在哪个模块,这会导致代码变得难以维护。
// 一个Vue组件
const Counter = {
	template: `<span>Count is: {{ count }}</span>`,
	computed: {
		count() {
			return store.state.count;
		},
	},
};
  1. vuex 通过 Vue 的插件系统将store实例从根组件注入到每一个组件,实现组件与store的通信,从而实现组件与组件之间的状态共享。
const Counter = {
	template: `<span>Count is: {{ count }}</span>`,
	computed: {
		count() {
			// 子组件中直接访问 store 实例
			return this.$store.state.count;
		},
	},
};
  1. 当一个组件需要获取多个状态的时候,我们可以使用mapState 辅助函数帮助我们生成计算属性。
import { mapState } from "vuex";

export default {
	computed: {
		...mapState({
			count: (state) => state.count,
			// 可以直接传递一个字符串参数
			countAlias: "count", // 等价于 count: state => state.count
			// 为了能够使用 `this` 获取局部状态,必须使用常规函数
			countPlusLocalState(state) {
				return state.count + this.localCount; // this.localCount 是组件的局部状态
			},
		}),
	},
};

// 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
computed: {
	...mapState(["count"]),
},

// mapState 函数返回一个对象,因此可以与局部计算属性混合使用
computed: {
	localComputed() {},
	...mapState({
		count: (state) => state.count,
	}),
},

State: 定义了应用状态的数据结构,可以在这里设置默认的初始状态。

Getters: store 的计算属性

  • 允许组件从 Store 中获取状态,mapGetters 辅助函数可以将 store 中的 getter 映射到局部计算属性。
// getter 可以认为是 store 的计算属性,接受store为第一个参数
const store = createStore({
	state: {
		todos: [
			{ id: 1, text: "...", done: true },
			{ id: 2, text: "...", done: false },
		],
	},
	getters: {
		filterList(state) {
			return state.todos.filer((todo) => todo.done);
		},
	},
});

// 调用 getters
store.getters.filterList; // -> [{ id: 1, text: "...", done: true }]

/* ********** */

// getters 可以接受其他 getter 作为第二个参数
getters: {
  // ...
	todosCount(state, getters){
		return getters.filterList.length
	}
}
// 代码中访问
store.getters.todosCount; // -> 1

// 组件中访问
computed: {
  // ...
	todosCount() {
		return this.$store.getters.todosCount; // -> 1
	}
}
  • 通过方法访问
// 定义 getters
getters:{
	// ...
	getListById: (state) => (id) => {
		return state.todos.find(todo => todo.id === id)
	}
}

// 使用 getters
store.getters.getListById(1); // -> { id: 1, text: "...", done: true }

// 组件中访问
computed: {
  // ...
	getGetterListById() {
		return this.$store.getters.getListById(1);
	}
}
  • mapGetters 辅助函数,仅仅是将 store 中的 getter 映射到局部计算属性
import { mapGetters } from "vuex";
export default {
	// ...
	computed: {
		// 使用对象展开运算符将 getter 混入 computed 对象中
		// 调用前面写过的 getter 方法
		...mapGetters([
			"todosCount",
			"filterList",
			"getListById"
			// ...
		]),
	},
};
// 给 getter属性另取名字
...mapGetters({
	// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
	doneCount: "doneTodosCount",
})

Mutations: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation 是唯一允许更新应用状态的地方,它接受 state 作为第一个参数,接受额外参数作为第二个参数。

const store = createStore({
	state: {
		count: 0,
	},
	mutations: {
		increment(state) {
			// 变更状态
			state.count++;
		},
	},
});

// 调用 mutation
store.commit("increment"); // -> state.count + 1
  • 我们不能直接调用一个mutation处理函数,需要通过commit来触发一个mutation处理函数,mutation处理函数必须是同步函数。(这个选项更像事件注册)
  • 提交参数(payload):
mutations: {
	increment(state, n) {
		state.count += n;
	},
}

store.commit("increment", 233)

/* ******* */

// payload 应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
mutations: {
	increment(state, payload) {
		state.count += payload.amount;
	},
}
store.commit("increment", {
	amount: 10,
})

// 对象风格的提交方式
store.commit({
	type: "increment",
	amount: 10,
})
  • 作为代码规范,我们需要使用常量替代mutation事件类型
// mutation-type.js
export const SOME_MUTATION = "SOME_MUTATION";

// store.js
import Vuex from "vuex";
import { SOME_MUTATION } from "./mutation-type";
const store = new Vuex.Store({
	state: {
		//...
	},
	mutations: {
		// 使用 es2015 风格的计算属性命名功能来使用一个常量作为函数名
		[SOME_MUTATION](state, payload) {
			// mutate state
			state.count += payload.amount;
		},
	},
});
  • Mutation 必须是同步函数,这是为了调试和跟踪状态变化。
mutations: {
	increment(state) {
		setTimeout(() => {
			state.count++;
		}, 1000); // 异步操作不行
	}
	someMutation(state) {
	  api.callAsyncData(()=>{
		  state.count++; // 异步操作不行
		})
	}
}
  • mapMutations 辅助函数,在组件中提交mutation
import { mapMutations } from "vuex";
export default {
	// ...
	methods: {
		// 将 `this.increment()` 映射为 `this.$store.commit('increment')`
		...mapMutations(["increment"]),
		// 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
		...mapMutations({
			incrementBy: "increment",
		}),
	},
};

Actions:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。

  • Actions 提交的是 mutation,而不是直接变更状态。
  • Actions 可以包含任意异步操作。
const store = createStore({
	state: {
		count: 0,
	},
	mutations: {
		increment(state) {
			state.count++;
		},
	},
	actions: {
		increment(context) {
			// context 对象具有和 store 实例相同的方法属性,但并不是 store 实例本身
			context.commit("increment");
		},
	},
});

// 实际写法
actions: {
	increment({ commit }) {
		commit("increment");
	},
},
  • 使用 Action: store.dispatch("increment");
  • 可以在 Action 内部执行异步操作
// 使用 Action
store.dispatch("increment");

// Action 内部执行异步操作
actions: {
	incrementAsync({commit}){
		setTimeout(() => {
			commit("increment");
		}, 100);
	}
}

// 以 payload 的形式提交
store.dispatch("incrementAsync", {
	amount: 10,
})

// 以对象形式提交
store.dispatch({
	type: "incrementAsync",
	amount: 10,
})

/* ********* */
// e.g.:
actions:{
	checkout({ commit, state }, products) {
		// 将当前物品备份
		const savedCartItems = [...state.cart.added];
		// 发起 checkout 请求,然后使用备份的物品,清空购物车
		commit(types.CHECKOUT_REQUEST);

		shop.buyProducts(
			products,
			() => {
				// 请求成功
				commit(types.CHECKOUT_SUCCESS);
			},
			() => {
				// 请求失败,恢复 cart 状态
				commit(types.CHECKOUT_FAILURE, savedCartItems);
			}
		);
	},
};
  • 在组件中使用 actions
import { mapActions } from "vuex";
export default {
	methods: {
		...mapActions(["increment"]),
		// 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
		...mapActions({
			incrementBy: "increment",
		}),
	},
};
  • 组合多个 actions
// 如何组合多个 actions
actions:{
	actionA({ commit }) {
		return new Promise((res, rej) => {
			setTimeout(() => {
				commit("someMutation");
				res();
			}, 100);
		});
	},
};

store.dispatch("actionA").then(() => {
	// 做一些actionA执行完之后的事情...
});

// 在 actions 中分发其他 actions
actions: {
	// ...
	actionB({ dispatch, commit }) {
		return dispatch("actionA").then(() => {
			commit("someOtherMutation");
		});
	},
};

/* ****** */
// 更推荐结合 async/await
actions: {
  async actionA({commit}){
		commit('gotData',await getData())
	}
	async actionB({dispatch,commit}){
		await dispatch('actionA')
		// 等待 actionA 完成之后再执行其他操作
		commit('gotOtherData',await getOtherData())
	}
}

Modules: 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 Vuex 允许将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块。

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

// 挂载
const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
  • 模块的局部状态
const moduleA = {
  state:()=>{
		return{
			count:0,
			id:2333
		}
	}
	mutations:{
		// 这里 state 是模块的局部状态
		increment(state){
			state.count++
		}
	}
	actions:{
		// 对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
	  async increment({ state, commit, rootState }){
			await { data } = api.fetchData({id:state.id})
			// ...
		  if(state.count < 10){
			  commit('doubleCount')
		  }else{
			  rootState.count++
			}
	  }
	}
	// 模块内部的 getter,根节点状态会作为第三个参数暴露出来:
	getters:{
		sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
		doubleCount(state){
		  return state.count * 2
		}
	}
}

namespace 命名空间: 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的 —— 这样使得多个模块能够对同一 mutation 或 action 作出响应。 可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

// 命名空间
const store = createStore({
	modules: {
		account: {
			namespaced: true, // 开启命名空间
			state: () => {},
			getters: {
				isAdmin() {
					// 调用方法: getters['account/isAdmin']
					// getters['account/isAdmin']
				},
			},
			actions: {
				logIn() {
					// 调用方法: dispatch('account/logIn')
					// dispatch('account/logIn')
				},
			},
			mutations: {
				logOut() {
					// 调用方法: commit('account/logOut')
					// commit('account/logOut')
				},
			},
		},
	},
});

// 嵌套模块的命名空间
const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,
      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

结构

--- main.js
--- api
--- userData.js
--- HomeData.js
--- App.vue
--- User.vue
--- store
--- index.js  // 组装模板并导出 store 的地方

Vuex 对比组件通信

特性Vuex父子通信 / Event Bus / Props 等
适合场景跨组件复杂状态共享简单数据传递或局部状态管理
状态追踪清晰,支持时间旅行调试难以跟踪
数据流向单向可变(可能出现回调地狱)