Vue3 (三): 速记笔记之Vuex (Vue3匹配4.x版本)

原创
2022/06/27 17:54
阅读数 650

1. 简单的 store 模式

const store = {

  debug: true,


  state: reactive({

    message: 'Hello!'

 }),


  setMessageAction(newValue) {

    if (this.debug) {

      console.log('setMessageAction triggered with', newValue)

 }


    this.state.message = newValue

 },


  clearMessageAction() {

    if (this.debug) {

      console.log('clearMessageAction triggered')

 }


    this.state.message = ''

 }

}

 

需要注意,所有 store state 的变更,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发。当错误出现时,现在也会有一个 log 记录 bug 之前发生了什么。

<div id="app-a">{{sharedState.message}}</div>


<div id="app-b">{{sharedState.message}}</div>


const appA = createApp({

  data() {

    return {

      privateState: {},

      sharedState: store.state

 }

 },

  mounted() {

    store.setMessageAction('Goodbye!')

 }

}).mount('#app-a')


const appB = createApp({

  data() {

    return {

      privateState: {},

      sharedState: store.state

 }

 }

}).mount('#app-b')

 

2. Vuex sotre:

export default new Vuex.Store({

state: {}, //状态值

mutations: {}, //修改状态

actions: {}, //接口异步请求, 服务端请求数据

modules: {}

})

 

PS Actions 获取数据->Mutations 修改处理->State 状态更新->Vue 组件渲染;

 

* 执行 mutations 用 commit 方法;

3. State 状态设置

3.1 普通设置方式:

//状态值:store/index.js

state: {

 count : 0,

 name : 'Mr.Lee',

 age : 100,

 gender : ''

 },

    //修改状态

 mutations: {

 setAge(state, value) {

 state.age = value

 }

 },

    //如果不想直接用繁琐的插值,可以先用计算属性:Person.vue

 computed : {

 name() {

            return this.$store.state.name

 },

 gender() {

            return this.$store.state.gender

 },

 age : {

 get() {

            return this.$store.state.age

 },

 set(value) {

            this.$store.commit('setAge', value)

 }

 }

}

 

3.2 mapState 辅组函数

import { mapState } from 'vuex'


//只是显示的话,且状态名和模版插值名一致,用数组

computed : { ...mapState(['name', 'gender', 'age']) },


//v-model 因为丢失计算属性 set,get,改成事件

<input type="text" @input="setAge" :value="age">


methods : {

 setAge(e) {

        this.$store.commit('setAge', e.target.value)

 }

}

 

4. Getters 派生状态

- 有时候我们需要从 store 中的 state 中派生出一些状态,Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。

- Getter 接受 state 作为其第一个参数:

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)

 }

 }

})

 

通过属性访问

Getter 会暴露为 store.getters 对象, 你可以以属性的形式访问这些值:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

 

Getter 也可以接受其他 getter 作为第二个参数:

getters: {

    // ...

    doneTodosCount (state, getters) {

      return getters.doneTodos.length

 }

 }

 

通过方法访问

你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

getters: {

    // ...

    getTodoById: (state) => (id) => {

      return state.todos.find(todo => todo.id === id)

 }

 }

 

  store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

 

mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex'


export default {

  // ...

 computed: {

  // 使用对象展开运算符将 getter 混入 computed 对象中

 ...mapGetters([

      'doneTodosCount',

      'anotherGetter',

      // ...

 ])

 }

}

 

如果你想将一个 getter 属性另取一个名字,使用对象形式:

...mapGetters({

  //  `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`

 doneCount: 'doneTodosCount'

})

 

5. Mutations 状态提交

更改 Vuex store 中的状态的唯一方法是提交 mutationVuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = createStore({

 state: {

 count: 1

 },

 mutations: {

 increment (state) {

      // 变更状态

 state.count++

 }

 }

})

 

你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:当触发一个类型为 increment  mutation 时,调用此函数。要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:

store.commit('increment')

 

提交载荷(Payload)

