愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Vuex学习总结

一、什么是vuex

在一个复杂的大型系统中,状态会在多个组件之间跨层级地、错综复杂地传递,这会使得状态难以追踪,debug起来也会很麻烦。而vuex就是为了解决这么一个问题而出现的东西,它能够集中地管理应用的状态,并且能够使得每一种状态都是以可预测的方式发生变化的。


二、单向数据流

可以从一个简单的例子切入:

new Vue({
    data() {
        return {
            count: 0
        }
    },
    template: `<div>{{count}}</div>`,
    methods: {
        increment() {
            this.count++
        }
    }
})

从例子里,可以发现该应用的状态是自管理的,且它包含有以下的部分:

  • state:驱动应用的数据源
  • view:以声明方式将state映射到视图
  • action:响应在view上的用户输入导致的状态变化

单向数据流图示如:

但是大型的系统中,往往会面临以下的问题:
1)多个组件之间需要依赖于同一个状态
2)来自不同组件的行为,需要更新同一个状态

在一般情况下,如果我们要解决问题1,那么我们就需要把所依赖的这个状态一层一层地传递下去。而如果需要解决问题2,那么就需要父子组件的直接引用,或者通过事件来变更和同步状态。而这些解决问题的办法通常会带来令人困扰的维护难题。

vuex的核心思想是提供一个全局的单例统一进行管理状态,并且定义和隔离状态管理中的各种状态,强制要求遵循约定来编写代码,而这带来的好处便是代码会更加的结构化和容易维护。


三、核心概念

1、仓库(store)

store是一个容器,用来容纳应用中的绝大部分的状态(通常是共享状态)。它和全局对象的区别在于:
1)状态存储是响应式的,可以实现数据和视图的双向绑定
2)不能够直接改变store里的状态,而是应该显示地提交mutations。这样子做的好处就是我们能够方便地追踪变更
创建一个store的方法如:

// 如果是在模块化系统中使用,开头需要先调用 Vue.use(Vuex)
const store = Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++
        }
    }
})

此后,当我们需要用到这个状态的时候,可以使用store.state来调用,如:

store.state.count

当需要改变这个状态的时候,则使用

store.commit('increment')

2、状态(State)

Vuex使用单一状态树,用一个对象包含了全部的应用层级状态。vuex中只有一个唯一的数据源,也就是只有一个store实例。
一般情况下,我们通过以下方式能够获得store里的状态:

const Counter = {
    template: `<div>{{count}}</div>`,
    computed: {
        count() {
            return store.state.count
        }
    }
}

但是这种方式个缺陷,因为store.state.count中的store其实是个全局变量,在每个组件中使用的时候,我们就需要频繁地导入这个单例。为了使得store的导入更为方便,vuex中的解决方案为:将store注入到跟组件中,然后每一个子组件中都自动获得this.$store来引用store单例,如:

const app = new Vue({
    store,  // 在根组件里注入
    components: {
        Counter
    },
    template: `
        <div>
            <counter></counter>
        </div>
    `
})

此后,Counter里就可以这么写:

const Counter = {
    template: `<div>{{count}}</div>`,
    computed: {
        count() {
            return this.$store.state.count;
        }
    }
}

由于当一个组件需要多个状态的时候,每次都声明一个计算属性,这个过程就有点麻烦。为了解决这个问题,vuex提供了mapState辅助函数,使用如:

import { mapState } from 'vuex';

export default {
    // ...
    computed: mapState({
        /*
        相当于:count(state) {
            return state.count
        }
        */
        count: state => state.count,

        /*
        'count' 相当于 state => state.count
        */
        countAlias: 'count',
    })
}

当计算属性的名称和state中状态名称相同的时候,可以给mapState传一个字符串数组,如:

computed: mapState([
    'count',
    'name'
])

它的效果相当于:

computed: {
    count(state) {
        return state.count;
    },
    name(state) {
        return state.name;
    }
}

如果我们的组件有局部的计算属性,那么如何混合使用呢?因为mapState()返回的是一个对象,所以我们可以使用...展开运算符来实现,如:

computed: {
    localComputed() { /* code */ },
    ...mapState({
        // code
    })
}

注意:虽然vuex可以很方便地管理状态,但这并不意味着所有的状态都需要放到vuex里,放到vuex里的状态,一般是需要在不同组件之间传递和使用的。如果一个状态是严格只属于自身的,那么可以不必放到Vuex里

3、Getters

getters相当于store里的计算属性,它用来从state中派生出新的状态。可以在Vuex.Store的选项里添加如:

