vuex 学习笔记

82 阅读3分钟

一、Vuex基本使用

1.1 安装搭建

  • 安装vuex(本文使用4.x)

    • npm install vuex
  • 创建store

    import { createStore } from 'vuex'
    
    const store = createStore({
      state() {
        return {
          counter: 0
        }
      }
    })
    
    export default store
    
  • app.use(store)

    import { createApp } from 'vue'
    import store from './store'
    import App from './App.vue'
    
    createApp(App).use(store).mount('#app')
    
  • tempate -> $store.state.counter

    <template>
      <div class="app">
        <h2>App当前计数: {{ $store.state.counter }}</h2>
      </div>
    </template>
    

1.2 Vuex的状态管理

  • 每一个Vuex应用的核心就是store(仓库):

    • store本质上是一个容器,它包含着你的应用中大部分的状态(state);
  • Vuex和单纯的全局对象有什么区别呢?

    • 第一:Vuex的状态存储是响应式

      • 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新
    • 第二:不能直接改变store中的状态

      • 改变store中的状态的唯一途径就显示提交 (commit) mutation

      • 这样使得我们可以方便的跟踪每一个状态的变化,从而能够通过一些工具帮助更好的管理应用的状态

    image.png

1.3 组件中使用store

<template>
  <div class="home">
    <h2>Home当前计数: {{ $store.state.counter }}</h2>
    <h2>Computed当前计数: {{ storeCounter }}</h2>
    <h2>Setup当前计数: {{ counter }}</h2>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
export default {
  computed: {
    storeCounter() {
      return this.$store.state.counter
    }
  }
}
</script>

<script setup>
import { toRefs } from 'vue'
import { useStore } from 'vuex'

const store = useStore()
const { counter } = toRefs(store.state)


function increment() {
  // 不能直接修改
  // store.state.counter++
  store.commit('increment')
}
</script>

二、Vuex核心概念

2.1 State

2.1.1. 基本使用

import { createStore } from 'vuex'

const store = createStore({
  state() {
    return {
      name: 'coder',
      level: 88,
      avatarUrl: 'http://123.431.342.5:xxx.png'
    }
  }
})

export default store
<template>
  <div class="home">
    <!-- 在模板中直接使用多个状态 -->
    <h2>name: {{ $store.state.name }}</h2>
    <h2>level: {{ $store.state.level }}</h2>
    <h2>avatarUrl: {{ $store.state.avatarUrl }}</h2>
  </div>
</template>

2.1.2 映射使用(mapState)

  • options api
