阅读 737

【姐妹篇】人家都在玩源码,你还在纠结vuex的使用...

紧接上一篇路由

还是来吧废话少说,我们争取一篇文章就搞定vuex的使用及原理(用法主要提炼的官网)

最近一直在受虐,状态不是很好。如有误,望指教,感恩!

姐妹篇 vue-router的使用及手写

2.1 先说一下概念

vuex比起vue-router来说知识点便要少的多了。

这里只要搞明白5个东西就可以了,分别是State,Getter,Mutation,Action,Module

2.2 重点

State

基本使用

直接上栗子,向state中放一个数据

state: {
        count: 0,
    },
复制代码

怎么在组件中获取呢?和vue-router类似,在根vue实例的时候是把这个状态实例传进了vue的构造函数中,组件实例可以使用this.$store拿到这些状态

组件中获取

<template>
    <div id="app">
        {{$store.state.count}}
    </div>
</template>
复制代码

值得注意的是:我们直接操作数据,但是为了保证状态的可监控这种操作是不被允许的,在vuex中操作数据的唯一方式就是触发一个mutation

为了不要每次拿数据都使用$store.state.count这种方式去拿,我们可以把它放进我们的计算属性中

computed:{
    count(){
      return this.$store.state.count
    },
}
复制代码

但是,如果当前组件要使用state中的多个数据,我们难道要在计算属性中一个个声明嘛,显然太麻烦了

辅助函数 mapState

vuex为我们提供了一个辅助函数 mapState

利用这个辅助函数帮我们声明计算属性

栗子

store中

 state: {
        count: 0,
        name: 'gxb',
        age: 18
    },
复制代码

组件中

import {mapState} from 'vuex'
export default {
  computed:mapState(
    ['count','name','age']
  ) 
};
复制代码

现在就好使多了

<template>
    <div id="app">
        {{count}}{{name}}{{age}}
    </div>
</template>
复制代码

Getter

基本使用

Getter就是相当于vuex的一个计算属性,由state中的数据派生而来,原数据改变这个派生数据才会跟着改变,否则就使用缓存

直接上栗子

每个getter接收一个state作为参数

export default new Vuex.Store({
    state: {
        count: 0,
        name: 'gxb',
        age: 18
    },
    getters: {
        demo: state => {
            return `${state.name} is ${state.age} 岁了`
        }
    }
}
复制代码

组件中获取

<template>
    <div id="app">
        {{$store.getters.demo}}
    </div>
</template>
复制代码

getter也是可以接收一个别的getter作为它的第二个参数的

getters: {

        demo: (state, getters) => {
            return `${state.name} is ${state.age} 岁了` + getters.demo02
        },
        demo02: state => {
            return state.count
        }
    }
复制代码

一个getter的返回值也可以是个函数

 getters: {

        demo: (state, getters) => {
            return `${state.name} is ${state.age} 岁了` + getters.demo02
        },
        demo02: state => {
            return state.count
        },
        demo03: state => n => state.count === n

    }
复制代码

组件

<template>
    <div id="app">
        {{$store.getters.demo03(0)}}
    </div>
</template>
复制代码

开始当然是true了

在组件中为了不一直使用this.$store.getters这种方式拿数据,也可以把它放到当前组件的计算属性中

 computed:{
     demo(){
         return this.$store.getters.demo
     }
 }
复制代码

辅助函数mapGetters

当然如果是多个getter,vuex也提供了一个辅助函数

import { mapGetters } from 'vuex'

export default {
  computed:{
   //...原有计算属性
  ...mapGetters(['demo'])
  },
复制代码

Mutation

基本使用

修改state中数据的唯一方式就是触发一个Mutation。Mutation类似一个事件注册,触发一个Mutation需要使用commit方法

直接看栗子

store中(每个mutation的第一参也是state)

    state: {
        count: 0,
        name: 'gxb',
        age: 18
    },
    mutations: {
        add(state) {
            state.count++
        },

    },
复制代码

组件中

 <button @click="$store.commit('add')">add</button>
复制代码

commit第一参就是要触发的mutations的事件类型(即add就相当于一个mutations的事件类型,后面的函数体就相当于事件的回调函数)

当然commit是可以传入第二个参数的

再来个栗子

store中(mutation当然也需要第二个参数来收一下吧)

mutations: {
        add(state) {
            state.count++
        },
        add02(state, n) {
            state.count += n
        }

    },
复制代码

组件中

指定加法步长

<button @click="$store.commit('add02',2)">add</button>
复制代码

这个commit的第二参还有一个名称叫做 载荷,这个 载荷 一般情况下多为一个对象(对象传的东西多啊)

<button @click="$store.commit('add02',{
n:2
})">add</button>
复制代码
mutations: {
        add(state) {
            state.count++
        },
        add02(state, obj) {
            state.count += obj.n
        }

    },