const store = Vuex.Store({
    // ...
    getters: {
        countPlus1(state) {
            return state.count + 1;
        }
    }
    // ...
});

getters里的属性和Vue的计算属性一样,是具有缓存的,只有当依赖上的值发生了变更,相应的值才会更新。当我们在组件里需要用到这个getter的时候,可以使用this.$store.getter对象来获取,一种方便的使用方式是在组件内定义计算属性,如:

computed: {
    countPlus1() {
        return this.$store.state.count + 1;
    }
}

但是如果我们需要多个getter的时候,这样子写无疑是麻烦的。为了解决这个问题,和state一样,vuex也提供了mapGetters方法,像上述例子就可以改写为:

computed: {
    ...mapGetters([
        'countPlus1'
    ])
}

如果我们在组件内使用的getter名字和store里不一样的话,那么可以使用对象的形式,如:

computed: {
    ...mapGetters({
        myCount: 'countPlus1'
    })
}

4、Mutations

在vuex里面,我们不能够直接改变状态,而是应该通过提交mutation。mutations应该是一个同步函数,其中不应该包含有异步操作(如果需要用到异步,请用actions)。声明mutations的方式如:

const store = Vuex.Store({
    // ...
    mutations: {
        increment(state) {
            state.count++;
        }
    }
    // ...
});

当我们需要改变一个状态的时候,就可以使用

this.$store.commit('increment')

来提交变更。有时候,我们还希望能够携带一些参数,携带的这些参数称之为载荷(payload),使用如:
1)第一种方式如:

this.$store.commit('increment', 1, 2, 3);

这种情况下,mutations里的increment方法能够以以下方式接收到参数:

increment(state, a, b, c) {
    // code
}

其中a, b, c的值在这里将分别取到1, 2, 3。大多数情况下,推荐载荷为一个对象,因为这样子可以增加可读性:

// ...
increment(state, payload) {
    console.log(payload.amount);
    // code
}
// ...

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

还可以使用一个带有type属性的对象来提交mutations,如:

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

这种情况下,整个对象都会成为payload参数的值

当在对象中需要使用多个mutations的时候,vuex中也有mapMutations辅助函数,用法如:

// in some component
methods: {
    // 方式一(同名情况)
    ...mapMutations([
        'increment',
        'decrement'
    ]),
    // 方式二(异名情况)
    ...mapMutations({
        inc: 'increment'
    })
}
// ...

vuex中推荐使用常量的形式来作为mutations事件名称,那么,我们就可以这么写:

// mutations-types.js
export const INCREMENT = 'INCREMENT';

// store.js
import Vuex from 'vuex';
import { INCREMENT } from './mutations-types';

const store = Vuex.Store({
    // ...
    mutations: {
        [INCREMENT](state) {
            // code
        }
    }
});

这种情况对于多人协作的大型项目是很有好处的,因为我们可以将事件名称抽离出来放在一个文件里,这样子整个项目的事件名称将一目了然。

规约

在使用vue+vuex进行开发的时候,我们需要遵守:
1)在store里事先声明好所需属性。这是因为Vue的属性是响应式的,事先声明好属性,可以让Vue事先做好预处理工作
2)如果需要添加新的属性,那么可以使用Vue.set(obj, 'newProp', 123)的形式,或者用新对象来替换老对象,如:state.obj = { ...state.obj, newProp: 123 }

5、Actions

由于Vuex规定mutation都是同步的,那么当我们需要异步操作的时候,就需要有另外一种机制。而vuex里面的这种机制便是actionsactions可以包含任意的异步操作,而它所提交的是mutation,而非直接改变状态,如:

const store = Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++;
        }
    },
    actions: {
        increment(context) {
            setTimeout(() => {
                context.commit('increment')
            }, 1000);
        }
    }
});

这里面,context并非是store实例,而是当前模块的上下文(具体的内容在下面的模块这一概念中将涉及)。mutation是用commit()方法来触发,而action则是通过dispatch()方法触发,调用方式有:

store.dispatch('increment');
store.dispatch('increment', 100);
store.dispatch({
    type: 'increment',
    amount: 100
})

而在组件中,当根组件里注入store属性后,而可以通过this.$store.dispatch('xxx')来分发action。和stategettermutation一样,Vuex也提供了mapActions()辅助函数,所以我们在组件内可以这么写:

// ...
methods: {
    ...mapActions([
        'increment'
    ]),
    ...mapActions({
        dec: 'decrement'
    })
}
// ...

组合actions

