Vuex


Vuex

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

🎬 Vuex 的核心概念和工作方式如下图所示

vuex
vuex

Vuex 是将多个组件共用的状态(数据)state 提取出来进行管理;而对于(一般是基础组件)组件独占的状态,应该使用组件的 data 进行管理。为了可以追踪 state 的变化,所以必须==遵循一个原则:更改 Vuex 中的状态 state 的唯一方法只允许是通过提交 commit mutation 来实现==。

⚠️ 这种模式导致组件依赖全局状态单例。

Play Video

安装引入

可以使用 CDN 引入

html
<script src="https://unpkg.com/vuex/dist/vuex.js"></script>

也可以通过 npm 安装,然后通过 Vue.use() 的方式安装插件

bash
npm install vuex --save
js
import Vuex from 'vuex'

Vue.use(Vuex)

Vuex 的核心就是仓库 store 基本上就是一个容器,它使用单一状态树,即每个 Vue 应用中只有一个 store 实例(对象),包含全部的应用层级共享的状态 state 和修改状态的相关方法。

💡 单状态树和模块化并不冲突

实例化一个仓库,其中存放着组件共享的状态和修改状态的相关方法

js
const store = new Vuex.Store({
  state: {},
  getters: {},
  mutations: {},
  actions: {}
})

🎬 为了在 Vue 组件中访问 this.$store property,需要为 Vue 实例提供创建好的 store。🎬 Vuex 提供了一个从根组件向所有子组件,以选项 store 的方式「注入」该 store 的机制:

js
new Vue({
  el: '#app',
  store: store,
})

Play Video

Play Video

State

state 是组件共享的的 🎬 单一状态树,遵循 single source of truth 唯一数据源原则。

在 store 的选项 state 中进行定义,类似于组件的 data 选项。

js
const store = new Vuex.Store({
  // 状态
  state: {
    numbers: [0, 1, 2, 3, 4, 5, 6]
  },
});

Play Video

🎬 在组件中通过 store.state.stateNamethis.$store.state.stateName 访问具体的状态(属性)。由于 Vuex 的状态存储是响应式的,所以一般将从 store 中读取的状态 state 作为组件计算属性,这样每当相应的 state 变化的时候, 相应的计算属性也会重新计算,并且触发更新相关联的 DOM。

Play Video

通过模块化构建的项目系统中,在多个组件都需要导入各种 state 的属性。🎬 Vuex 提供的辅助函数 mapState 更方便地生成计算属性,该函数可以传递对象,也可以传递数组作为参数:

js
import { mapState } from 'vuex'

// ...
// 为辅助函数 `mapState` 传递对象(键值对有多种方式)
computed: mapState({
  // 直接以 state 的属性的字符串形式作为值,键可以是任意名,这样可以重命名 state 的属性
  countAlias: 'count',
  // 使用箭头函数,以 state 作为传入的参数,返回所需的 state 中一个属性
  count: state => state.count,
  // 使用常规函数,以 state 作为传入的参数,这样能够通过 this 获取当前组件的局部状态(data 属性),返回一个新的混合状态
  countPlusLocalState(state) {
    return state.count + this.localCount
  }
})
js
import { mapState } from 'vuex'

// ...
// 为辅助函数 `mapState` 传递数组(映射的计算属性的名称与 state 的属性名相同)
// 计算属性只包含从 state 映射得到的属性
computed: mapState([
  'count'
])
js
import { mapState } from 'vuex'

// 如果组件的计算属性包含从 state 映射得到的属性,还包含组件中的局部计算属性
// mapState 函数返回的是一个对象
// 可以利用对象展开运算符解构 mapState 返回的对象,将它们与组件中的局部计算属性混合
computed: {
  localComputed() {...},
  ...mapState({...}),
  // 数组形式也一样
  ...mapState([...])
}

💡 存储在 Vuex 中的数据状态 state,和 Vue 实例中的 data 遵循相同的规则,对象必须是纯粹 plain

Play Video

Getter

🎬 Getter 是通过 state 派生出一些状态,它们定义在 store 的选项 getters 中,类似于组件的 computed 选项的作用。

Play Video

🎬 Getter 函数接受的第一个参数是 state 对象,(可选)接受 getters 作为第二个参数,它是一个包含其他 Getter 的对象,最后返回一个值或一个函数。

js
const store = new Vuex.Store({
  // 状态
  state: {
    numbers: [0, 1, 2, 3, 4, 5, 6]
  },
  // Getter
  getters: {
    oddNumbers(state) {
      return state.numbers.filter((num) => {
        return num % 2
      })
    },
    evenNumbers(state) {
      return state.numbers.filter((num) => {
        return (num % 2) - 1
      })
    },
    numbersLen: (state, getters) => {
      return getters.oddNumbers.length + getters.evenNumbers.length
    }
  }
});

Play Video

