Vuex学习笔记

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能

为什么要使用vuex?记得在vue的props中说过,vue的数据是单向流动的,每一次更改数据都必须通过父组件的方法更改,但是因为数据是单向流动的,当我们出现这么一个场景,两个同级组件都需要同一个数据怎么办,这个时候就只能将父组件当做容器进行传递数据了,可能这个数据就只有那两个组件需要,大家可以试想一下当我们的应用有几个页面需要一些公有的数据层级非常深的时候,传递数据可以说是非常的复杂,这时候我们肯定会考虑一个管理数据的容器了?这就是为什么要使用vuex了!

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

一个简单的基于vuex的简单计数器小例子

<body>
    <div id="app">
        <p>{{ count }}</p>
        <p>
            <button @click="increment">+</button>
            <button @click="decrement">-</button>
        </p>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="../lib/vuex.js"></script>
    <script>

        // new 一个VuexStore对象
        const store = new Vuex.Store({
            state: {
                count: 0
            },
            // 要想改变state必须通过mutations方法改
            mutations: {
                increment: state => state.count++,
                decrement: state => state.count--
            }
        })

        new Vue({
            el: '#app',
            computed: {
                count() {
                    // 通过计算属性绑定store到对应组件上
                    // 需要哪一个组件绑定哪一个组件
                    return store.state.count
                }
            },
            methods: {
                // 要想调用mutations必须使用store.commit对应的mutation函数
                increment() {
                    store.commit('increment')
                },
                decrement() {
                    store.commit('decrement')
                }
            }
        })
    </script>
</body>

注释已经比较详细了

state

  • 单一状态树,意思就是管理应用所有状态的对象只有一个,每一个Vue实例也只能包含一个store实例

  • 你会发现获取Store的数据是通过计算属性获取的,因为store的更新会带动使用了store中对应数据的组件的更新,所以每一次store中的数据更新,计算属性如果检测到需要的数据更新都将会重新求值重而重新渲染界面

  • 但是我们还有一种更简单的注入store数据的方法,直接通过store选项绑定到vue实例上,并且会分发给所以此实例的子组件,相当于将store绑定到了这一组件树上了

    image.png

  • mapState辅助函数,可以快速的帮我们绑定store数据到组件上

            computed:Vuex.mapState({
                    // key为组件内访问的名称,后面是一个函数接收store中的state我们可以直接返回
                    // 我们需要的数据,或者通过state的数据计算出我们需要的数据
                    mcount: state => state.count
                }),
    <div id="app">
        <p>{{ mcount }}</p>
        <p>
            <button @click="increment">+</button>
            <button @click="decrement">-</button>
        </p>
    </div>

上面的绑定方式提供了一种简写方式

            computed:Vuex.mapState({
                    // key为组件内访问的名称,后面是一个函数接收store中的state我们可以直接返回
                    // 我们需要的数据,或者通过state的数据计算出我们需要的数据
                    // mcount: state => state.count

                    // 上面的写法可以简写为
                    // 传入字符串count就等于 --> state => state.count
                    mcount:'count'
                }),
            data() {
                return {
                    tcount:88
                }
            },
            computed: Vuex.mapState({
                // key为组件内访问的名称,后面是一个函数接收store中的state我们可以直接返回
                // 我们需要的数据,或者通过state的数据计算出我们需要的数据
                // mcount: state => {
                //     // 这里的this是window对象,大概是因为箭头函数的缘故
                //     console.log(this);
                //     return state.count
                // }

                // 上面的写法可以简写为
                // 传入字符串count就等于 --> state => state.count
                // mcount:'count'


                // 也可以使用函数的写法,可以获取当前作用域下的this
                mcount(state) {
                    return state.count + this.tcount
                }
            }),

如果我们传递键值对是相同的,类似于state的key是什么我们就以什么key绑定到组件上,还可以进行进一步简写

    <div id="app">
        <p>{{ count }}</p>
        <p>
            <button @click="increment">+</button>
            <button @click="decrement">-</button>
        </p>
    </div>
computed: Vuex.mapState(['count']),

大家会发现上面的这种写法其实已经可以说是替换掉了所有的computed了,难道vuex使用就有这种缺点吗?不不不,vue官方提供了一种解决方案帮助我们将mapState返回的对象融入到computed对象中(为什么会占用整个computed,因为mapState返回的就是一个对象),那么大家肯定有一种想法了,看下面的例子

            computed: {
                state:Vuex.mapState(['count'])
            },

不知道大家有没有这种想法,但是很遗憾就是这个方法不行,所以我们只能老老实实用vue官方的方法(低版本浏览器就不要想了)

 computed: {
                ctcount(){
                    // 22次平方,Python数学运算符
                    return this.tcount ** 22;
                },
                // 混入mapState
                // 这是展开运算符,但是是展开对象的,而不是数组
                ...Vuex.mapState(['count'])
            },
    <div id="app">
        <p>混合成功: {{ctcount}}</p>
        <p>{{ count }}</p>
        <p>
            <button @click="increment">+</button>
            <button @click="decrement">-</button>
        </p>
    </div>