<template>
  <div class="home">
    <!-- 计算属性(映射状态:数组语法) -->
    <h2>name: {{ name }}</h2>
    <h2>level: {{ level }}</h2>

    <!-- 计算属性(映射状态:对象语法) -->
    <h2>name: {{ sName }}</h2>
    <h2>level: {{ sLevel }}</h2>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState(['name', 'level', 'avatarUrl']),
    ...mapState({
      sName: state => state.name,
      sLevel: state => state.level
    })
  }
}
</script>
  • composition api

    1. 通过 bind 绑定 this
    import { computed } from 'vue'
    import { useStore, mapState } from 'vuex'
    
    const { name, level } = mapState(['name', 'level'])
    const store = useStore()
    const cName = computed(name.bind({ $store: store }))
    const cLevel = computed(level.bind({ $store: store }))
    
    1. 封装hook
    import { computed } from 'vue'
    import { useStore, mapState } from 'vuex'
    
    export function useState(mapper) {
      const store = useStore()
      const stateFnsObj = mapState(mapper)
      const state = {}
    
      Object.keys(stateFnsObj).forEach(fnKey => {
        state[fnKey] = computed(stateFnsObj[fnKey].bind({ $store: store }))
      })
    
      return state
    }
    
    import { useState } from '../hooks/useState'
    const { name, level } = useState(['name', 'level'])
    
    1. 使用 toRefs 直接对store进行解构(推荐
    import { toRefs } from 'vue'
    import { useStore } from 'vuex'
    
    const store = useStore()
    const { name: cName, level: cLevel } = toRefs(store.state)
    

2.2 getter

2.2.1 基本使用

  • 直接使用

  • 引入别的getters

  • 返回函数, 接收参数

    import { createStore } from 'vuex'
    
    const store = createStore({
      state() {
        return {
          counter: 100,
          name: 'coder',
          level: 88,
          avatarUrl: 'http://123.431.342.5:xxx.png',
          friends: [
            { id: 111, name: "kobe", age: 24 },
            { id: 112, name: "james", age: 22 },
            { id: 113, name: "curry", age: 21 }
          ],
        }
      },
      getters: {
        doubleCounter(state) {
          return state.counter * 2
        },
        totalAge(state) {
          return state.friends.reduce((pre, cur) => {
            return pre + cur.age
          }, 0)
        },
        // 引入别的getters
        info(state, getters) {
          return `name: ${state.name} level: ${state.level} friendsTotalAge: ${getters.totalAge}`
        },
        // 引入函数,接收参数
        getFriendById(state) {
          return function(id) {
            return state.friends.find(friend => friend.id === id)
          }
        }
      }
    })
    
    export default store
    

2.2.2 映射使用

  • options api

    import { mapGetters } from 'vuex'
    export default {
      computed: {
        ...mapGetters(['doubleCounter', 'totalAge']),
        ...mapGetters(['getFriendById'])
      }
    }
    
  • composition api

    import { useStore, mapGetters } from 'vuex'
    
    // const { info: infoFn } = mapGetters(["info"])
    // const sInfo = computed(infoFn.bind({ $store: store }))
    
    const store = useStore()
    const sInfo = computed(() => store.getters.info)
    const getFriendById = computed(() => store.getters.getFriendById)
    

2.3 Mutations

2.3.1 重要原则

  • 修改state, 必须使用mutation
  • 不要在mutation执行异步操作, 必须同步操作

2.3.2 基本使用

  • 直接使用

  • 传入参数

    const store = createStore({
      // state() {...},
      mutations: {
        increment(state) {
          state.counter++
        },
        changeName(state, payload) {
          state.name = payload
        },
        incrementLevel(state) {
          state.level++
        },
        changeInfo(state, newInfo) {
          state.name = newInfo.name
          state.level = newInfo.level
        }
      }
    })
    
    import { useStore } from 'vuex'
    
    const store = useStore()
    
    function incrementLevel() {
      store.commit('incrementLevel')
    }
    

2.3.3 映射使用

  • options api

    <template>
      <div class="home">
        <button @click="changeName('张三')">修改name</button>
        <button @click="incrementLevel">数组语法修改level</button>
        <button @click="storeIncrementLevel">对象语法修改level</button>
        <button @click="changeInfo({name: '李四', level: 120})">修改Info</button>
        <h2>store name: {{ $store.state.name }}</h2>
        <h2>store level: {{ $store.state.level }}</h2>
      </div>
    </template>
    
    <script>
    import { CHANGE_INFO } from '../store/mutations_types'
    import { mapMutations } from 'vuex'
    export default {
      methods: {
        ...mapMutations(['changeName', 'incrementLevel', CHANGE_INFO])
        // 对象语法重命名
        ...mapMutations({
          storeIncrementLevel: 'incrementLevel'
        })
      }
    }
    </script>
    
  • composition api

    import { CHANGE_INFO } from '../store/mutations_types'
    
    import { useStore, mapMutations } from 'vuex'
    
    const store = useStore()
    
    const mutationsObj = mapMutations(['changeName', 'incrementLevel', CHANGE_INFO])
    const newMutations = {}
    
    Object.keys(mutationsObj).forEach(key => {
      newMutations[key] = mutationsObj[key].bind({ $store: store })
    })
    
    const {
      changeName,
      incrementLevel,
      changeInfo
    } = newMutations
    

2.3.4 使用常量

  • 定义常量:mutation-type.js
    • export const CHANGE_INFO = 'changeInfo'
  • 定义mutation
    [CHANGE_INFO](state, newInfo) {
      state.name = newInfo.name
      state.level = newInfo.level
    }
    
  • 提交mutation
    • this.$store.commit(CHANGE_INFO, { name: '李海', level: 99 })
    import { useStore } from 'vuex'
    
    const store = useStore()
    
    function changeInfo() {
      store.commit(CHANGE_INFO, {
        name: '李海',
        level: 99
      })
    }
    

2.4 Actions

2.4.1 actions的基本使用

  • 定义action

    const store = createStore({
      state() {
        return {
          counter: 100,
          name: 'coder'
      },
      mutations: {...},
      actions: {
        incrementAction(context) {
          // console.log(context.commit) // 用于提交mutation
          // console.log(context.getters) // getters
          // console.log(context.state) // state
          context.commit("increment")
        },
        changeNameAction(context, payload) {
          context.commit('changeName', payload)
        }
      }
    })
    
  • dispatch派发action

    <script>
    export default {
      methods: {
        incrementAction() {
          this.$store.dispatch('incrementAction')
        },
        // changeNameAction() {
        //   this.$store.dispatch('changeNameAction', '赵四')
        // }
      }
    }
    </script>
    
    <script setup>
    import { useStore } from 'vuex'
    
    const store = useStore()
    
    function changeNameAction() {
      store.dispatch('changeNameAction', '王云')
    }
    </script>
    

2.4.2 actions的辅助函数

<script>
import { mapActions } from 'vuex'
export default {
  methods: {
    ...mapActions(['incrementAction'])
  }
}
</script>

<script setup>
import { useStore, mapActions } from 'vuex'

const store = useStore()

// 使用mapActions
const actionsObj = mapActions(['changeNameAction'])
const newActions = {}
Object.keys(actionsObj).forEach(key => {
  newActions[key] = actionsObj[key].bind({ $store: store })
})
const { changeNameAction } = newActions

</script>

2.4.3 actions的异步操作

  • store

    const store = createStore({
      state() {
        return {
          counter: 100,
          name: 'coder',
    
          // 服务器数据
          banner: [],
          recommend: []
        }
      },
      mutations: {
        changeBanner(state, banner) {
          state.banner = banner
        },
        changeRecommend(state, recommend) {
          state.recommend = recommend
        }
      },
      actions: {
        async fetchHomeMultidataAction(context) {
          // 1. 返回Promise,给Promise设置then
          // fetch('http://xxx.207.xxx.32:8000/home/multidata').then(res => {
          //   res.json().then(data => {
          //     console.log(data)
          //   })
          // })
    
          // 2. Promise链式调用
          // fetch('http://xxx.207.xxx.32:8000/home/multidata').then(res => {
          //   return res.json()
          // }).then(data => {
          //   console.log(data)
          // })
    
          // 3. async/await
          const res = await fetch('http://xxx.207.xxx.32:8000/home/multidata')
          const data = await res.json()
          // console.log(data)
          context.commit('changeBanner', data.data.banner.list)
          context.commit('changeRecommend', data.data.recommend.list)
        }
      }
    })
    
  • 使用

    <template>
      <div class="home">
        <h2>Home Page</h2>
        <ul>
          <template v-for="item in $store.state.banner" :key="item.acm">
            <li>{{ item.title }}</li>
          </template>
        </ul>
      </div>
    </template>
    
    <script setup>
    import { useStore } from 'vuex'
    
    const store = useStore()
    
    // 告诉vuex发送网络请求
    store.dispatch('fetchHomeMultidataAction')
    </script>
    

2.4.4 action结果返回Promise

  • Action 通常是异步的,那么如何知道 action 什么时候结束呢?
    • 可以通过让action返回Promise,在Promise的then中来处理完成后的操作

image.png

2.5 Module

2.5.1 module的基本使用

  • 抽取到对象:
    • state
    • mutations
    • getters
    • actions
export default {
  state: () => ({
    // 服务器数据
    banner: [],
    recommend: []
  }),
  mutations: {
    changeBanner(state, banner) {
      state.banner = banner
    },
    changeRecommend(state, recommend) {
      state.recommends = recommend
    }
  },
  actions: {
    fetchHomeMultidataAction(context) {
      return new Promise(async (resolve, reject) => {
        const res = await fetch("http://xxx.207.xxx.32:8000/home/multidata")
        const data = await res.json()
        
        // 修改state数据
        context.commit("changeBanner", data.data.banner.list)
        context.commit("changeRecommend", data.data.recommend.list)

        resolve("aaaaa")
      })
    }
  }
}
  • modules: { home: 对象 }
  • state.home.xxx
  • getters.xxx
  • commit
  • dispatch
<template>
  <div class="home">
    <h2>Home Page</h2>
    <ul>
      <template v-for="item in $store.state.home.banner" :key="item.acm">
        <li>{{ item.title }}</li>
      </template>
    </ul>
    <hr>
    <h2>Counter Page</h2>
    <!-- 使用state时,需要state.moduleName.xxx -->
    <h3>counter: {{ $store.state.counter.count }}</h3>
    <!-- 使用getter时,直接getters.xxx -->
    <h3>doubleCounter: {{ $store.getters.doubleCount }}</h3>
    <button @click="decrement">count模块-1</button>
  </div>
</template>

<script setup>
import { useStore } from 'vuex'

const store = useStore()

// 告诉vuex发送网络请求
// 派发事件和提交mutation时,默认不需要跟模块
store.dispatch('fetchHomeMultidataAction')

function decrement() {
  store.dispatch('decrementAction')
}

</script>

2.5.2 module的命名空间

  • 默认情况下,模块内部的action和mutation仍然是注册在全局的命名空间中的:

    • 这样使得多个模块能够对同一个 action 或 mutation 作出响应

    • Getter 同样也默认注册在全局命名空间

  • 如果希望模块具有更高的封装度和复用性,可以添加 namespaced: true 的方式使其成为带命名空间的模块:

    • 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
  • namespaced: true
  • getters["home/xxx"]
  • commit("home/xxx")
<template>
  <div class="home">
    <h2>Home Page</h2>
    <!-- 使用state时,需要state.moduleName.xxx -->
    <h2>counter: {{ $store.state.counter.count }}</h2>
    <!-- 使用getter时,直接getters.xxx -->
    <h2>doubleCounter: {{ $store.getters['counter/doubleCount'] }}</h2>
    <button @click="decrement">count模块-1</button>
  </div>
</template>

<script setup>
import { useStore } from 'vuex'

const store = useStore()
function decrement() {
  store.dispatch('counter/decrementAction')
}

</script>
  • 在命名空间模块中修改根组件
changeRootNameAction({commit, dispatch, state, rootState, getters, rootGetters}) {
  // console.log(state)
  // commit('decrement')
  // 第二个是额外传入的参数
  commit('changeName', '柒柒', { root: true })
  dispatch('changeNameAction', null, { root: true })
}