Vuex学习笔记

290 阅读4分钟

1.vue应用的核心是store(仓库)

stor是个容器,包含应用中的大部分状态

vuex与全局对象的不同

1.vuex的状态存储是响应式,vue组件从store中读取状态时,若store的状态发生变化,组件也会得到更新 2.改变store状态唯一途径是现实的提交commit,可以方便跟踪抓柜台变化,能实现工具

创建store:提供初始state对象和mutation

import{} from 'vue'
import {} from 'vuex'
const store=createStore({
state(){
return{
count:0
}
},mutatons:{
increment(state){
state.count++
}
}
})
const app =createApp({根组件});
app.use(store);//把store实例作为插件安装

通过store.state()获取状态对象,store.commit()触发状态变更

vue组件中,this.$store访问store,可以在组件的方法中提交变更

methods:{
increament(){
this.$store.commit('increament');
console.log(this.$store.state.count);
}
}

通过在methods提交muation而非直接改变store.state.count,因为想更明确追踪状态变化,这样使意图更明显,能记录每次状态改变

2.State

单一状态树

vuex用一个对象包含全部应用层及状态,作为唯一数据源,每个应用只有一个store实例,能直接定位任意特定状态片段,调试过程能轻易取得应用状态快照。vuex中数据和vue实例的data规则相同,状态对象必须纯粹(plain)

vue组件获得vuex状态

vuex状态存储响应式,从store中读取状态最简单的就是计算书心中返回某个状态。

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

count变化时,都会重新求取计算属性(computed),触发更新相关的DOM,vuex通过vue插件把store实例从根组件注入所有子组件,子组件通过this.$store访问 return store.state.count

mapState辅助函数

// 在单独构建的版本中辅助函数为 Vuex.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
    }
  })
}

mapState函数返回一个对象,可以使用对象展开运算符简化写法:

computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

有些状态严格属于单个组件,还是作为组建的局部状态比较好,不需要放入vuex

3.Getter

有时需要从state中派生一些状态,比如对列表过滤并计数:

return this.$store.state.todos.filter(todo => todo.done).length

getter可以认为是store的计算属性,接受state作为第一个参数:

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

通过属性访问

通过属性访问时,是作为vue的响应式系统的一部分缓存其中的 可以以属性访问Getter: store.getters.doneTodos 可以接受其他getter作为第二个参数:

getters: {
  // ...
  doneTodosCount (state, getters) {
    return getters.doneTodos.length
  }
}

使用时:

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

通过方法访问

可以让getter返回一个函数实现给getter传参,getter通过方法访问每次都会调用,不会缓存结果

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

mapGetters辅助函数

把store的getter映射到局部计算属性:

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

把getter属性取一个别的名字,可以用对象形式:

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

4.Mutation

mutation类似于事件,每个mutation有一个字符串的事件类型type和回调函数handler,回调函数时进行状态更改的地方,会接受state作为第一个参数。不能直接调用mutation处理函数,要唤醒一个mutation处理函数,需要以相应的type调用store.commit。 store.commit('increment)

提交载荷

可以传入额外参数作为载荷,通常hi一个对象,这样含多个字段并记录的mutation更易读

对象风格的提交方式

直接使用含type属性的对象,此时整个对象作为载荷传给mutation,处理函数不变

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

常量替代mutation事件类型

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = createStore({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // 修改 state
    }
  }
})

mutation必须是同步函数

因为在mutation中异步函数的回调使状态改变不可追踪

在组件中提交

了一使用this.$store.commit(),或者mapMutation辅助函数把methods映射成store.commit调用,需要在根节点注入store

5.Action

Action可以包含任意异步操作,提交的是mutation,不是直接变更状态。

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

action接受了和store实例有相同方法和属性的context对象,所以调用context.commit提交mutation。可以这样简化:

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

分发action

action内部可以执行异步操作

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

actions支持在和方式和对象方式进行分发 store.dipach('',{amount: })//载荷 store.dispatch({type:'' ,amount:}) 进行一系列一部蔡总,并通过提交mutation记录action副作用(状态变更)