你可以向 store.commit 传入额外的参数,即 mutation 载荷(payload

// ...

mutations: {

 increment (state, n) {

 state.count += n

 }

}

store.commit('increment', 10)

 

对象风格的提交方式

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

store.commit({

    type: 'increment',

    amount: 10

 })

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:

 

  mutations: {

    increment (state, payload) {

      state.count += payload.amount

 }

 }

 

Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:

mutations: {

    someMutation (state) {

      api.callAsyncMethod(() => {

        state.count++

 })

 }

 }

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

 

mapMutations

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

import { mapMutations } from 'vuex'


export default {

  // ...

  methods: {

 ...mapMutations([

      'increment', //  `this.increment()` 映射为 `this.$store.commit('increment')`


      // `mapMutations` 也支持载荷:

      'incrementBy' //  `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`

 ]),

 ...mapMutations({

      add: 'increment' //  `this.add()` 映射为 `this.$store.commit('increment')`

 })

 }

}

 

6. Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。

 

const store = createStore({

    state: {

      count: 0

 },

    mutations: {

      increment (state) {

        state.count++

 }

 },

    actions: {

      increment (context) {

        context.commit('increment')

 }

 }

})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state  context.getters 来获取 state getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。

 

实践中,我们会经常用到 ES2015 参数解构来简化代码(特别是我们需要调用 commit 很多次的时候):

 

actions: {

    increment ({ commit }) {

      commit('increment')

 }

}

 

分发 Action

Action 通过 store.dispatch 方法触发:

  store.dispatch('increment')

使用 mapActions 辅助函数在组件中分发 Action

你在组件中使用 this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

import { mapActions } from 'vuex'


export default {

  // ...

  methods: {

 ...mapActions([

      'increment', //  `this.increment()` 映射为 `this.$store.dispatch('increment')`


      // `mapActions` 也支持载荷:

      'incrementBy' //  `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`

 ]),

 ...mapActions({

      add: 'increment' //  `this.add()` 映射为 `this.$store.dispatch('increment')`

 })

 }

}

 

组合 Action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise

actions: {

    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,我们可以如下组合 action

// 假设 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 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

7. Module:

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块——从上至下进行同样方式的分割:

const moduleA = {

    state: () => ({ ... }),

    mutations: { ... },

    actions: { ... },

    getters: { ... }

 }

  

  const moduleB = {

    state: () => ({ ... }),

    mutations: { ... },

    actions: { ... }

 }

  

  const store = createStore({

    modules: {

      a: moduleA,

      b: moduleB

 }

 })

  

  store.state.a // -> moduleA 的状态

  store.state.b // -> moduleB 的状态

 

模块的局部状态

对于模块内部的 mutation getter,接收的第一个参数是模块的局部状态对象

对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

const moduleA = {

    // ...

    actions: {

      incrementIfOddOnRootSum ({ state, commit, rootState }) {

        if ((state.count + rootState.count) % 2 === 1) {

          commit('increment')

 }

 }

 }

 }

 

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

const moduleA = {

    // ...

    getters: {

      sumWithRootCount (state, getters, rootState) {

        return state.count + rootState.count

 }

 }

 }

命名空间

默认命名空间: 默认情况下,模块内部的 action mutation 仍然是注册在全局命名空间——这样使得多个模块能够对同一个 action mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。

 

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteraction mutation 都会自动根据模块注册的路径调整命名。例如:

const store = createStore({

    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']

 }

 }

 }

 }

 }

 })

启用了命名空间的 getter action 会收到局部化的 getterdispatch  commit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

 

在带命名空间的模块内访问全局内容(Global Assets

 如果你希望使用全局 state getterrootState  rootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch  commit 即可。

modules: {

 foo: {

 namespaced: true,


 getters: {

      // 在这个模块的 getter 中,`getters` 被局部化了

      // 你可以使用 getter 的第四个参数来调用 `rootGetters`

 someGetter (state, getters, rootState, rootGetters) {

 getters.someOtherGetter // -> 'foo/someOtherGetter'

 rootGetters.someOtherGetter // -> 'someOtherGetter'

 rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'

 },

 someOtherGetter: state => { ... }

 },


 actions: {

      // 在这个模块中, dispatch  commit 也被局部化了

      // 他们可以接受 `root` 属性以访问根 dispatch  commit

 someAction ({ dispatch, commit, getters, rootGetters }) {

 getters.someGetter // -> 'foo/someGetter'

 rootGetters.someGetter // -> 'someGetter'

 rootGetters['bar/someGetter'] // -> 'bar/someGetter'


 dispatch('someOtherAction') // -> 'foo/someOtherAction'

 dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'


 commit('someMutation') // -> 'foo/someMutation'

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

 },

 someOtherAction (ctx, payload) { ... }

 }

 }

}

 

带命名空间的绑定函数: mapStatemapGettersmapActions mapMutations

当使用 mapStatemapGettersmapActions  mapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐, 对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

computed: {

 ...mapState('some/nested/module', {

    a: state => state.a,

    b: state => state.b

 }),

 ...mapGetters('some/nested/module', [

    'someGetter', // -> this.someGetter

    'someOtherGetter', // -> this.someOtherGetter

 ])

},

methods: {

 ...mapActions('some/nested/module', [

    'foo', // -> this.foo()

    'bar' // -> this.bar()

 ])

}

 

而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')


export default {

  computed: {

    //  `some/nested/module` 中查找

 ...mapState({

      a: state => state.a,

      b: state => state.b

 })

 },

  methods: {

    //  `some/nested/module` 中查找

 ...mapActions([

      'foo',

      'bar'

 ])

 }

}

 

模块动态注册和模块重用

      详见参考vuex(v4.x)参考文档;

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
0 评论
0 收藏
0
分享
返回顶部
顶部