尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vuex 4 集中式状态管理实战指南:从核心概念到源码原理(Vue 3 官方状态管理库)

Vuex 4 集中式状态管理实战指南:从核心概念到源码原理(Vue 3 官方状态管理库) Vuex 4 集中式状态管理实战指南从核心概念到源码原理Vue 3 官方状态管理库【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuexVuex 是 Vue.js 官方出品的集中式状态管理方案它为应用中的所有组件提供一个统一、可预测的 store状态容器并借助 Vue 官方 devtools 扩展实现零配置的时间旅行调试与状态快照导出/导入。本文以本仓库Vuex 4.1.0面向 Vue 3的 README 与官方文档为主线结合 src/store.js 等核心源码与 examples 下的真实示例带你完整掌握 Vuex 的安装使用、五大核心概念state / getters / mutations / actions / modules以及底层实现原理读完即可在 Vue 3 项目中独立搭建可维护的集中式状态管理方案。Vuex 是什么状态管理模式 库Vuex 的定位是 a state management pattern library for Vue.js applications即一套状态管理模式外加对应的实现库。它的核心主张可以用一句话概括一个服务于应用所有组件的集中式 store配以明确的规则保证状态只能以可预测的方式被修改。Vuex 的 store 与普通的全局对象global object有两个本质区别详见 docs/guide/index.mdVuex store 是响应式的当组件从 store 中读取状态时如果 store 的状态发生变化组件会响应式地、高效地自动更新。不能直接修改 store 的状态改变 store 状态的唯一途径是显式地commit 一个 mutation。这条约定让每一次状态变更都留下可追踪的记录也为日志、快照、时间旅行调试等工具化能力提供了基础。这种设计带来的直接收益是单一数据源single source of truth、可预测的状态变更以及与官方 devtools 的深度集成——零配置即可获得时间旅行调试time-travel debugging和状态快照导出/导入能力这一点也是 README 中特别强调的核心卖点。生态现状Pinia 已成为新默认Vuex 3/4 继续维护在开始使用之前必须先了解本仓库 README 开头明确说明的生态背景这直接关系到技术选型Vue 官方状态管理库的新默认已经变更为Pinia其 API 与 Vuex 5 RFCvuejs/rfcs第 271 号提案所描述的 Vuex 5 几乎完全一致或更强可以简单地把 Pinia 理解为换了名字的 Vuex 5并且 Pinia 同样兼容 Vue 2.x。Vuex 3 与 Vuex 4 仍会被继续维护但不太可能再增加新功能。本仓库即 Vuex 4.1.0面向 Vue 3。Vuex 与 Pinia 可以安装在同一项目中因此将既有 Vuex 应用逐步迁移到 Pinia 是可行的渐进式路径但如果是全新项目官方强烈建议直接使用 Pinia。如果你正在维护存量 Vuex 4 项目本文介绍的全部 API 与机制依然有效且稳定理解 Vuex 的核心模型单一状态树、mutation/action 分层、命名空间模块对你迁移到 Pinia 也有直接的思维迁移价值。安装与快速开始安装方式根据 docs/installation.mdVuex 4 支持多种安装途径NPMnpm install vuexnext --savenext指向 Vue 3 兼容的 4.x 版本线本仓库 package.json 中peerDependencies声明vue: ^3.2.0即需要 Vue 3.2 及以上Yarnyarn add vuexnext --saveCDN / 直接下载通过 unpkg 等 NPM CDN 引入dist/vuex.global.js构建产物package.json 的unpkg与jsdelivr字段均指向该文件在 Vue 之后引入即可自动安装Dev Build开发版构建从 GitHub 克隆仓库后自行构建构建流程为yarn后执行yarn build对应 package.json 中的build脚本此外 package.json 的exports字段完整定义了各环境下的入口require对应dist/vuex.cjs.js、import对应dist/vuex.mjs、打包器 ESM 对应dist/vuex.esm-bundler.js、浏览器全局版对应dist/vuex.global.js同时types/index.d.ts提供 TypeScript 类型声明。创建最小的 store官方文档docs/guide/index.md给出了最小可用示例——只需提供一个初始state对象和若干mutationsimport { createApp } from vue import { createStore } from vuex // 创建一个新的 store 实例 const store createStore({ state () { return { count: 0 } }, mutations: { increment (state) { state.count } } }) const app createApp({ /* 根组件 */ }) // 将 store 作为插件安装 app.use(store)此后即可通过store.state读取状态、通过store.commit触发变更store.commit(increment) console.log(store.state.count) // - 1在组件内部则统一通过this.$store访问 store例如在组件方法中提交 mutationmethods: { increment() { this.$store.commit(increment) console.log(this.$store.state.count) } }需要说明的是之所以要用 commit mutation 而不是直接改store.state.count正是为了显式追踪每一次变更。这个简单的约定让代码意图更明确也让记录每次 mutation、拍摄状态快照、进行时间旅行调试成为可能。在组件中使用 store 状态的标准姿势是把它放进computed因为 store 状态是响应式的触发变更则是在组件方法中 commit mutation。核心概念之一State 与单一状态树Vuex 采用单一状态树single state tree设计详见 docs/guide/state.md整个应用级状态集中在一个对象中作为唯一数据源。通常每个应用只有一个 store。单一状态树的优势在于定位某块状态非常直接且便于为调试拍摄当前应用状态的快照。同时单一状态树与模块化并不冲突——后面会讲到如何把 state 和 mutations 拆分到子模块中。另外需要注意存入 Vuex 的数据必须遵循与 Vue 实例data相同的规则即状态对象必须是普通plain对象。在组件中获取状态由于 store 是响应式的最简单的方式是在计算属性中直接返回 store 状态const Counter { template: div{{ count }}/div, computed: { count () { return store.state.count } } }但这种方式会让组件依赖全局 store 单例在模块化系统中每个组件都要 import store测试时还需要 mock。因此 Vuex 通过 Vue 的插件机制把 store 注入到根组件下的所有子组件统一以this.$store暴露const Counter { template: div{{ count }}/div, computed: { count () { return this.$store.state.count } } }mapState辅助函数当组件需要用到多个状态或 getters 时逐个声明计算属性会非常啰嗦。mapState可以批量生成计算属性import { mapState } from vuex export default { computed: mapState({ // 箭头函数写法最简洁 count: state state.count, // 传字符串 count 等价于 state state.count countAlias: count, // 需要通过 this 访问组件局部状态时必须使用普通函数 countPlusLocalState (state) { return state.count this.localCount } }) }当映射的计算属性名与状态子树名一致时还可以直接传字符串数组computed: mapState([ // 将 this.count 映射到 store.state.count count ])由于mapState返回的是一个对象若想与组件自身的计算属性混用借助对象展开运算符即可computed: { localComputed () { /* ... */ }, // 用对象展开运算符混入 ...mapState({ // ... }) }从源码看mapState的实现位于 src/helpers.js它会为每个 key 生成一个mappedState函数读取this.$store.state传入 namespace 时则定位到对应模块的context.state当映射值是函数时以(state, getters)调用它否则直接取state[val]。值得注意的是生成的函数会被打上res[key].vuex true标记专门供 devtools 识别使用。组件仍可保留局部状态使用 Vuex 并不意味着把所有状态都塞进 store。虽然把更多状态放进 Vuex 会让变更更显式、更易调试但有时也会让代码更啰嗦、更间接。如果某块状态严格属于单个组件完全可以保留为组件局部状态——官方文档明确建议权衡取舍按应用的实际开发需求做决策。核心概念之二GettersGetters 相当于 store 的计算属性用于对 state 派生新状态。在 docs/guide/getters.md 中有完整讲解其基本形式为const store createStore({ state: { todos: [ { id: 1, text: ..., done: true }, { id: 2, text: ..., done: false } ] }, getters: { doneTodos (state) { return state.todos.filter(todo todo.done) } } })Getters 会作为store.getters暴露并作为系统级缓存属性computed存在——源码层面体现在 src/store.js 中store 持有EffectScope实例注册新 getters 时将其包裹在EffectScope内从而保证组件卸载时 gettercomputed不会被意外销毁。组件内可用mapGetters辅助函数批量映射import { mapGetters } from vuex export default { computed: { ...mapGetters([ doneTodos ]) } }从 src/helpers.js 的实现看mapGetters与mapState类似但会先把命名空间前缀拼接到 getter 名上val namespace val再生成对应的mappedGetter函数。核心概念之三Mutations 与可预测变更改变 store 状态的唯一方式是提交 mutation详见 docs/guide/mutations.md。Mutation 非常类似于事件每个 mutation 有一个字符串type和一个handlerhandler 接收 state 作为第一个参数在其中执行实际的状态修改const store createStore({ state: { count: 1 }, mutations: { increment (state) { state.count } } })不能直接调用 mutation handler——把它理解为事件注册当 type 为increment的 mutation 被触发时执行该 handler。触发方式是通过store.commit(increment)。携带 payload 提交可以给store.commit传第二个参数即 mutation 的payloadmutations: { increment (state, n) { state.count n } } // 触发 store.commit(increment, 10)大多数情况下 payload 应该是一个对象以便承载多个字段也让 devtools 中记录的 mutation 更具可读性mutations: { increment (state, payload) { state.count payload.amount } } store.commit(increment, { amount: 10 })对象风格提交Object-Style Commit也可以直接提交一个带type属性的对象。此时整个对象都会被作为 payload 传给 handlerstore.commit({ type: increment, amount: 10 })源码中src/store.js 的commit方法首先通过unifyObjectStyle位于 src/store-util.js统一两种调用风格再取出type、payload与options随后在this._withCommit(() { ... })包裹下遍历执行this._mutations[type]对应的所有 handler。_withCommit见 src/store.js负责置起_committing标志mutation 执行期间任何绕过 commit 的直接改值行为都会被严格模式检测到并发出警告。使用常量定义 mutation types在 Flux 各类实现中用常量定义 mutation types 是很常见的模式它能让代码充分利用 linter 等工具把所有常量集中到一个文件里也能让协作者一眼看清整个应用可能发生哪些变更// mutation-types.js export const SOME_MUTATION SOME_MUTATION// store.js import { createStore } from vuex import { SOME_MUTATION } from ./mutation-types const store createStore({ state: { ... }, mutations: { // 利用 ES2015 计算属性名语法用常量作为函数名 [SOME_MUTATION] (state) { // mutate state } } })是否使用常量很大程度上是个人偏好——在大项目、多开发者场景下很有帮助不喜欢也完全可以不用。Mutation 必须是同步的这是 Vuex 最重要的规则之一mutation handler 必须是同步函数。原因非常直接devtools 在记录每个 mutation 时需要分别捕获变更前和后的状态快照如果 mutation 内部发起异步回调再改状态回调在 commit 时尚未执行devtools 根本无法得知回调何时真正调用——回调里发生的一切状态变更本质上都不可追踪mutations: { someMutation (state) { api.callAsyncMethod(() { state.count // 这种写法是不可追踪的 }) } }因此 Vuex 中 mutations 是同步事务store.commit(increment)执行完成的那一刻该 mutation 可能引发的所有状态变更都应已发生。异步逻辑交给 actions 处理。在组件中提交mapMutations组件内可用this.$store.commit(xxx)或用mapMutations把组件方法映射为store.commit调用需要根 store 注入import { mapMutations } from vuex export default { methods: { ...mapMutations([ increment, // 将 this.increment() 映射为 this.$store.commit(increment) incrementBy // mapMutations 同样支持 payloadthis.incrementBy(amount) - commit(incrementBy, amount) ]), ...mapMutations({ add: increment // 将 this.add() 映射为 this.$store.commit(increment) }) } }从 src/helpers.js 的实现看mapMutations生成的方法会从this.$store取出commit命名空间场景下取模块的context.commit当映射值是函数时以[commit, ...args]调用它否则直接commit.apply(store, [val, ...args])。核心概念之四Actions 与异步流程Actions 与 mutations 相似但有两点关键区别详见 docs/guide/actions.mdActions 不直接修改状态而是提交commitmutationsActions 可以包含任意异步操作。一个最简单的 actionconst store createStore({ state: { count: 0 }, mutations: { increment (state) { state.count } }, actions: { increment (context) { context.commit(increment) } } })Action handler 接收一个 context 对象暴露与 store 实例相同的方法/属性集合可以用context.commit提交 mutation用context.state/context.getters访问状态甚至用context.dispatch调用其他 action。之所以是 context 对象而非 store 实例本身是为了在模块化场景下能拿到模块局部的 commit/state/getters详见下文 Modules。实践中常用 ES2015 解构简化写法actions: { increment ({ commit }) { commit(increment) } }触发 actionstore.dispatchActions 通过store.dispatch触发store.dispatch(increment)为什么要绕一圈 dispatch 而不是直接commit因为mutations 必须同步actions 不必——异步操作可以安全地放在 action 里actions: { incrementAsync ({ commit }) { setTimeout(() { commit(increment) }, 1000) } }Actions 支持与 mutations 相同的 payload 格式与对象风格 dispatch// 带 payload 的 dispatch store.dispatch(incrementAsync, { amount: 10 }) // 对象风格 dispatch store.dispatch({ type: incrementAsync, amount: 10 })一个真实的业务示例来自 examples/classic/shopping-cart/store/modules/cart.js是购物车结算调用异步 API 并提交多个 mutations包括失败回滚actions: { async checkout ({ commit, state }, products) { const savedCartItems [...state.items] commit(setCheckoutStatus, null) // 清空购物车乐观更新 commit(setCartItems, { items: [] }) try { await shop.buyProducts(products) commit(setCheckoutStatus, successful) } catch (e) { console.error(e) commit(setCheckoutStatus, failed) // 回滚到请求前保存的购物车 commit(setCartItems, { items: savedCartItems }) } } }可见 action 的职责是编排异步操作流程而把副作用状态变更统一通过 commit 记录下来。源码层面src/store.js 的dispatch会先统一对象风格参数依次触发订阅者的before回调然后执行 action handler当同一 type 存在多个 handler如跨模块时用Promise.all聚合最终返回一个 Promise在完成/失败时分别触发订阅者的after/error回调并 resolve/reject。在组件中分发mapActions组件内可用this.$store.dispatch(xxx)或用mapActions把组件方法映射为store.dispatch调用import { mapActions } from vuex export default { methods: { ...mapActions([ increment, // 将 this.increment() 映射为 this.$store.dispatch(increment) incrementBy // this.incrementBy(amount) - dispatch(incrementBy, amount) ]), ...mapActions({ add: increment // 将 this.add() 映射为 this.$store.dispatch(increment) }) } }组合 ActionsAction 常常是异步的如何知道它何时完成如何组合多个 action 处理更复杂的异步流程关键在于store.dispatch能处理 action handler 返回的 Promise并且 dispatch 本身也返回 Promiseactions: { actionA ({ commit }) { return new Promise((resolve, reject) { setTimeout(() { commit(someMutation) resolve() }, 1000) }) } } store.dispatch(actionA).then(() { // ... })在另一个 action 中组合actions: { actionB ({ dispatch, commit }) { return dispatch(actionA).then(() { commit(someOtherMutation) }) } }更优雅的是使用async / await// 假设 getData() 和 getOtherData() 都返回 Promise actions: { async actionA ({ commit }) { commit(gotData, await getData()) }, async actionB ({ dispatch, commit }) { await dispatch(actionA) // 等待 actionA 完成 commit(gotOtherData, await getOtherData()) } }值得注意一次store.dispatch可能同时触发不同模块中的多个 action handler此时返回值是一个 Promise它会在所有被触发的 handler 都 resolve 之后才 resolve。核心概念之五Modules 与命名空间当应用变大时可以把 store 拆分成模块modules。每个模块拥有自己的 state、getters、mutations、actions甚至可以嵌套子模块。模块化的 state 通过模块路径合并到根状态树中。示例中的购物车即展示了这种拆分examples/classic/shopping-cart/store/modules/cart.jsexport default { namespaced: true, state, getters, actions, mutations, modules: { nested } }namespaced: true声明模块为命名空间模块其内部 getters / actions / mutations 会以模块名/方法名的形式注册。示例中 cart 模块提交 products 模块的 mutation 时使用的commit(products/decrementProductInventory, { id }, { root: true })就是跨模块提交{ root: true }表示以根 store 为上下文提交。模块化使得 context 对象的设计有了实际意义模块内的 action 拿到的context.commit/context.state都是模块局部的。动态模块注册方面源码 src/store.js 提供了store.registerModule(path, rawModule, options)支持字符串或数组形式的模块路径options.preserveState可保留原状态与store.unregisterModule(path)注册后通过resetStoreState重建 gettersstore.hasModule(path)可查询模块是否已注册store.hotUpdate(newOptions)用于热更新模块定义。对应实现可进一步阅读 src/module/module-collection.js模块树注册与合并与 src/module/module.js单个模块的规范化以及 docs/guide/modules.md 的完整说明。源码原理Store 类与安装流程仓库的公开 API 全部汇聚在 src/index.js 中既提供默认导出对象也提供具名导出Store、storeKey、createStore、useStore、mapState、mapMutations、mapGetters、mapActions、createNamespacedHelpers、createLogger。其中createStore(options)只是new Store(options)的语法糖src/store.js。Store构造函数src/store.js依次完成解析plugins/strict/devtools选项初始化内部_actions、_mutations、_wrappedGetters、_modules、_subscribers等容器将dispatch/commit绑定到 store 自身保证解构后this正确通过installModule递归注册根模块与所有子模块通过resetStoreState初始化响应式状态并把 wrapped getters 注册为 computed最后依次执行所有plugins。install(app, injectKey)src/store.js是 Vue 插件安装入口app.provide(injectKey || storeKey, this)提供注入键app.config.globalProperties.$store this注册全局$store并依据devtools选项决定是否调用addDevtools(app, this)接入 devtools。其他实用 APIsubscribe订阅 mutation见 src/store.js、subscribeAction订阅 action 的 before/after/error 生命周期、watch(getter, cb, options)基于 Vue 的watch监听 getter 返回值、replaceState显式替换整个 statemutation 提交规范之外的状态回填/持久化恢复常用它。Devtools 集成时间旅行调试与状态快照的原理README 强调的零配置时间旅行调试、状态快照导出/导入在实现上依托于 src/plugins/devtool.js通过vue/devtools-api的setupDevtoolsPlugin注册 id 为org.vuejs.vuex的插件暴露 Vuex 检查器inspector与两条时间轴timeline层vuex:mutationsVuex Mutations和vuex:actionsVuex Actions。每个 mutation 提交后store.subscribe回调会把 mutation 的type、payload、state作为时间轴事件上报并同步刷新检查器树与状态面板每个 action 的before/after事件则被记录为带groupId的开始/结束事件并计算 action 耗时duration。检查器inspector会按模块树含命名空间标签展示 state 与 getters且支持在 devtools 面板中直接编辑状态on.editInspectorState内部走store._withCommit确保编辑也符合提交规范——这正是状态快照导出/导入和调试体验的底层支撑。额外值得一提的还有 src/plugins/logger.js 提供的createLogger插件默认只在开发环境生效可在控制台以组的形式打印每次 mutation 前后的状态快照与变更 diff常用于本地调试。结合 docs/guide/plugins.md 与 docs/guide/strict.md严格模式任何非 mutation 途径的状态修改都会在开发环境抛出错误可以搭建更完善的调试与约束体系。运行仓库自带的示例应用README 说明examples目录下带有多个基于 Vuex 构建的示例应用classic 与 composition 两套覆盖 counter、counter-hot、shopping-cart、chat、todomvc 等场景。运行方式$ npm install $ npm run dev # serve examples at localhost:8080其中npm run dev对应 package.json 中的node examples/server.js基于 express webpack-dev-middleware 的本地开发服务器。推荐先看最精简的 examples/classic/counter/store.js单一状态树 单个 mutation 的最小闭环再对照 examples/classic/shopping-cart/store/index.js 与 examples/classic/shopping-cart/store/modules/cart.js命名空间模块、跨模块 commit、异步结算理解模块化实战写法。仓库内另外还有一套 composition 版示例examples/composition演示与 Vue 3 Composition API 配合的useStore用法对应文档见 docs/guide/composition-api.md 与 src/injectKey.js。质量保障与工程化本仓库的工程化配置同样值得关注见 package.json测试npm test会依次执行 lint、build、类型测试tsc -p types/test、单元测试Jest覆盖 test/unit 下的 store、modules、helpers、hot-reload、module-collection 等模块、SSR 测试、e2e 测试Puppeteer 驱动 test/e2e 中的购物车/聊天/计数器/todomvc 场景以及 ESM 导入测试test/esm。多构建产物由 rollup.config.js 产出 CJS / ESM-bundler / ESM-browser / global 等多种格式配合types/index.d.ts、types/helpers.d.ts、types/logger.d.ts等声明文件类型用例见 types/test/index.ts。变更记录各版本详细变更记录在 CHANGELOG.md。总结Vuex 4 以单一状态树 mutation 同步提交 action 异步编排 模块化拆分 devtools 深度集成构建了一套完整、可预测的集中式状态管理模型。即使官方新默认已转向 Pinia本仓库所承载的 Vuex 3/4 仍将持续维护是存量 Vue 2/Vue 3 项目最稳定可靠的选型之一。通过本文你既可以在 docs/guide 的完整文档体系中对照学习每个核心概念的细节也可以顺着 src/store.js、src/helpers.js、src/plugins/devtool.js 等源码路径深入理解其实现原理还可以在 examples 中直接运行官方示例进行实战验证。【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表