组件中分发action

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

组合Action,promise

store.dispatch可以处理被触发的action的处理函数返回的Promise,并且store.dispatch仍然返回promise,store.dispatch在不同模块中可以触发多个action函数,只有所有触发函数完成后,返回的promise才会执行。 利用asnyc/await,可以如下组合action:

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

6.Module

为了解决单一状态树下,应用所有状态集中到较大对象,当应用变得非常负责,store对象就可能变得臃肿,vuex允许将store分割成模块(module),每个模块有自己的state,mutation,action,getter,嵌套子模块:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

模块的局部状态

模块内部mutation和getter,接受的第一个参数使模块的局部状态对象(也是state,但是是本模块的),模块内的action,局部状态通过context.state暴露出来,根节点为context.rootState,对于模块内部的getter,根节点状态会作为点第三个参数暴露出来。

命名空间

模块内部的action和mutation仍然是注册在全局命名空间,这样使得多个模块能够对同一个action或mutation作出响应。Getter默认注册在全局命名空间,但是目前这并非出于功能上的目的(只是维持现状避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的getter从而导致错误。可以添加namespaced:true使其成为带命名空间的模块。当模块被注册后,它的所有getter\action及mutation都会自动根据模块注册的路径调整命名。启用了命名空间的getter和Action会受到局部化的getter,dispatch和commit,在使用模块内容时不需要在同一模块内额外添加空间名前缀。更改namespace属性后不需要修改模块内的代码。

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

使用全局state和getter,rootState和rootGetters会作为第三和第四参数传入getter,也会通过context对象的属性传入action。 若需要在全局命名空间内分发action或提交mutation,将{root:true}作为第三参数传给dispatch或commit即可。

在带命名空间的模块注册全局action

若需要在带命名空间的模块注册全局action,你可添加root:true,并将这个action的定义放在函数handler

带命名空间的绑定函数

当使用mapState,mapGetters,mapActions和mapMutations,这些辅助函数来绑定带命名空间的模块时,写起来可能比较繁琐:

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

可以把模块的空间名称字符串作为第一个参数传送给上述函数,这样所有绑定都会自动将该模块作为上下文

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

使用createNamespacedHelpers创建基于某个命名空间辅助函数,返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数。

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

插件开发注意事项

Plugin(插件)提供了模块并循序用户将其添加到Vuex store,需要考虑模块的空间名称问题,可以通过插件的参数对象来允许用户指定空间名称

const namespace = options.namespace || ''
    store.dispatch(namespace + 'pluginAction')

模块动态注册

在store创建之后,你可以使用store.registerModule方法注册模块

import { createStore } from 'vuex'

const store = createStore({ /* 选项 */ })

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

通过store.state.myModule和store.state.nested.myModule访问模块的状态。 模块动态注册功能使其他vue插件可以通过在store中附加新模块来使用vuex管理状态,vuex-router-sync插件就是通过动态注册模块把vueRouter和Vuex结合,实现应用的路由管理。 store.hasModule(moduleName)方法检查模块是否已被注册到store,嵌套模块以数组形式传给registerModule和hasModule,不是以路径字符串形式传递给module

保留state

在注册一个新module时,你很有可能想保留过去的state,例如从一个服务端渲染的应用保留state.可以通过preserveState选项将其归档:store.registerModule('a', module, { preserveState: true }),设置后该模块会被注册,action,mutation,getter被添加到store中,state不会

模块重用

需要创建一个模块的多个实例,使用纯对象来声明模块装填,状态对象会通过引用被共享,修改时store或模块间数据会相互污染,和vue组件的data时同样的问题,可以使用函数来声明模块状态(2.3.0+支持)

const MyReusableModule = {
  state: () => ({
    foo: 'bar'
  }),
  // mutation、action 和 getter 等等...
}

7.项目结构

1.应用层级状态集中到单个store对象中

2.提交mutation时更改状态的唯一方法,且过程同步

3.一部逻辑都封装到action

8.组合式API

通过调用useStore函数,在setup钩子函数中访问store,这和组件中用选项式apo访问this.$store等效


import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()
  }
}