🎬 在组件中有两种方式使用 Getter:

  • 可以通过 store.getters.getterName 以属性的形式访问具体的 Getter
  • 也可以通过 store.getters.getterName(params) 以调用方法的形式访问具体的 Getter(对应地,在定义该 Getter 时,其返回值应该是一个函数

💡 当 Getter 通过属性的形式访问时,可以认为是 store 的 computed,有依赖缓存的功能,即只有当它的依赖值发生了改变才会被重新计算;而 Getter 通过方法的形式调用时,可以认为是 store 的 methods,每次都会去进行调用,而不会缓存结果。

Play Video

🎬 Vuex 提供的辅助函数 mapGetters 更方便地将 store 中的 getter 映射到组件的局部计算属性,该函数可以传递对象或字符串数组作为参数:

js
import { mapGetters } from 'vuex'

// ...
// 为辅助函数 `mapGetters` 传递数组(映射的计算属性的名称与 getters 的属性名相同)
computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
  ...mapGetters([
    'doneTodosCount',
    'anotherGetter',
  ])
}
js
import { mapGetters } from 'vuex'

// ...
// 如果想将重命名 getter 属性,为辅助函数 `mapGetters` 传递对象
computed: {
  ...mapGetters({
    // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
    doneCount: 'doneTodosCount'
  })
}

Play Video

Mutation

为了可以追踪 state 的变化,🎬 所以需要==遵循一个原则:更改 Vuex 中的状态 state 的唯一方法只允许是通过提交 commit mutation 来实现==。

Play Video

🎬 在 store 的选项 mutations 中,以函数的形式定义一个 mutation type(类似于事件类型) 及其 mutation handler(回调函数),在其中修改状态 state。

回调函数接受 state 作为第一个参数,(可选)第二个参数,称为载荷 payload,接收传递过来的数据。

js
const store = new Vuex.Store({
  // 状态
  state: {
    count: 1
  },
  mutations: {
    INCREMENT(state) {
      // 变更状态
      state.count++
    },
    SET_COUNT(state, n) {
      // 变更状态
      state.count = n
    }
  }
});

Play Video

💡 ==推荐 mutationType 使用常量(大写),并赋值给变量==,而在定义 mutation handler 时采用计算型属性名,🎬 这样既可以使在输入变量时 linter 之类的工具发挥作用,也可以避免重复代码

Play Video

🎬 ==在 mutation handler 中的操作必须是同步的==,这样 devtools 才会准确地捕抓到每一条 mutation 前一状态和后一状态的快照。

Play Video

Play Video

🎬 在组件中类似于触发事件一样,去提交 commit 一个特定 Mutation 事件类型,这样就会执行相应的 mutation handler 修改 state 的对应属性,传入(可选)第二个参数 payload,作为数据传递给 mutation handler

js
store.commit('SET_COUNT', { count: 10 })

💡 也可以使用对象风格来提交 Mutation,传递一个包含 type 属性的对象,它指定了 muationType,其他属性作为 payload 传递给 handler

js
store.commit({
  type: 'SET_COUNT',
  count: 10
})

🎬 Vuex 提供的辅助函数 mapMutations 更方便地将 store 中的 mutation 映射到组件的局部方法中,这样方便后续直接在组件中通过调用相应的方法就实现提交 mutation,该函数可以传递对象或字符串数组作为参数:

js
import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    // 数组形式
    ...mapMutations([
      // 将 this.increment() 映射为 this.$store.commit('increment')
      // 调用方法时支持载荷,将 this.increment(amount) 映射为 this.$store.commit('increment', amount)
      'increment',
    ]),
    // 对象形式,可重命名 method
    ...mapMutations({
      add: 'increment' // 将 this.add() 映射为 this.$store.commit('increment')
    })
  }
}

Play Video

Play Video

Action

🎬 Action 类似于 mutation,也是函数,不同在于:Action 一般用于提交 mutation,而不是直接变更状态,而且 Action 可以包含异步操作;而 Mutation 中只能有同步操作。

Play Video

🎬 在 store 的选项 actions 中以函数的形式定义一个 action type(类似于事件类型)及其 action handler(回调函数),在其中执行异步操作和提交 mutation

回调函数接受 context 作为第一个参数,它与 store 实例具有相同方法和属性的对象,因此可以通过调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters。(可选)第二个参数,称为载荷 payload,接收传递过来的数据。

js
const store = new Vuex.Store({
  // ...
  actions: {
    increment (context, payload) {
      context.commit('INCREMENT', payload)
    }
  }
});

Play Video

💡 Action 中通常存在异步操作,🎬 因此在组件中分发 action 时 Vuex 默认返回一个 Promise,但是如果在 action handler 回调函数显式地返回 Promise return new Promiser((resolve, reject) => {}),这样便可以对异步操作有更多的控制(何时 resolve 返回异步操作结果),可以在组件中监听回调函数的 Promise 执行情况,在 then() 中执行后续操作(或使用 async-await 结构):