复制代码

commit的参数可以是一个对象

this.$store.commit({
    type:'add02',
    n:2
})
复制代码

这个时候整个对象都是一个 载荷

mutations: {
  increment (state, payload) {
    state.count += payload.n
  }
}
复制代码

还是老问题,我们一直使用$store.commit这样太难受了吧

辅助函数mapMutations

vuex又给我们提供了一个辅助函数mapMutations

直接看栗子吧

import { mapState } from "vuex";
import { mapGetters } from "vuex";
import { mapMutations } from "vuex";
export default {
  computed: {
    ...mapState(["count"]),
    ...mapGetters(["demo"]),
  },
  methods: {
    ...mapMutations(["add"]),
  },
};
复制代码

这个时候,methods对象就相当于有了一个add函数,调用这个add函数就等价于调用$store.commit('add')

 <button @click="this.add">add</button>
复制代码

这时舒服多了吧

Mutation这个需要注意几个问题

  • Mutation要遵循vue的响应式规则(因为vuex中所有的状态数据都应该是响应式的,不能明知道vue不能监听一个新增属性你还往state中新添一个吧。可借助Vue.set或者使用老对象作为容器)
  • Mutation里必须都是同步函数(无法确定回调函数的执行时机,devtools无法记录)

Action

基本使用

既然Mutation无法进行异步操作,那么异步就交个Action了,Action也不能直接操作state中的数据,它也是通过触发Mutation来间接改变的

commit用于触发mutation,dispatch用来触发action

栗子

store中:每一个action的第一参是一个与store实例具有相同方法属性的上下文对象,故可以从中解构出commit方法

    state: {
        count: 0,
        name: 'gxb',
        age: 18
    },
    mutations: {
        add(state) {
            state.count++
        },
        add02(state, n) {
            state.count += n
        }

    },
    actions: {
        test({ commit }) {
            commit('add')
        }
    },
复制代码

组件中

 <button @click="$store.dispatch('test')">触发actions</button>
复制代码

一般在action中执行异步操作

    state: {
        count: 0,
        name: 'gxb',
        age: 18
    },
    mutations: {
        add(state) {
            state.count++
        },
        add02(state, n) {
            state.count += n
        }

    },
    actions: {
        increment({ commit }) {
            setTimeout(function() {
                commit('add')
            }, 1000)
        }
    },
复制代码

Actions 支持同样的载荷方式和对象方式进行分发

    state: {
        count: 0,
        name: 'gxb',
        age: 18
    },
    mutations: {
        add(state) {
            state.count++
        },
        add02(state, obj) {
            state.count += obj.n
        }

    },
    actions: {
        increment({ commit }, obj) {
            setTimeout(function() {
                commit('add02', obj)
            }, 1000)
        }
    },
复制代码
  <button @click="$store.dispatch('increment',{
    n:2
  })">触发actions</button>
复制代码

以对象方式触发一个action

   <button @click="$store.dispatch(
  {
    type:'increment',
    n:2
  })">触发actions</button>
复制代码

组合Action

组合Action,因为一般Action是处理异步的操作。如果有多个Action,比较有Action01,和Action02,有个要求就是需要Action01先处理一个数据,完成之后Action02在进行处理。我们得怎样组合它呢

首先来看store.dispatch这个方法的返回值是个啥东西

它是一个promise

那么栗子就来咯,先让action01---处理,完成之后再交给action02处理。

    actions: {
        action01({ commit }) {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    commit('add')
                    resolve('s')
                }, 1000)
                reject('f')
            })
        },
        action02({ commit, dispatch }) {
            dispatch('action01').then(() => {
                commit('add')
            })
        }
    },