可以结合Promise使用,如:

actions: {
    actionsA({ commit }) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                commit('increment');
                resolve();
            }, 1000);
        })
    }
}

我们现在就可以这么使用了:

store.dispatch('actionsA').then(() => {
    // code
});

或者在另一个action里进行组合,从而实现更为复杂的异步控制流程:

actions: {
    // ...
    actionsB({ dispatch, commit }) {
        return dispatch('actionsA').then(() => {
            commit('someOtherMutation')
        });
    }
}

利用async/await,还可以进一步改写为:

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

注意:一个store.dispatch在不同的模块中可以触发多个action函数,只有当触发完了所有的action函数后,返回的Promise才会执行

6、Modules

由于使用单一状态树,应有的所有状态都会集中到store对象里,应用越复杂的时候,store对象就有可能越臃肿。这种情况下,vuex还提供了模块机制,允许对store进行分割。分割后的每个模块,都拥有自己的state、mutation、action、getter,而一个模块里还可以进一步分割出嵌套子模块。
一个简单的分割例子:

const moduleA = {
    state: { /* code */ },
    getters: { /* code */ },
    mutations: { /* code */ },
    actions: { /* code */ }
}
const moduleB = {
    state: { /* code */ },
    mutations: { /* code */ },
    actions: { /* code */ }
}
const store = Vuex.Store({
    modules: {
        a: moduleA,
        b: moduleB
    }
});

如此一来,store就被分割成了子模块a和b,我们可以使用以下方式分别访问到子模块里的状态:

store.state.a;
store.state.b;

模块的局部状态

在一个模块内的getter、mutation,接收到的参数都是局部的状态,如:

const moduleA = {
    state: {
        count: 0
    },
    getters: {
        doubleCount(state) {
            // 这里的state是局部的state
        }
    },
    mutations: {
        increment(state) {
            // 这里的state也是局部的state
        }
    }
}

而上文提到,actions里的context并非store实例。实际上,它是当前模块的上下文,context对象里暴露出了state这一局部状态信息,而根实例上的state,则通过rootState来暴露,我们可以通过解构来获取这一些信息,如:

actions: {
    someAction({state, commit, rootState}) {
        // code
    }
}

对于getter而言,rootState则会作为第三个参数传递,如:

getters: {
    someGetter(state, getters, rootState) {
        // code
    }
}

命名空间

在默认的情况下,getter、mutation、action是注册在全局命名空间的,这样子的好处就是多个模块能够对同一mutation或者action做出响应。
但是我们可以启用namespaced参数,来使得我们的模块变得更加的自包含和具有更高的重用性。启用namespaced参数后,模块的所有getter、mutation、action都会根据注册的路径调整命名。看示例便知区别:

const moduleA = {
    namespaced: true,
    state: { /* code */ }, // state不受影响
    getters: {
        someGetter(){} // getters['moduleA/someGetter']
    },
    actions: {
        someAction(){} // dispatch('moduleA/someAction')
    },
    mutations: {
        someMutation(){} // commit('moduleA/someMutation')
    },
    modules: {
        childA: {
            getters: {
                childGetter()
                // 由于没有开启命名空间,所以继承父级命名空间getters['moduleA/childGetter']
            }
        },
        childB: {
            namespaced: true,
            getters: {
                childGetter()
                // getters['moduleA/childB/childGetter']
            }
        } 
    }
}

虽然加了namespaced参数后,模块的命名调整了。但是在模块内部,我们仍然可以不需要加这些路径,模块内部并不需要根据namespaced参数启用还是关闭而做出调整。
那么,现在问题来了,如何在模块内访问全局的内容呢?
1)对于全局的state和getter,对于getter、mutation而言,可以使用第三个参数和第四个参数(分别是rootStaterootGetter),对于action而言,则context对象里也包含有rootStaterootGetter
2)如果需要在全局命名空间内分发 action 或提交 mutation,那么将 { root: true } 作为第三参数传给 dispatch 或 commit 即可:

dispatch('someAction', null, { root: true });
commit('someMutation', null, { root: true });

带命名空间的辅助函数

如果我们启用了命名空间,那么在使用辅助函数的时候,我们通常需要这么写:

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

为了解决这个问题,我们可以给辅助函数传递第一个参数来指定命名空间路径,如:

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

模块动态注册

我们通过在Vuex.Store()里配置module属性来注册模块(这称为静态模块),但是如果我们在后续需要添加模块,那么该怎么办呢?解决方式是使用vuex提供的动态模块注册功能,如:

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