Vuex 学习手册

Vuex摘要

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

基础结构

const store = new Vuex.Store({
    state:{
        // 可存取的状态, 通过此接口可以暴露状态值
        todos:[
            {id:1, name:'Task A', done:true},
            {id:2, name:'Task B', done:true},
            {id:2, name:'Task C', done:false}
        ]
    },
    getters:{
        getById(id){
            return this.todos.filter(todo=>todo.id==id);
        },
        // 包装器
        doneList(){
            // 计算属性,返回todos中done==true的项目
            return this.todos.filter(item=>item.done)
        }
    },
    mutations:{
        // 
        inc(state){
            state.cnt++;
        }
    },
    actions:{
        add(){
            // 触发更新事件修改状态值
            this.dispatch('inc');
        }
    }
});

结构解析

state

state vuex中保存状态(键值),这些值类似于受保护的全局变量。state原则上不应该直接修改,其作为一个module存在于Vue应用的全局对象中。

  1. 一个vuex的模板代码
// 在spa通常这样来启用一个vuex
import Vue from 'vue'
import Vuex from 'vuex'
// App模板
import App from './App.vue';
// 挂载vuex组件
Vue.use(Vuex);

// 初始化一个Vuex对象,此过程通常放到独立的js当中
const store = new Vuex({
    state:{},
    mutations:{},
    actions:{}
    // etc
});

// 创建Vue对象
const app = new Vue({
    el:'#app',
    store,
    // ... 其他项目
    render:h=>h(App)
});
  1. App.vue 中调用vuex的示例
<template>
<!-- u structure -->
</template>

<script>
// 简要代码
export default {
    name:'My Powerful Application',
    computed:{
        add(){
            // this.$store 即返回vuex
            return ++ this.$store.state.cnt;
        }
    }
    // ...
}
</script>

<style>
/** something styles */
</style>

getters

getters 相当于java中的封装概念,即需要对状态进行初级包装时设定具有适当逻辑的方法来访问封闭state中的值。每次调用均相当于执行一次函数,因此不适合大量计算。

  1. 利用getters访问属性
store.getters.doneList();
store.getters.getById(1); // {id:1, ...}
  1. 利用mapGetters辅助函数访问属性
import {mapGetters} from 'vuex'
export default {
    // ...
    computed:{
        // 直接返回列表
        ...mapGetters:([
            getById,
            doneList
        ]),
        // 指定返回的键
        mapGetters:({
            byId:'getById',
            done:'doneList'
        })
    }
}

mutation

mutation vuex中状态更改的入口,类似于触发一个事件,每个事件则对应一个调整方法,比如一个登陆行为可以触发修改token、应用state状态等。所有的state都应该提供一个修改的入口以便调用。mutation并非只是简单的修改一个状态值,同时还会触发相关的状态事件,比如同步ui等。

  1. 常见的mutation定义
const store = new Vuex({
    // ...
    mutations:{
        // state 为当前vuex对象的state集合
        SET_TOKEN:(state, token)=>{
            state.code = token;
        },
        SET_AVATAR:(state, avatar)=>{
            state.avatar = avatar;
        }
    }
});

// 未知地带调用, 状态应立即被修改
store.commit('SET_TOKEN', new_token);

// 对象风格
store.commit({
    type:'SET_TOKEN',
    new_token
});
  1. 注意事项
  • 初始化vuex对象时就定义好所有的state项
  • 临时增加属性的话会降低可读性,除非是有限范围内的逻辑否则还是定义时就确定
Vue.set(store, 'newKey', 'newValue'); // 没这么用过,推荐初始化后就不再调整了
  • mutation 必须是 ++同步函数++,不能在其中执行异步操作。原因是:mutation被调用后会执行相应的状态捕捉,如果是异步的话,调用时并不会捕获到新的状态,这回导致在必要的判定或UI更新上出现不可预计的错误。这类似于数据库开发中遇到的脏读、幻读等情况。
  1. 利用mapMutations映射到vue中
import {mapMutations} from './store'; // store为自定义的vuex
//...
export default{
    // 正常的姿势,注意是映射名是字符串
    ...mapMutations([
        'SET_TOKEN',
        'SET_AVATAR'
    ])
    // 或者指定键名
    ...mapMutations({
        setToken: 'SET_TOKEN',
        setAvatar: 'SET_AVATAR'
    })
}

action

action 提供了异步修改state的接口,这点与mutation不同。

  1. 常见的 action 定义
const store = new Vuex({
    state:{
        cnt:0
    },
    mutation:{
        inc(state){
            state.cnt ++;
        },
        add(state, num){
            state.cnt += num;
        }
    },
    actions:{
        inc(context){
            context.commit('inc')
        },
        // ES6中提供了参数简化的写法
        incByNum({commit}, num){
            // {commit} 映射了context中的commit方法, 等同于的store.commit,详见下文
            commit('add', num);
        },
        // 如果需要的话这样也成
        incWithLog({commit, state}, num){
            commit('add', num);
            console.log(state.cnt);
        }
    }
});

// 调用action时可传入的对象还有:
{
    state,      // 等同于 store.state, 如果在module中时则为局部状态
    rootState,  // 等同于 store.state, 只在module中可用
    commit,     // 等同于 store.commit
    dispatch,   // 等同于 store.dispatch
    getters,    // 等同于 store.getters
    rootGetters // 等同于 store.getters 只存在于module中
}
  1. 利用分发调用 action
// 一般性
store.dispatch('inc');

// 带参数
store.dispatch('incByNum', 1);

