Vue2饥人谷技术博客Web前端之路

Vuex一篇文章总结

2017-08-03  本文已影响1762人  sunny519111

vuex

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

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

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

单向数据流

单向数据流图
  1. 所有的状态都是通过state反应
  2. 所有的组件数据驱动都是来自于一个对象

运行原理

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

开始使用

项目中简单的使用可以查看我的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'
      }
    }
  }
}
上一篇 下一篇

猜你喜欢

热点阅读