关于state部分介绍到此

Getter

想像一种场景,我们组件需要的数据是通过state中的数据计算出来的,这时候简单的计算还好说如果是复杂的过滤呢?每一次使用这个数据或者很多个组件需要过滤后的数据那么就必须重复写许多的过滤方法,或者定义一个公共的方法

computed: {
  doneTodosCount () {
// 如果其他组件需要要么抽离出去一个公共函数,要么直接copy代码
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

其实无论那种解决的办法都不是特别的理想,所以vuex允许我们在store中定义getter属性(可以认为是store的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算

        const store = new Vuex.Store({
            state: {
                count: 0
            },
            // 要想改变state必须通过mutations方法改
            mutations: {
                increment: state => state.count++,
                decrement: state => state.count--
            },
            getters:{
                // 每一个getter接收state为第一个参数
                compuCount(state){
                    // 这样我们就计算除了一个不知道的值
                    return state.count ** 3 / 87 * 4 % 2;
                }
            }
        })

那么我们是把getter定义出来了,但是我们需要怎么访问这个getter呢?
不可能是这样吧...Vuex.mapState(['compuCount']),这样就直接报错了,那么vuex提供了三种方式访问Store的getter

  1. 通过属性访问
<body>
    <div id="app">
        <p>{{compuCount}}</p>
        <p>
            <button @click="increment">+</button>
            <button @click="decrement">-</button>
        </p>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="../lib/vuex.js"></script>
    <script>

        // new 一个VuexStore对象
        const store = new Vuex.Store({
            state: {
                count: 2
            },
            // 要想改变state必须通过mutations方法改
            mutations: {
                increment: state => state.count++,
                decrement: state => state.count--
            },
            getters:{
                testGetter(state){
                    return state.count ** 2;
                },
                // 每一个getter接收state为第一个参数
                // getter还可以接收其他的getter作为第二个参数
                compuCount(state,otherGetters){
                    // 第二个参数包含了所有的getters
                    console.log(otherGetters);
                    // 这样我们就计算除了一个不知道的值
                    return state.count ** 10 / 87 * 4 % 5;
                }
            }
        })

        new Vue({
            el: '#app',
            // 绑定store
            store,
            data() {
                return {
                    
                }
            },
            computed: {
                compuCount(){
                    return this.$store.getters.compuCount;
                }
            },
            methods: {
                // 要想调用mutations必须使用store.commit对应的mutation函数
                increment() {
                    store.commit('increment')
                },
                decrement() {
                    store.commit('decrement')
                }
            }
        })
    </script>
</body>

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

  1. 通过方法访问

你可以让你定义的getter返回一个函数,通过传入不同的参数返回不同的结果

<body>
    <div id="app">
        <p>{{powerCount}}</p>
        <button @click="add">+</button>
        <button @click="minus">-</button>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="../lib/vuex.js"></script>
    <script>
        const store = new Vuex.Store({
            state: {
                count: 1
            },
            mutations: {
                add: (state) => { state.count++ },
                minus: (state) => { state.count-- }
            },
            getters: {
                powerCount: (state) => (num) => {
                    console.log(state.count,num);
                    return state.count ** num;
                }
            }
        })

        new Vue({
            // 必须绑定store,不然mapState报一些奇怪的错误
            store,
            computed: {
                ...Vuex.mapState(['count']),
                powerCount(){
                    return store.getters.powerCount(this.count);
                }
            },
            methods: {
                add() {
                    store.commit('add');
                },
                minus() {
                    store.commit('minus')
                }
            },
        }).$mount("#app");
    </script>
</body>

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

  1. mapGetters辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性,跟mapState的使用方法完全一致

            computed: {
                ...Vuex.mapState(['count']),
                // 如果你不想使用getter的name绑定到store上
                // 可以{yourName:'powerCount'},传入一个对象
                // 你定义什么名字,就绑定什么名字到实例上

                // 实测无法绑定返回函数的getter
                ...Vuex.mapGetters(['pCount'])
            },

Mutation

mutation一句话就是改变state的唯一途径(后面的异步action也是通过最后调用mutation改变state的)
跟redux一样,触发mutation我们可以给mutation传参,叫做payload(载荷)

<body>
    <div id="app">
        <p>{{count}}</p>
        <button @click="add">+10</button>
        <button @click="minus">-</button>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="../lib/vuex.js"></script>
    <script>
        const store = new Vuex.Store({
            state: {
                count: 1
            },
            mutations: {
                add: (state, num, num2) => {
                    // 10 undefined
                    console.log(num, num2);
                    num = num || 1;
                    return state.count += num;
                },
                minus: (state) => { state.count-- }
            },
            getters: {
                powerCount: (state) => (num) => {
                    console.log(state.count, num);
                    return state.count ** num;
                },
                pCount(state) {
                    return state.count ** state.count;
                }
            }
        })

        new Vue({
            // 必须绑定store,不然mapState报一些奇怪的错误
            store,
            computed: {
                ...Vuex.mapState(['count']),
                // 如果你不想使用getter的name绑定到store上
                // 可以{yourName:'powerCount'},传入一个对象
                // 你定义什么名字,就绑定什么名字到实例上

                // 实测无法绑定返回函数的getter
                ...Vuex.mapGetters(['pCount'])
            },
            methods: {
                add() {
                    // 最多只能接收一个payload
                    // 所以大多数情况下payload为对象
                    store.commit('add', 10, 10);
                },
                minus() {
                    store.commit('minus')
                }
            },
        }).$mount("#app");
    </script>
</body>

mutation需要遵守Vue的响应规则

  1. 最好提前在你的 store 中初始化好所有所需属性。
  2. 当需要在对象上添加新属性时,你应该
  • 使用 Vue.set(obj, 'newProp', 123), 或者

  • 以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:

    state.obj = { ...state.obj, newProp: 123 }
    
  • 使用常亮替代mutation类型
    首先在对应的目录新建一个mutation-types的js文件,用于导出所有mutation函数名


    image.png

    然后在index.html中就可以这样写

        // 使用es6语法导入mutation-types
        import { ADD, MINUS } from "../lib/mutation-types.js";
        const store = new Vuex.Store({
            state: {
                count: 1
            },
            mutations: {
                [ADD]: (state, num, num2) => {
                    // 10 undefined
                    console.log(num, num2);
                    num = num || 1;
                    return state.count += num;
                },
                [MINUS]: (state) => { state.count-- }
            },
            getters: {
                powerCount: (state) => (num) => {
                    console.log(state.count, num);
                    return state.count ** num;
                },
                pCount(state) {
                    return state.count ** state.count;
                }
            }
        })

我这样写在浏览器中会出现错误,也不知道什么情况(猜测大概是因为import需要在一个单独的js文件中写吧)mutation必须是同步函数,不支持异步操作

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

store.commit({
  type: 'increment',
  amount: 10
})

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

<body>
    <div id="app">
        <p>{{count}}</p>
        <button @click="add">+</button>
        <button @click="minus">-</button>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="../lib/vuex.js"></script>
    <script>
        const store = new Vuex.Store({
            state: {
                count: 1
            },
            mutations: {
                add: (state) => {
                    return state.count ++;
                },
                minus: (state) => { state.count-- }
            }
        })

        new Vue({
            // 必须绑定store,不然mapState报一些奇怪的错误
            store,
            computed: {
                ...Vuex.mapState(['count'])
            },
            methods: {
                ...Vuex.mapMutations([
                    // 将add -> mutation映射到this.add
                    'add','minus'
                ])
            },
        }).$mount("#app");
    </script>
</body>

不仅仅可以直接映射到方法,甚至还可以映射一个带参数方法

<body>
    <div id="app">
        <p>{{count}}</p>
        <button @click="add(10)">+</button>
        <button @click="minus">-</button>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="../lib/vuex.js"></script>
    <script>
        const store = new Vuex.Store({
            state: {
                count: 1
            },
            mutations: {
                add: (state,num) => {
                    return state.count += num;
                },
                minus: (state) => { state.count-- }
            }
        })

        new Vue({
            // 必须绑定store,不然mapState报一些奇怪的错误
            store,
            computed: {
                ...Vuex.mapState(['count'])
            },
            methods: {
                ...Vuex.mapMutations([
                    // 将add -> mutation映射到this.add
                    'add','minus'
                ])
            },
        }).$mount("#app");
    </script>
</body>

跟其他两个map函数一样,同样可以通过一个独享设置别名

// this.madd = this.$store.commit('add')
mapMutations({madd:'add'})

其实mapMutations的映射原理,就是通过对应的实例绑定store选项,然后将对应的函数映射到$store的commit()方法上了

Action

action就是专门用来解决无法异步更新state的,但是大家要明白其实action最后依然调用的是mutation并不是直接更新state的,Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters

<body>
    <div id="app">
        <p>{{count}}</p>
        <button @click="add(10)">+</button>
        <button @click="minus">-</button>
        <button @click="addAsync(5)">async-</button>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="../lib/vuex.js"></script>
    <script>
        const store = new Vuex.Store({
            state: {
                count: 1
            },
            mutations: {
                add: (state,num) => {
                    return state.count += num;
                },
                minus: (state) => { state.count-- }
            },
            actions:{
                addAsync(context){
                    setTimeout(()=>{
                        context.commit('add',10);
                    },1000)
                }
            }
        })

        new Vue({
            // 必须绑定store,不然mapState报一些奇怪的错误
            store,
            computed: {
                ...Vuex.mapState(['count'])
            },
            methods: {
                ...Vuex.mapMutations([
                    // 将add -> mutation映射到this.add
                    'add','minus'
                ]),
                addAsync(num){
                    store.dispatch("addAsync",num);
                }
            },
        }).$mount("#app");
    </script>
</body>

action必须使用store的dispatch触发(太像redux了)
Actions 支持同样的载荷方式和对象方式进行分发:

// 以载荷形式分发
store.dispatch('incrementAsync', {
  amount: 10
})
// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

在组件中分发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小例子

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,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: { count: 0 },
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

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

暂停更新,更新时间待定,也不知道什么时候才能更新了?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349