// 一对象形式也没问题
store.dispatch({
    type:'incWithLog',
    num:2
});
  1. mapActions 映射
import {mapActions} from 'vuex';

export default{
    ...mapActions([
        inc,
        incByNum,
        incWithLog
    ]),
    // 指定键名
    ...mapActions({
        inc:'inc',
        incByNum:'incByNum',
        incWithLog:'incWithLog'
    })
}
  1. 异步action
// 搬运个示例
actions:{
    actionA({commit}){
        // 这里返回一个Promise对象
        return new Promise((resolve, reject)=>{
            setTimeout(()=>{
                commit('someMutation');
                resolve();
            });
        }, 1000)
    }
}

// 完事你可以
store.dispatch('actionA').then(()=>{
    // do something async
});

// async await, 即需要Promise被resolve后执行,如:

actions:{
    async actionA:({commit}){
        commit('someMutation', await doSomething())
    }
    // 这种情况下 doSomething 应该返回Promise对象并且触发了resolve才会相应someMutation方法
}

async await 这里说明下,async方法必须返回Promise对象,并且对象的状态被resolved后 await方法才能执行,且await修饰的函数必须出现在async内。
这里参考此文
个人理解,这点相当于约束async函数唯一同步函数,不过是必须要等待resolve。

这里在搬运一参考文中的代码加深下理解:

//我们仍然使用 setTimeout 来模拟异步请求
function sleep(second, param) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(param);
        }, second);
    })
}

async function test() {
    let result1 = await sleep(2000, 'req01');
    let result2 = await sleep(1000, 'req02' + result1);
    let result3 = await sleep(500, 'req03' + result2);
    console.log(`
        ${result3}
        ${result2}
        ${result1}
    `);
}

test();
//req03req02req01
//req02req01
//req01

// 需要说明下rejected状态的处理,即增加一个try/catch包裹
async function tryMe(){
    try{
        await doSometing(); // 返回一个 Promise 对象
    } catch(e){
        processError(e);
    }
}

另外,ES6中的Promise对象详解请参考 《ECMAScript6入门》

module

module 分割vuex为多个模块,每个模块都可以拥有自己独立的特性。

  1. 一个平凡的module
const moduleA = {
    state:{...},
    mutations:{...},
    actions:{...}
};

const moduleB = {
    state:{...},
    mutations:{...},
    actions:{...}
};

// 构造一个vuex
const store = new Vuex({
    modules:{
        moduleA,
        moduleB
    }
    // 说明下这里的简写相当于
    modules:{
        moduleA:moduleA,
        moduleB:moduleB
        // 或者重命名
        a:moduleA,
        b:moduleB
    }
});

// 用一个
store.state.moduleA // moduleA状态
  1. 模块嵌套
// 不怎么用,直接搬运
const store = new Vuex({
    modules:{
        // 一级
        account:{
            namespace:true,
            state:{...},
            getters:{
                profile(){} // ->getters['account/profile']
            },
            actions:{
                login(){} // ->dispatch('account/login')
            },
            mutations:{
                login(){} // ->commit('account/login')
            }
        },
        // 二级
        modules:{
            // 集成父级命名空间
            profile:{
                state:{...},
                getters:{
                    view(){} // ->getters['account/view']
                }
            },
            posts:{
                namespace:true,
                state:{...},
                getters:{
                    // 嵌套了二级
                    hot(){} // ->getters['account/posts/hot']
                }
            }
        }
    }
});

这里每当指定了namespace:true后就需要增加一级目录,否则就是平行世界。一般应用基本上用不到这么复杂的命名空间,过多的模块嵌套反而提升程序复杂性。

  1. 跨级状态调用
// ...
modules:{
    moduleA:{
        namespace:true,
        getters:{
            getA(state, getters, rootState, rootGetters){
                getters.getB; // moduleA/getB
                rootGetters.getB // getB
            },
            getB: state=>{...}
        },
        mutations:{
            SET_TOKEN:(state, token){
                state.token = token;
            }
        },
        actions:{
            actionA({dispatch, commit, getters, rootGetters}){
                getters.getB // moduleA/getB
                rootGetters.getB // getB
                
                dispatch('actionB') // moduleA/getB
                dispatch('actionB', null, {root:true}) // getB
                // 注意,null指的是payload,也就是参数表
                
                commit('SET_TOKEN') // moduleA/actionA
                commit('SET_TOKEN', new_token, {root:true}) // actionA
            },
            actionB(ctx, payload){
                // ...    
            },
            actionC:{
                root:true, // 强制设为root级方法
                handle(namespacedCtx, payload){
                    // ...
                    // TODO 此需要测试下多级嵌套的问题
                }
            }
        }
    }
}
// ...

个人觉得跨级调用会产生很多可读性问题,看看示例知道意思就好


严格模式

严格模式下,任何不是由 mutation 函数引起的state改变都将报错

const store = new Vuex({
    // ...
    strict:true
    // 与开发环境结合,注意安装 cross-env 插件
    strict:process.env.NODE_ENV !== 'production'
})

表单处理

这里推荐一个双向绑定的示例

<input v-model="message"/>

<script>
// ..
export default{
    name:'App',
    computed:{
        get(){
            return this.$store.state.ff.message;
        },
        set(v){
            // 调用store的mutation修改
            this.$store.commit('updateMessage', v)
            this.$store.dispatch('someAction')
        }
    }
}
</script>

看多了文档有些迷茫,整理下来留个底方便查阅。如果你也喜欢的话千万不要吝啬收藏哦~!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值