Vue 系列:Vuex vs Pinia

1,829 阅读5分钟

前言

两年的时间都在写 react, 太久没有接触 vue 的项目了,对于其的生态也变的更加的陌生了。

比如:

  • 以前的 vuex,针对 vue3 出来,有所变动,没有了解过。
  • 新出来的 pinia,更是没有使用过(幸好,还听说过,哈哈)

针对自己的不熟悉东西,去多掌握掌握,总是没有坏处的,万一哪一天,我开始写 vue 了呢?总不至于尴尬吧!

今天清明节,学习 vue 的状态管理库: vuexpinia

下面是自己的学习笔记,有误多多指教

vuex

vuex的官网

采用中文官方的概念解释:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

vuex 开发流程架图

01_vuex1.png

上图就是针对开发人员使用 vuex 的开发流程,还是很明确的。

vuex 的核心概念

vuex2.png

vuex 核心使用要点

安装

pnpm install vuex

定义 store 实例对象,全局注册

// store/index.js
import { createStore } from 'vuex'
const store = createStore({})
​
// main.js
const app = createApp(App)
app.use(store) // 中间件,全局注册store

vuex3.png

对象或者函数形式都可以,因为实现数据共享,就算是同一个内存地址也没有关系

/**
 * =================================
 * 定义
 * =================================
 */
const store = createStore({
  state() {  // 函数形式
    return {
      name: 'copyer'
    }
  }
})
​
​
/**
 * =================================
 * 使用
 * =================================
 */
// 方式一:在 template 模板中使用
<div> {{ $store.state.name }}</div> 
// 方式二:在 <script> 中使用
import { useStore } from 'vuex'
const store = useStore()
console.log(store.state.name)
​
​
/**
 * =================================
 * 辅助函数
 * =================================
 */
// 辅助函数,就是为了防止多次重复书写(这里只针对 options api 的使用,针对 composition api 会在后面封装后使用)
import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState({name: state => state.name})
  }
}
​
/**
 * =================================
 * 模块中的state
 * =================================
 */
// 假设模块名为 home: { name: 'home'}
// template 中使用
<div> {{ $store.state.home.name }}</div> 
​
// 辅助函数
import { mapState } from 'vuex'
export default {
  computed: {
   // 第一个参数为模块名 namespace
    ...mapState('home', {name: state => state.name})
  }
}

注意一下:辅助函数本质上就是一个函数,执行之后返回一个对象( key-value 的形式)。

key: string

value: function

还有种情况就是辅助函数接受一个数组,其内部对数组进行了转化(调用了 normalizeMap 函数),也会转化成一个对象的形式。

这里需要先关注一下,后面的辅助函数封装,理解这里是非常必要的。

vuex4.png

/**
 * =================================
 * 定义与使用
 * 默认参数
 * =================================
 */
const store = createStore({
  ...
  getters: {
    /**
     * 名字变成大写
     * @param state 实例 store 的 state
     * @param getters 实例 store 的 getters
     */
    getNameToUpperCase(state, getters) {
      return state.name.toLocaleUpperCase();
    },
  },
});
​
// 使用
<h1>{{ $store.getters.getNameToUpperCase }}</h1>
​
/**
 * =================================
 * 接受参数
 * =================================
 */
const store = createStore({
  ...
  getters: {
    // 返回一个函数,来接受参数
    getNameJoinStr(state) {
      return (str) => {
        return state.name + '' + str
      }
    }
  },
});
​
<h1>{{ $store.getters.getNameJoinStr('_name') }}</h1>
​
/**
 * =================================
 * 辅助函数: 与 mapState 用法一致
 * =================================
 */

vuex5.png

/**
 * =================================
 * 使用:this.$store / useStore
 * 函数触发,参数传递
 * =================================
 */
const store = createStore({
  ...
  mutations: {
    /**
     * 改变 name
     * @param state 实例 store 的 state
     * @param payload 传递的参数
     */
    changeName(state, payload) {
      state.name = payload.name
    }
  }
});
​
// 使用
// options api
export default {
  methods: {
    btn() {
      // 两种写法
      this.$store.commit('changeName', {name: 'james'})
      this.$store.commit({type: 'changeName', name: 'james'})
      
      // 针对模块中的 changeName 方法调用
      this.$store.commit('home/changeName', {name: 'james'})
    }
  }
}
// composition api
const store = useStore()
store.commit('changeName', {name: 'james'}) // 跟上面一样,也是两种写法
// 都是一样的道理
​
​
/**
 * =================================
 * 辅助函数
 * =================================
 */