js
const store = new Vuex.Store({
  // ...
  actionA ({ commit }) {
    // 定义一个 action 操作,它返回一个 Promise
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
});
js
// 在组件中使用
store.dispatch('actionA').then(() => {
  // ...
})

Play Video

🎬 在组件中类似于分发事件一样,去分发 dispatch 一个特定 Action 事件类型,这样就会执行相应的 action handler,传入(可选)第二个参数作为 payload(一般是对象),以传递数据到 action handler

js
store.dispatch('actionType')

Vuex 提供的辅助函数 mapActions 更方便地将 store 中的 action 映射到组件的局部方法中,这样方便后续直接在组件中通过调用相应的方法就实现分发 action,该函数可以传递对象或字符串数组作为参数:

js
import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    // 数组形式
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
      'incrementBy'
    ]),
    // 对象形式
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

Play Video

Module

Vuex 允许我们将 store 分割成模块 module,每个模块拥有自己的 statemutationsactionsgetters,也支持嵌套子模块。

在 store 的选项 modules 中注册模块

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

const store = new Vuex.Store({
    modules: {
      a: moduleA,
    }
})

在组件中可以结合模块名 store.state.a 访问相应的模块 a 的局部状态

⚠️ 模块中的状态 state 应该使用函数形式,并返回一个对象,包含局部状态的属性。

局部状态

在模块中,Mutation 和 Getter 接收的第一个参数是模块里局部状态的对象。

js
const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // `state` is the local module state
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

如果需要在 Getter 中访问根节点的状态 root state,则是在第三个参数暴露出来

js
const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

对于模块中的 Action 局部状态是通过 context.state 暴露出来,根节点状态是通过 context.rootState 暴露出来的:

js
const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

💡 根状态 rootState 其实包含了加载的模组的局部状态(因此模组中可以 🎬「借助」rootState 作为中间者,来访问其他各个注册的模组的局部状态,例如通过 rootState.a 访问 a 模块的局部状态)

Play Video

命名空间

模块的状态本来就是局部的,在组件中(或父模块中)需要使用 store.state.a 进行访问 a 模块的状态。🎬 但模块内部的 actionsmutations getters 是注册在全局命名空间,使得多个模块能够对同一 store.commit('mutationType')store.dispatch('actionName') 作出响应。

Play Video

🎬 如果希望模块具有更高的封装度和复用性(它的 Getter、Action、Mutation 不会与全局注册的冲突),可以通过在模块中添加选项 namespaced: true 的方式,使其成为带命名空间的模块。

js
const moduleA = {
  namespaced: true,
  // ...
}

开启命名空间后,模块的 gettersactions mutations 都变成局部的,因此在组件中如果要针对该模块访问 Getter、提交 Mutation、分发 Action,则需要根据该模块的注册路径进行相应调用:

  • getters['moduleName/stateName']
  • dispatch('moduleName/actionType')
  • commit('moduleName/mutationType')

当然如果在模块内部访问 Getter、提交 Mutation、分发 Action 则不需要添加路径「前缀」,这样的设计也更便于模块的迁移和复用。

而如果要在带命名空间的模块内针对全局空间的 action 或提mutation 进行分发和提交,则相应地需要{ root: true } 作为第三参数传给 dispatchcommit

js
modules: {
  foo: {
    namespaced: true,
    actions: {
      // actions 被局部化了
      // 可以通过 `rootGetters` 访问全局空间的 getters
      someAction ({ dispatch, commit, getters, rootGetters }) {
        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
    }
  }
}

如果希望在开启了命名空间的模块内注册全局的 Action 或 Mutation,可以为它们添加选项 root 并将值设置为 true

js
modules: {
  foo: {
    namespaced: true,
    actions: {
      someAction: {
        root: true,
        handler (namespacedContext, payload) { ... } // -> 'someAction'
      }
    }
  }
}

💡 在组件中,使用 mapStatemapActions 等辅助函数进行映射时,可以添加命名空间模块的路径作为第一个参数,以简化调用代码

js
// 组件
computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo',
    'bar'
  ])
}

Play Video

动态注册模块

🎬 在 store 创建之后,仍可以使用 store.registerModule 方法注册模块;可以使用 store.unregisterModule(moduleName) 来动态卸载模块,🎬 但能使用此方法卸载静态模块(即创建 store 时在配置对象中就声明的模块)

js
const store = new Vuex.Store({ /* 选项 */ })

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

💡 可以通过 store.hasModule(moduleName) 方法检查该模块是否已经被注册到 store

Play Video

Play Video

🎬 在卸载一个 module 时,有可能想保留过去的 state,以便重新注册时再使用原有的局部状态,例如从一个服务端渲染的应用保留 state。

可以在注册模组是设置选项 preserveState 将局部状态归档,即模块被注册时,它的 actionsmutations getters 会被添加到 store 中,但是 state 不会。这里假设 store 里原来的 state 已经包含了这个 module 的 state 并且你不希望将其覆写

js
store.registerModule('a', module, {
    preserveState: true
});

Play Video


Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes