Vuex一篇文章总结

vuex

场景重现:一个用户在注册页面注册了手机号码,跳转到登录页面也想拿到这个手机号码,你可以通过vue的组件化通讯来实现数据的传递,子组件传递事件$emit('事件', data),父组件来监听这个自定义事件@事件调用这个方法,如果对于数据复杂的情况下,你是不是需要一个能够让所有的组件都访问一个数据源的需求?

Vuex:就是专门管理vue.js开发的状态管理模式,集中管理了组件的状态和数据,这样我们可以清楚的知道哪一个数据被改变。

什么情况下需要使用vuex: 当你的页面数据很复杂,通讯很复杂的时候,vuex就是一个非常不错的选择了。

单向数据流

  • state,驱动应用的数据源;
  • view,以声明方式将state映射到视图;
  • actions,响应在view上的用户输入导致的状态变化。
单向数据流图
  1. 所有的状态都是通过state反应
  2. 所有的组件数据驱动都是来自于一个对象

运行原理

  1. Vue组件通过dispatch来触发Vuex的actions
  2. Vuex的actions触发自己内部的mutations
  3. mutations触发内部的数据源state
  4. 数据源(state)反过来渲染Vue组件
运行原理

开始使用

  • 安装依赖

    npm install --save vuex
    
  • 使用

    1. 在项目中的src下面创建一个store目录,然后在store目录下创建一个store.js文件
    // src下面的store.js
    import Vue from 'vue';
    import Vuex from 'vuex'
    Vue.use(vuex);
    
    export default new Vuex.Store({
      state: {
        count: 0
      },
      mutations: {
        increment: (state) => state.count++;
        decrement: (state) => state.count--;
      }
    })
    

    解析:上面的代码导入了vue和vuex模块,然后注册使用vuex,最后导出一个vuex的实例,在state中定义了count属性,用来计数,然后通过mutations定义了incrementdecrement方法用来数据的加减。

    1. 在项目的vue初始化的时候引用并初始化。main.js中写下如下代码
    import store from '.src/store.js'  //引入store
    
    //vue实例中使用
    new Vue({
      el: '#app',
      router,
      store,
      template: '<App/>',
      components: { App }
    })
    
  • 解释说明

    每一个vuex的核心就是一个store(仓库)。store相对于是一个容器,里面存放着你的项目中的大部分的状态。

    主要注意下面2点来理解vuex

    1. Vuex的状态管理是响应式的。我们在Vue组件中使用了state,如果vuex中的state发生变化的时候,对应的组件也会相应的state也会发生变化,所以一般在vue组件中使用计算属性computed,来获取vuex中state的值。
    2. 在严格模式下,state的状态只能通过mutations来通过commit来修改。这样方便我们对于数据源和数据的监听和使用。

项目中简单的使用可以查看我的github地址

核心api

vuex中主要的状态管理和模块化都是通过5个api来实现交互和数据的传递。

  1. state
  2. mutations
  3. getters
  4. actions
  5. modules

1. state(状态的管理)

单一的状态树

Vuex使用的是单一的状态管理,一个仓库store包含了项目中所有的数据,每一个应用都只包含一个store实例,单一的状态树可以让我们更加直接定位到对应的数据源。

单一的状态树和一切皆模块的思想并不冲突----后面我们会讲到通过vuex的模块化机制来管理和分布到各个文件中。

Vue组件中获取vuex的状态(state)

由于Vuex中的状态储存是响应式的,从store实例中获取读取状态最好是通过计算属性来返回某个状态。

computed: {
  count() {
    return this.$store.state.count
  },
}

每次当数据源this.$store.state.count发生变化的时候,都会触发计算属性重现计算并且触发相应的dom渲染。

mapState辅助函数

当一个组件需要很多状态的时候,将这些状态都声明成computed是不是会显得很冗余,为了解决这个问题,我们需要引入mapState辅助函数帮助我们生成计算属性(少些了this.$store,和store.js里面写法一样了)。

// 对应的文件引入mapState 
import {mapState} from 'vuex'

export default {
  // ...
  computed: mapState( {
    count: state => state.count,
    
    //传字符串参数'count', 等同于`state => state.count`
    countAlias: 'count',
    
    // 为了能够使用` this `,获取局部变量,必须使用常规函数
    countPlusLocalState(state) {
      return state.count + this.localCount
    }
  })
}

当映射的计算属性的名称和state的子节点的名称相同时,我们也可以给mapState传入一个字符串数组。

computed: mapState(['count']) //映射  this.count 为 store.state.count

// 当我们执行mapState的时候返回的是一个对象,包含了我们传入的参数
mapState(['count','todos'])  //运行函数
// 得到了一个对象
Object
    count: function mappedState()
    todos: function mappedState()
    __proto__

对象展开符

mapState函数返回的是一个对象。我们如果才能把它和局部的计算属性混合使用呢?就是说,我们需要一个工具,把多个对象合并成一个对象,ES6的对象展开运算符正好满足

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

// 实际运用
computed: {
  name() {
    return this.$store.state.a.name
  },
  ...mapState(['count']),
},

组件中仍然可以保有局部变量

使用了Vuex后并不代表你所有的状态都需要放到Vuex中,虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。

2.Getters

有时候我们需要从store中的state中派生一些状态,例如对列表进行过滤并计数

computed: {
  doneTodosCount: {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

如果我们有多个组件都需要用到此属性,我们是选择复制这个函数,还是抽取成一个共享函数然后多处导入--好像不管是哪一种都不太合理。

Vuex允许我们在store中定义『getters』(可以认为是 store 的计算属性)。

Getters接受state作为它的第一个参数:

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodo(state){
      return state.todos.filter(todo => todo.done)
    }
  }
})

同时Getters会被暴露成为store.getters对象

// store.js中使用
store.getters.doneTodo // -> [{ id: 1, text: '...', done: true }]

Getters也可以接受其他getters作为第二个参数

getters: {
  //...
  doneTodoCount:(state,getters){
    return getters.doneTodo.length
  }
}

store.getters.doneTodosCount // -> 1

我们也可以在组件中使用getters

computed: {
  doneTodoCount() {
    return this.$store.getters.doneTodoCount
  }
}

mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getters 映射到局部计算属性:传入一个数组

import {mapGetters} from vuex;

export default {
  computed: {
    // 使用对象展开运算符将 getters 混入 computed 对象中
    ...mapGetters(['doneTodoCount','anotherGetter'])
  }
}

可以将getters属性另取一个值,使用对象的模式

mapGetters({
   // 映射 this.doneCount 为 this.$store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})

3.Mutations

上一部分我们知道getters可以说是state的计算属性,并不能改变state的值。更改Vuex中state的唯一方法就是提交mutations。Vuex中的mutations类似于一个事件。每一个mutation都拥有一个事件类型(type)回调函数(handle)。这个回调函数就是我们实际进行状态修改的地方,并且它接受state作为第一个参数

const store = new Vuex.store({
  state: {
    count: 1
  },
  mutations: {
    increment(state){
      // 变更状态
      state.count++
    }
  }
})

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

store.commit('increment')
this.$store.commit('increment')

提交载荷(Payload)

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

// ...
mutations: {
  increment(state,n) {
    state.count += n
  }
}

//如果payload是一个值,就会被直接覆盖旧的载荷

在大多数情况下,载荷都是一个对象,这样可以包含多个字段并且记录的mutations更加具有可读性

//... 
mutations: {
  increment(state,payload) {
    state.count+ = payload.amount
  }
}

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

对象风格

提交mutations的另一种方式就是使用包含的type属性

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

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

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

Mutations 需遵守 Vue 的响应规则

由于Vuex的state是响应式的,当我们状态更新的时候,对应的监听状态的vue也会更新,这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

  1. 最好提前在你的 store 中初始化好所有所需属性。

  2. 当需要在对象上添加新属性时,你应该

    • 使用 Vue.set(obj, 'newProp', 123), 或者 -

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

      state.obj = { ...state.obj, newProp: 123 }
      

Mutation 必须是同步函数

一条重要的原则就是要记住mutation 必须是同步函数

当我们改变数据的时候,需要知道对应的数据变化,如果是异步的函数,发送请求,我们不知道什么时候请求返回,这样我们无法跟踪state的改变

组件中提交mutations

和上面的一样,我们可以直接通过this.$store.commit('xxx')提交mutations,也可以通过辅助函数mapMutations将组件中的methods映射到对应的store.commit中。

import {mapMutations} from vuex 

export default{
  // ... 
  methods: {
    ...mapMutations(['increment']),  // this.increment() 为 this.$store.commit('increment')
    ...mapMutations({
      add: 'increment'   // 映射 this.add() 为 this.$store.commit('increment')
    })
  }
}

4.Actions

action类似于mutation,不同在于:

  1. Action提交的是mutation,而不是直接变更状态
  2. Action可以包含任何异步操作

一个简单的action例子

const store = new Vuex.store({
  state: {
    count: 0
  },
  mutations: {
    increment(state){
      state.count++
    }
  },
  actions: {
    increment(context){
      context.commit('increment')
    }
  }
})

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

实际工作做,我们会经常用到ES2015的参数解构来简化代码

actions: {
  increment({commit}){
    commit('increment')
  }
}

分发Action

Action通过store.dispatch方法触发

store.dispatch('increment')

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

actions: {
  incrementAsync({commit}){
    setTimeout(() => {
      commit('increment')
    },1000)
  }
}

由于action提交的是mutations,所以Actions同样支持载荷方式对象方式进行分发:

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

在组建中分发Action

在组件中使用this.$store.dispatch('xxx')分发action,或者使用mapActions辅助函数将组件的methods映射为store.dispatch调用

import {mapActions} from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions(['increment']) //映射this.increment() 为this.$store.dispatch('increment')
    ...mapActions({
      add: 'increment' // 映射 this.add() 为 this.$store.dispatch('increment')
    })
  }
}