复制代码

或者使用async,await

官网的栗子

假设两个异步取数据的方法,返回值是promise
actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}
复制代码

辅助函数

辅助函数与mutation一样,就省略了

Module

为啥出去这个,因为vuex是使用的单一状态树,也就是说所有的状态,以及状态派生的getter,改变状态的mutation还有操作异步的actions都在一个文件中。

如果是一个复杂应用,我去,想想就有优点恐怖了

简单使用

为了解决这个问题,vuex可以将store分割成一个个的模块,每个模块都有那一套东西(state、mutations、actions、getters)

像这样(为了方便演示写一块了)

const module01 = {
    state: () => ({
        count: 1
    }),
    mutations: {
        add(state) {
            state.count++
        }
    },
    actions: {
        addAsync({ commit }) {
            setTimeout(() => {
                commit('add')
            }, 1000);
        }
    },
    getters: {
        countToStr02(state) {
            return state.count + ''
        }
    }
}
const module02 = {
    state: () => ({
        count: 2
    }),
    mutations: {
        add(state) {
            state.count++
        }
    },
    actions: {
        addAsync({ commit }) {
            setTimeout(() => {
                commit('add')
            }, 1000);
        }
    },
    getters: {
        countToStr03(state) {
            return state.count + ''
        }
    }
}


export default new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        add(state) {
            state.count++
        }
    },
    actions: {
        addAsync({ commit }) {
            setTimeout(() => {
                commit('add')
            }, 1000);
        }
    },
    getters: {
        countToStr01(state) {
            return state.count + ''
        }
    },
    modules: {
        module01,
        module02
    },

})
复制代码

在组件中咋拿module01的状态呢?

像这样

 {{$store.state.module01}}
复制代码

怎么改变状态呢?

你仍然还是要触发的mutations,现在先可以把他们理解成全局的

就使用store.commit('add')去改变就行,不过此时你这样会触发两个mutation,一个mutation是用来改变根模块状态的,一个是改变模块module01中的状态的

栗子

<template>
  <div id="app">
    {{$store.state.count}}
    {{$store.state.module01}}
    <button @click="$store.commit('add')">测试</button>
  </div>
</template>
复制代码

模块的局部状态

上面的栗子其实已经用了一部分了

对于模块中的mutations和getters,它们的第一参仍是当前此模块的状态对象,只不过getter多了一个第三参,是根模块状态对象;action也是和原来一样,只不过此上下文对象中又多了一个可以拿根模块状态的对象即context.rootState

命名空间

以上的模块都是注册在全局的,即像上面一个commit触发了两个模块的mutation

严格来说,我们肯定是不希望这种多变化的情况,我们一般还是喜欢比较受控的玩意

操作也很简单,就在模块里加一个属性namespaced值设置为true

那么我们再次触发那个add的mutation时就可以这样写了

 <button @click="$store.commit('module01/add')">测试</button>
复制代码

getters和dispatch和commit一样,这是就只触发module01模块的add类型的mutation了

嵌套的模块会继承父模块的命名空间

还有一些小知识点

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

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

单解释第二句话吧,你在有命名空间的模块的下,比如你想触发一个mutation,你在action中得这样写吧commit(...),这里是默认给你加上当前命名空间的路径的,如果你此时想要触发的是一个处于跟模块的mutation,你就可以加上{ root: true }这个参数

即commit('demo',null,{ root: true })(假设没有载荷)

3 若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中 (吃不了撑的不在根模块注册?唉)

瞅一样官网的栗子

 actions: {
        someAction: {
          root: true,
          handler (context, payload) { ... } // -> 'someAction'
        }
      }
复制代码

4 模块的动态创建

store创建之后

可以使用store.registerModule('myModule',{...})创建模块

store.registerModule(['nested', 'myModule'], {})创建嵌套的nested/myModule

5 模块重用

state不用对象了,和vue的data一样使用函数返回一个新对象装数据

2.3 实现一个简单版的vuex

有了上一个router的栗子,这个就更加简单了吧