import { mapMutations} from 'vuex'
export default {
  methods: {
    ...mapMutations({
      change: 'changeName', // 映射方法,会直接映射到 this.$store.commit('changeName')
    })
    // 针对模块也是一样的
    ...mapMutations('home', {
      changeHome: 'changeName'
    })
  }
}

vuex6.png

/**
 * =================================
 * 参数解析
 * =================================
 */
const store = createStore({
  ...
  actions: {
    /**
     * 异步操作,触发mutations,修改state
     * @param context {commit, dispatch, getters, rootGetters, rootState, state}
     * @param payload 携带的参数
     */
    asyncChangeName(context, payload) {
      setTimeout(() => {
        context.commit("changeName", payload);
      }, 2000);
    },
  },
});
​
/**
 * =================================
 * 使用:this.$store / useStore
 * 函数触发,参数传递
 * =================================
 */
// 使用
// options api
export default {
  methods: {
    btn() {
      // 两种写法
      this.$store.dispatch('asyncChangeName', {name: 'james'})
      this.$store.dispatch({type: 'asyncChangeName', name: 'james'})
      
      // 针对模块中的 changeName 方法调用
      this.$store.dispatch('home/asyncChangeName', {name: 'james'})
    }
  }
}
// composition api
const store = useStore()
store.dispatch('asyncChangeName', {name: 'james'}) // 跟上面一样,也是两种写法/**
 * =================================
 * 辅助函数:跟 mapMutations 的使用基本一致
 * =================================
 */

vuex7.png

没有 namespaced 的模块合并

// 针对 state 的合并(以 home 模块为例)
state() {
  return {
    // 这里的合并还是存在层级关系的
    name: 'copyer',
    home: {
      name: 'home'
    }
  }
}
​
// 针对 getters, mutations, actions 合并,就是直接采用类似**解构**的方式,但是不是解构哈
getters: {
  getNameToUpperCase() {},
  ...home.getters
}
mutations: {
  changeName(state, payload) {},
  ...home.mutations
}
actions: {
  asyncChangeName() {},
  ...home.actions
}

那么就会存在一种情况,如果函数名存在相同的话,就会执行多个相同函数名的函数(不是解构,因为解构的话,遇到相同的函数名就是覆盖操作了,但是并没有进行覆盖)

添加了 namespaced: true,就会进行模块名的拼接

就拿 mutations 中的一个来解释吧,getters / actions 类似。

// 伪代码
mutations: {
  changeName(state, payload) {},
  // 假设 home 模块中也存在 changeName 函数
  // 当合并到 root 上时,就会进行拼接 '模块名/函数名'
  'home/changeName': function(state, payload){}
}

这时,也许你就知道了,在触发模块中的 mutations 和 actions 时,是以这样的形式:

this.$store.commit('home/changeName', {name: 'james'})
this.$store.dispatch('home/asyncChangeName', {name: 'james'})

好奇内部是怎么实现的吗?

module 的源码阅读

针对 modules 属性中的模块,是都需要进行安装的,也就是注册到 root 模块上

function getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    // 根据 module.namespaced 拼接路径
    // namespaced 为 true,返回拼接的字符串
    // 为 false,就为 ''
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}
​
export function installModule (store, rootState, path, module, hot) {
  // 是否是根节点
  const isRoot = !path.length
  // 根据 namespaced 获取注册的 路径拼接字符串
  const namespace = store._modules.getNamespace(path)
​
  // 设置嵌套的 state
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 拿到 path 的最后一个字符串
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      // 把 state 挂载到 父模块上
      parentState[moduleName] = module.state
    })
  }
​
  const local = module.context = makeLocalContext(store, namespace, path)