组合Actions

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

首先,我们应该知道store.dispatch可以处理被触发的action的回调函数返回的promise,并且store.dispatch仍旧返回Promise:

// actionA返回一个promise并触发`someMutation`
actions: {
 actionA({commit}){
   return new Promise((resolve,reject) => {
     setTimeout(() => {
       commit('someMutation')
       resolve()
     },1000)
   })
 }  
}  

现在我们可以调用actionA

store.dispatch('actionA').then(() => {
  // ....
})

也可以在另一个action中调用actionA

actions: {
  //...
  actionB({dispatch, commit}) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

当然,如果我们通过async/await来处理,更加方便的组合action:

//store.js 公司代码为例
state: {
  user: ''
}
mutations: {
  update(state,user){
    state.user = user;
  }
}
actions: {
  // 登录页面
  async login({dispatch},data) {
    return await dispatch('update',await fetch.post('/front/login',data))
  }
  // 更新登录状态
  async update({commit},user){
    commit('update',user)
  }
}

登录页面中调用actions中的login

// login.vue
this.$store.dispatch('login',{
  username: 'hcc',
  password: '123456'
})

Modules

由于使用单一状态树,应用的所有状态都会集中到一个大的对象中,如果项目很复杂,store的状态就会相当的臃肿。

为了解决这个问题,Vuex允许将store分割成模块(module),每一个模块都有自己的state,mutations,actions,getter,甚至是嵌套子模块——从上至下进行同样方式的分割:

// 声明2个模块
const moduleA = {
  state: {...},
  getters: {...},
  mutations: {...},          
  actions: {...},
}
const moduleB = {
  state: {...},
  getters: {...},
  mutations: {...}          
}              

// 开始使用
const store = new Vuex.Store({
    modules: {
      a: moduleA,
      b: moduleB       
    }
})  
// 调用
store.state.a   // moduleA 的状态
store.state.b   //moduleB 的状态          

模块的局部状态

对于模块内部的getters和mutations,接受的第一个参数都是模块的局部状态对象

const moduleA = {
  state: {count: 0},
  mutations: {
    increment(state) {
      //这里的`state`的对象是模块的局部状态
      state.count++
    }
  },
  getters: {
    doubleCount(state) {
      return state.count * 2 
    }
  }
}

同样的对于模块内部的actions,接受一个局部状态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
    }
  }
}

命名空间

刚刚我们只有对state进行根节点和模块节点,对于模块内部的gettersmutationsactions并没有区分。

因为默认情况下,模块内部的gettersmutationsactions是注册在全局命名空间的---这样使得多个模块能够对同一mutations或actions做出响应。如果你希望你的模块更加具有包含和提高可复用性,可以通过namespaced: true 的方式使其成为命名空间模块,当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,
      state: {....},
      getters: {
        isAdmin() {...}    // => getters['account/idAdmin']
      },
      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']
          }
        }
      }  
    }
  }
})

对于启用了命名空间的gettersactionsmutations会收到一个局部的getter,dispatch,commit

在命名空间模块内访问全局内容

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

若需要在全局命名空间中分发actions或者提交mutations,需要将{root: true}作为第三个参数传入到dispatchcommit

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

推荐阅读更多精彩内容