还是先来简单分析一下

  1. 首先是一个插件故要有个install方法
  2. 向内传了一个对象{state、getters....},且在实例store中可以拿。故这些东西是放进了构造函数中的
  3. 有两个api,commit和dispatch

接下来就简单多了

首先还是和router一样,将store使用混入放进vue原型中

先实现install方法吧

let Vue
function install(vue) {
    Vue = vue//还是为了保证下面可以拿到vue
    vue.mixin({
        beforeCreate() {
            if (this.$options.store) {
                Vue.prototype.$store = this.$options.store
            }
        }
    })
}
复制代码

初始化Store类,简单做一下getter的代理

这里仅是对getter做了一下代理(本来我打算getter的计算属性功能借助一下vue的计算属性呢,结果发现如果那样的话getter的处理函数是在vue中调用的,我无法进行传递实参this.state。也不想在这搞的太复杂,就先这样吧等我找到一个比较好的解决方法在改进一下)

class Store {
    constructor(options = {}) {
        this._vm = new Vue({
            data: {
                state: options.state
            },

        })
        this.actions = options.actions || {}
        this.mutations = options.mutations || {}
        this.getters = {}
        let getters = options.getters || {}


        // 就是简单做了一次代理
        Object.keys(getters).forEach(key => {
            Object.defineProperty(this.getters, key, {
                get: () => {
                    return getters[key](this.state)
                }
            })
        })
    }
    //state的存储器
     get state() {
        return this._vm.state
    }
}
复制代码

实现commit

这个都比较简单,就是根据传过来的mutation类型,去拿处理函数并进行调用

    commit(type, payload) {
        const entry = this.mutations[type]

        if (!entry) {
            throw new Error('没有此mutation')
        } else {
            entry(this.state, payload)
        }

    }
复制代码

实现dispatch

与上面基本是一样,不过action的第一参是与store实例具有相同属性方法的上下文对象.因为这里没有打算继续实现模块化,故我们就将把当前实例给他

同时不要忘了dispatch返回的是一个promise

   dispatch(type, payload) {
        const entry = this.actions[type]
        if (!entry) {
            throw new Error('没有此action')
        }
        // 返回的是一个promise
        return Promise.resolve(entry(this, payload))
    }
复制代码

这里还有一个小问题。

如我们的store配置文件,写一个action时可能是这样写的

    actions: {
        addAsync({ commit }) {
            setTimeout(() => {
                commit('add')
            }, 1000);
        }
    },
复制代码

我们都之前这种定时器中的回调函数this都是指的undefined(严格模式),但是commit函数中是需要通过this.state拿状态数据的,此时this指向undefined而不是store实例。故数据无法获取

改进:提前将commit内部中的this绑定好(dispatch也一样)

constructor(options = {}) { 
    //...
	this.commit = this.commit.bind(this)
}
复制代码

完整代码

let Vue

function install(vue) {
    Vue = vue
    vue.mixin({
        beforeCreate() {
            if (this.$options.store) {
                Vue.prototype.$store = this.$options.store
            }
        }
    })
}

class Store {
    constructor(options = {}) {

        this._vm = new Vue({

            data: {
                state: options.state
            },
            created() {
                window.state = this.state
            },
            computed: {

            },

        })
        this.actions = options.actions || {}
        this.mutations = options.mutations || {}
        this.getters = {}
        let getters = options.getters || {}


        // 就是简单做了一次代理
        Object.keys(getters).forEach(key => {
            Object.defineProperty(this.getters, key, {
                get: () => {
                    return getters[key](this.state)
                }
            })
        })


        this.commit = this.commit.bind(this)
        this.dispatch = this.dispatch.bind(this)

    }
    get state() {
        return this._vm.state
    }
    commit(type, payload) {
        const entry = this.mutations[type]

        if (!entry) {
            throw new Error('没有此mutation')
        } else {
            entry(this.state, payload)
        }

    }
    dispatch(type, payload) {
        const entry = this.actions[type]
        if (!entry) {
            throw new Error('没有此action')
        }
        // 返回的是一个promise
        return Promise.resolve(entry(this, payload))
    }

}

export default { install, Store }
复制代码

写到最后

希望这篇文章能给与您一丝帮助(卑微求赞)

期待着我们的下一次邂逅