访问State和Getter

访问state,getter,需要创建computer引用保留响应性,与选项式api中计算属性等效

import { computed } from 'vue'
import { useStore } from 'vuex'

export default {
setup () {
 const store = useStore()

 return {
   // 在 computed 函数中访问 state
   count: computed(() => store.state.count),

   // 在 computed 函数中访问 getter
   double: computed(() => store.getters.double)
 }
}
}

访问Mutaion和Action

Setup钩子函数中调用commit和dispatch函数

setup () {
    const store = useStore()

    return {
      // 使用 mutation
      increment: () => store.commit('increment'),

      // 使用 action
      asyncIncrement: () => store.dispatch('asyncIncrement')
    }

9.插件

Vuex的Store接受plugins选项,暴露出每次mutation的狗子,vuex插件是一个函数,接收store作为唯一参数:

const myPlugin = (store) => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
  })
}

使用方法:

const store = createStore({
  // ...
  plugins: [myPlugin]
})

插件中提交Mutation

,提交mutation,插件可以同步数据源到store,比如同步websocket数据源到store

export default function createWebSocketPlugin (socket) {
  return (store) => {
    socket.on('data', data => {
      store.commit('receiveData', data)
    })
    store.subscribe(mutation => {
      if (mutation.type === 'UPDATE_DATA') {
        socket.emit('update', mutation.payload)
      }
    })
  }
}
const plugin = createWebSocketPlugin(socket)

const store = createStore({
  state,
  mutations,
  plugins: [plugin]
})

生成State快照

插件要获得状态的快照,比较改变前后状态,需要对状态对象进行深拷贝:

const myPluginWithSnapshot = (store) => {
  let prevState = _.cloneDeep(store.state)
  store.subscribe((mutation, state) => {
    let nextState = _.cloneDeep(state)

    // 比较 prevState 和 nextState...

    // 保存状态,用于下一次 mutation
    prevState = nextState
  })
}

生成状态快照的插件只在开发阶段使用,使用Webpack,让构建工具帮我们处理:

onst store = createStore({
  // ...
  plugins: process.env.NODE_ENV !== 'production'
    ? [myPluginWithSnapshot]
    : []
})

发布阶段,需要使用webpack的DefinePlugin使process.env.NODE_ENV !=='production'为false

内置Logger插件

vuex自带日记插件用于一般的调试,createLogger有几个配置项。日志插件可以通过

10.严格模式

开启严格模式,无论何时发生状态变更且不失mutation引起,就会抛出错误,创建store时传入strict:true即可,发布环境不要启用,会深度监测状态树,关闭以避免性能损失,类似于插件,可以用构建工具处理这种情况:stict:process.env.NODE_ENV !='production'. 严格模式在state上用v-model,会修改obj,抛出错误,所以可以在中绑定value,侦听input或change事件,在事件回调中调用方法

input :value="message" @input="updateMessage">
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}
mutations: {
  updateMessage (state, message) {
    state.obj.message = message
  }
}

双向绑定的计算属性

可以用带setter的双向绑定计算属性:

<input v-model="message">
// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

#11.测试

Mutation

使用ES2015,将mutation定义在store.js,除了模块的默认到处,还应该把mutation进行命名导出:export const mutations={...} Getter类似

Action

需要增加mocking服务层 ,把api调用抽象成服务,然后测试文件用mock服务回应API调用。解决mock以来,可以利用webpack和inject-loader打包测试文件

执行测试

mutation和action编写正确,经过mocking处理后,这些测试不依赖任何浏览器api,可以直接用webpack打包测试文件在Node中执行:

// webpack.config.js
module.exports = {
  entry: './test.js',
  output: {
    path: __dirname,
    filename: 'test-bundle.js'
  },
  module: {
    loaders: [
      {
        test: /.js$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
}

命令行:

webpack
mocha test-bundle.js

12.热重载

用Webpack的Hot Module Replacement API,vuex支持在开发过程中热重载mutation,getter,action,getter,mutation和module,用store.hotUpdate().仅使用模块可以用require.context动态加载或热重载所有模块