​
  // 循环注册 mutations 
  module.forEachMutation((mutation, key) => {
    // namespacedType: 注册的字符串
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
}

vuex 的辅助函数封装

在封装之前,我们要充分的了解到,辅助函数是一个函数,返回的是一个对象,对象的形式: {key: string; value: function}

理解版

  • 利用 computed 的本质,接受一个函数
  • 理解内部调用的机制(mapState 内部使用 this.$store.state 获取值,但是setup中不存在this,所以通过bind 改变 this,使内部的 this 指向我们传递的对象)
imoprt { computed } from 'vue'
import { mapState, useStore} from 'vuex'export default useState(mapper) {
    // 拿到 store 对象
    const store = useStore()
    
    // 使用 mapState 对传入的参数进行解析(数组对象都可)
    // 返回值是一个对象(上面的 mapState 的结构解析)
    const storeStateFun = mapState(mapper)
    
    //定义一个对象
    cosnt storeState = {}
    
    //遍历对象,拿到key值,然后遍历key, 根据key,调用属性值函数
    Object.keys(storeStateFun).forEach(funKey => {
        //因为这是mapState解析后的函数,
        //而mapState内部实现是通过,this.$store.state来获取
        //然而setup中,又没有this,所以需要通过bind来改变this指向
        //$route: store的实例对象
        const fn = storeStateFun[funKey].bind({$store: store})
        //composition API中computed需要传递一个函数
        //把return的值,转化为一个ref
        //然后对对象保存起来: 属性名: key, 属性值: ref对象 
        storeState[funKey] = computed(fn) 
    })
    
    return storeState
}

完整版

  • 封装一致性(useState、useGetters、useMutations、useActions)
  • 直接模块化传递
import { useStore, createNamespacedHelpers } from 'vuex'
import { computed } from 'vue'// 封装一个工具函数
const commonFn = function(mapper, mapFn) {
    const store = useStore()
    const storeStateObj = mapFn(mapper)
    const res = {}
    Object.keys(storeStateObj).forEach(item => {
        const fn = storeStateObj[item].bind({$store, store})
        res[item] = computed(fn)
    })
    return res
}
​
// useState
const useState = (moduleName, mapper) => {
  // moduleName: 存在使用的模块 mapState,不存在就是 root
    let mapperFn = mapState
    if(typeof moduleName === 'string' && moduleName.length > 0) {
        //防止出现useState('', [])发生了
        mapperFn = createNamespacedHelpers(moduleName).mapState
    }
    return commonFn(mapper, mapperFn)
}
​
// useGetters
const useGetters = (moduleName, mapper) => {
    // moduleName: 存在使用的模块 mapGetters,不存在就是 root
    let mapperFn = mapGetters
    if(typeof moduleName === 'string' && moduleName.length > 0) {
        //防止出现useState('', [])发生了
        mapperFn = createNamespacedHelpers(moduleName).mapGetters
    }
    return commonFn(mapper, mapperFn)
}
​
// useMutations 和 useActions  等同

pinia

pinia的官网

pinia 起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。

pinia 也被称为菠萝,是因为与西班牙语的菠萝的词很相似,由名得来。

pinia 的起源

pinia 诞生的原因: vuex 团队在 2019 年开发 pinia 就是为了探索 vuex 的下一次迭代会是什么,结果写着写着,最后发现 pinia 已经具备了 vuex5 (vuex 的下一次迭代) 中的大部分内容,所以决定使用 pinia 代替 vuex5 的出现。

来源于 coderwhy 老师的讲述,真假与否,我也不知道了~~~

pinia 的核心概念

pinia1.png

其实理念跟 vuex 是一样的,只是去掉了一些概念(mutations 、modules)

pinia 的两个核心 api

pinia2.png

// store/index.js
import { createPinia } from "pinia";
// 创建一个 pinia 实例
const pinia = createPinia();
export default pinia;
​
// main.js
app = createApp(App);
app.use(pinia);
app.mount(box);
import { defineStore } from 'pinia'
// 创建一个 store 实例
const useUserStore = defineStore('user', {
})

pinia 核心使用要点

pinia3.png

/**
 * =================================
 * 定义和使用
 * =================================
 */
import { defineStore } from 'pinia'
// 创建一个 store 实例
export const useUserStore = defineStore('user', {
  state() {
    return {
      name: 'copyer'
    }
  }
})
​
// App.vue
import { useUserStore } from '@/store/useUserStore'
const userStore = useUserStore()
​
<div>{{ userStore.name }}</div>
​
​
/**
 * =================================
 * 响应式
 * =================================
 */
// 这里的 name 不是响应式
const { name } = useUserStore()
​
// 两种方式
// 方式一:利用 vue 提供 api
import { toRefs } from 'vue'
const { name } = toRefs(useUserStore())
​
// 方式二:利用 pinia 提供 api
import { storeToRefs } from 'pinia'
const { name } = storeToRefs(useUserStore())
​
/**
 * =================================
 * 修改
 * =================================
 */
// 直接修改(类似单个修改,写多次)
const userStore = useUserStore()
function btn() {
  userStore.name = 'james'
}
​
// 直接修改(批量修改)
function btn() {
  userStore.$patch({
    name: 'james'
  })
}
​
// 重置
function btn() {
  userStore.$reset()
}
​
// 响应式对象替换
function btn() {
  userStore.$state({
    name: 'kobe',
    age: 2
  })
}

pinia4.png

/**
 * =================================
 * state 的获取
 * =================================
 */
export const useUserStore = defineStore("user", {
  ...
  getters: {
    // 方式一:getter 的第一个参数
    // 方式二:this 直接拿取 实例 store
    getNameToUpperCase(state) {
      return state.name
    }
  },
});
​
/**
 * =================================
 * getter 之间的相互引用
 * =================================
 */
// 通过 this 拿取store 的实例对象,直接拿取 getter/**
 * =================================
 * 接受参数
 * =================================
 */
export const useUserStore = defineStore("user", {
  ...
  getters: {
    // 返回一个函数,用于接受参数
    getNameToUpperCase(state) {
      return (str) => {
        return state.name + '' + str
      }
    }
  },
});
​
/**
 * =================================
 * 使用其他的 store
 * =================================
 */
getNameToUpperCase(state) {
  const countStore = useCountStore()
  return (str) => {
    return state.name + '' + countStore.count
  }
}

pinia5.png

其实没有什么可以讲的呢,就正常的函数使用即可。

export const useUserStore = defineStore("user", {
  state() {
    return {
      name: "copyer",
    };
  },
  actions: {
    async changeName(name) {
      this.name = name
    }
  }
});

pinia 的其他补充

pinia7.png

辅助函数

pinia 的辅助函数就是为了对 options api 的支持。

  • mapState 和 mapGetters 的使用基本一致,针对属性
  • mapActions 针对函数
  • mapStores 注册多个 store 到组件实例中,绑定在 this 上
import { useUserStore } from '@/store/useUserStore'
import { mapState, mapActions, mapStores, mapGetters } from 'pinia'
export default {
  // mapState 与 mapGetters 使用相同
  computed: {
    // 其他计算属性
    // useUserStore 有一个名为 `name` 的 state 属性 (三种写法)
    ...mapState(useUserStore, {
      name: store => store.name + '' + '_name',
      // 注意如果你想要使用 `this`,那你不能使用箭头函数
      upperName(store) {
        return store.name.toLocaleUpperCase()
      },
      name: 'name'
    }),
    // 注册多个 store 到组件中
    ...mapStores(useUserStore, useCountStore)
  },
  methods: {
    // 其他方法属性
    // useUserStore 有一个 action,分别是 `changeName`。
    ...mapActions(useUserStore, { changeName: 'changeName'})
  },
​
  created() {
    setTimeout(() => {
      this.changeName('james')
    }, 2000)
  }
}

定义 store 的两种方式

// options store
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
  state: () => ({ name: 'copyer' }),
  getters: {
    getNameToUpperCase: (state) => state.name.toLocaleUpperCase()
  },
  actions: {
    changeName(name) {
      this.name = name
    },
  },
})
​
// setup store
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
  const name = ref('copyer')
  const getNameToUpperCase = computed(() => {
    return state.name.toLocaleUpperCase()
  })
  function changeName(name) {
    name.value = name
  }
  return {name, getNameToUpperCase, changeName}
})

pinia 对比 vuex 的优势

  1. 最大的一个特点就是: vuex 是层级化,pinia 是扁平化。而扁平化的数据更加利用操作。

pinia8.png

  1. 更加少的 api, 更加优雅的写法,也提供了多种写法
// 更加少的 api
vuex 需要通过 commit dispatch 触发函数, pinia 直接触发函数
去掉了 mutations 和 modules 概念,更加好理解
​
// 更加优雅的写法: 不在需要深层次 .state  .getters 了
vuex 的写法
<div> {{ $store.state.name }}</div>
<div> {{ $store.state.age }}</div>
<div> {{ $store.getters.getNameToUpperCase }}</div>
​
pinia 的写法
<div> {{ userStore.name }}</div>
<div> {{ userStore.age }}</div>
<div> {{ userStore.getNameToUpperCase }}</div>// 多种写法
options store
setup store
  1. 对 typescript 更加的友好,提示也更加的智能。
  2. 去掉了 mutations,也能使用 devtools 对数据进行监控。

结语

总结完成了,存在有误的地方,请多多指教~~~