• 技术文章 >web前端 >Vue.js

    聊聊Vuex与Pinia在设计与实现上的区别

    青灯夜游青灯夜游2022-12-07 18:24:37转载206

    大前端成长进阶课程:进入学习

    在进行前端项目开发时,状态管理始终是一个绕不开的话题,Vue 与 React 框架本身提供了一部分能力去解决这个问题。但是在开发大型应用时往往有其他考虑,比如需要更规范更完善的操作日志、集成在开发者工具中的时间旅行能力、服务端渲染等。本文以 Vue 框架为例,介绍 Vuex 与 Pinia 这两种状态管理工具在设计与实现上的区别。

    Vue 状态管理


    首先,先介绍一下 Vue 框架自身提供的状态管理的方式。【相关推荐:vuejs视频教程web前端开发

    Vue 组件内主要涉及到状态、动作和视图三个组成部分。

    在选项式 API 中通过 data 方法返回一个状态对象,通过 methods 方法设置修改状态的动作。

    如果使用组合式 API + setup 语法糖,则是通过 reactive 方法生成状态,而动作只需要当做普通函数或者箭头函数进行定义即可。

    选项式 API:

    <script>
    export default {
      data() {  // 状态 state
        return {
          count: 0
        }
      },
      methods() { // 动作 action
        increment() {
          this.count++
        }
      }
    }
    </script>
    // 视图 view
    <template> {{ count }} </template>

    组合式 API + setup 语法糖:

    <script setup>
    import { reactive } from 'Vue'
    // 状态 state
    const state = reactive({
      count: 0
    })
    // 动作 action
    const increment = () => {
      state.count++
    }
    </script>
    // 视图 view
    <template> {{ state.count }} </template>

    image.png

    视图由状态生成,操作可以修改状态。

    如果可以将页面的某一部分单独抽离成与外界解耦的状态、视图、动作组成的独立个体,那么 Vue 提供的组件内的状态管理方式已经足够了。

    但是开发中经常会遇到这两种情况:

    比如我们要做一个主题定制功能,需要在项目入口处获取接口中的颜色参数,然后在整个项目的很多页面都要使用到这个数据。

    一种方法是使用 CSS 变量,在页面的最顶层的 root 元素上定义一些 CSS 变量,在 Sass 中使用 var() 初始化一个 Sass 变量,所有页面都引用这个变量即可。在项目入口处获取接口数据,需要手动去修改 root 元素上的 css 变量。

    在 Vue 中,框架提供了一种 v-bind 的方式去编写 css,我们可以考虑将所有颜色配置存放在一个统一的 store 里面。

    遇到这两种情况,通常我们会通过组件间通信的方式解决,比如:

    1、如果是相邻的父子组件之间通信,可以通过 props+emit 的方式,父组件通过子组件的 props 传入数据,在子组件内部通过 emit 方法触发父组件的一些方法。

    image.png

    2、如果不是直接相邻,而是中间相隔很多层的嵌套关系,那么可以使用 provide+inject 的方式,高层级的组件抛出状态和动作,低层级的组件接收使用数据和触发动作。

    image.png

    如果目标的两个组件并不在同一条组件链上,一种可能的解决方法是「状态提升」。

    可以把共同的状态存储在二者的最小公共祖先组件上,然后再通过上述两种方式进行通信。

    后者编写代码更简洁,更不容易出错。

    这样已经能够解决大多数场景的问题了,那么在框架之外的状态管理工具,到底能提供哪些与众不同的能力?

    Vuex 与 Pinia 核心思想与用法


    Flux 架构

    Flux 是 Facebook 在构建大型 Web 应用程序时为了解决数据一致性问题而设计出的一种架构,它是一种描述状态管理的设计模式。绝大多数前端领域的状态管理工具都遵循这种架构,或者以它为参考原型。

    Flux 架构主要有四个组成部分:

    image.png

    整个数据流动关系为:

    1、view 视图中的交互行为会创建 action,交由 dispatcher 调度器。

    2、dispatcher 接收到 action 后会分发至对应的 store。

    3、store 接收到 action 后做出响应动作,并触发 change 事件,通知与其关联的 view 重新渲染内容。

    这就是 Flux 架构最核心的特点:单向数据流

    与传统的 MVC 架构相比,单向数据流也带来了一个好处:可预测性

    所有对于状态的修改都需要经过 dispatcher 派发的 action 来触发的,每一个 action 都是一个单独的数据对象实体,可序列化,操作记录可追踪,更易于调试。

    Vuex 与 Pinia 大体上沿用 Flux 的思想,并针对 Vue 框架单独进行了一些设计上的优化。

    Vuex

    image.png

    Vuex 中创建 store

    import { createStore } from 'Vuex'
    export default createStore({
      state: () => {
        return { count: 0 }
      },
      mutations: {
        increment(state, num = 1) {
          state.count += num;
        }
      },
      getters: {
        double(state) {
          return state.count * 2;
        }
      },
      actions: {
        plus(context) {
          context.commit('increment');
        },
        plusAsync(context) {
          setTimeout(() => { context.commit('increment', 2); }, 2000)
        }
      }
    })

    与 Vue 选项式 API 的写法类似,我们可以直接定义 store 中的 state、mutations、getters、actions。

    其中 mutations、getters 中定义的方法的第一个参数是 state,在 mutation 中可以直接对 state 同步地进行修改,也可以在调用时传入额外的参数。

    actions 中定义的方法第一个参数是 context,它与 store 具有相同的方法,比如 commit、dispatch 等等。

    Vuex 在组件内使用

    通过 state、getters 获取数据,通过 commit、dispatch 方法触发操作。

    <script setup>
    import { useStore as useVuexStore } from 'Vuex';
    const vuex = useVuexStore();
    </script>
    
    <template>
      <div>
        <div> count: {{ vuex.state.count }} </div>
    
        <button @click="() => {
          vuex.dispatch('plus')
        }">点击这里加1</button>
    
        <button @click="() => {
          vuex.dispatch('plusAsync')
        }">异步2s后增加2</button>
    
        <div> double: {{ vuex.getters.double }}</div>
      </div>
    </template>

    Pinia

    保留:

    舍弃:

    Pinia 创建 store

    import { defineStore } from 'Pinia'
    export const useStore = defineStore('main', {
      state: () => {
        return {
          count: 0
        }
      },
      getters: {
        double: (state) => {
          return state.count * 2;
        }
      },
      actions: {
        increment() {
          this.count++;
        },
        asyncIncrement(num = 1) {
          setTimeout(() => {
            this.count += num;
          }, 2000);
        }
      }
    })

    Pinia 组件内使用

    可直接读写 state,直接调用 action 方法。

    <script setup>
    import { useStore as usePiniaStore } from '../setup/Pinia';
    const Pinia = usePiniaStore();
    </script>
    
    <template>
      <div>
        <div> count: {{ Pinia.count }}</div>
        <button @click="() => {
           Pinia.count++;
        }">直接修改 count</button>
    
        <button @click="() => {
          Pinia.increment();
        }">调用 action</button>
    
        <button @click="() => {
          Pinia.asyncIncrement();
        }">调用异步 action</button>
        <div> double: {{ Pinia.double }}</div>
      </div>
    </template>

    1、对 state 中每一个数据进行修改,都会触发对应的 mutation。

    2、使用 action 对 state 进行修改与在 Pinia 外部直接修改 state 的效果相同的,但是会缺少对 action 行为的记录,如果在多个不同页面大量进行这样的操作,那么项目的可维护性就会很差,调试起来也很麻烦。

    Pinia 更加灵活,它把这种选择权交给开发者,如果你重视可维护性与调试更方便,那就老老实实编写 action 进行调用。

    如果只是想简单的实现响应式的统一入口,那么也可以直接修改状态,这种情况下只会生成 mutation 的记录。

    Pinia action

    Pinia 中的 action 提供了订阅功能,可以通过 store.$onAction() 方法来设置某一个 action 方法的调用前、调用后、出错时的钩子函数。

    Pinia.$onAction(({
      name, // action 名称
      store,
      args, // action 参数
      after,
      onError
    }) => {
      // action 调用前钩子
    
      after((result) => {
        // action 调用后钩子
      })
      onError((error) => {
        // 出错时钩子,捕获到 action 内部抛出的 error
      })
    })

    一些实现细节


    Vuex 中的 commit 方法

    commit (_type, _payload, _options) {
    // 格式化输入参数
    // commit 支持 (type, paload),也支持对象风格 ({ type: '', ...})
      const {
        type,
        payload,
        options
      } = unifyObjectStyle(_type, _payload, _options)
    
      const mutation = { type, payload }
      const entry = this._mutations[type]
      this._withCommit(() => {
        entry.forEach(function commitIterator (handler) {
          handler(payload)
        })
      })
      this._subscribers
        .slice()
        .forEach(sub => sub(mutation, this.state))
    }

    在使用 commit 时,可以直接传入参数 type 和 payload,也可以直接传入一个包含 type 以及其他属性的 option 对象。

    Vuex 在 commit 方法内会先对这两种参数进行格式化。

    Vuex 中的 dispatch 方法

    dispatch (_type, _payload) {
      const {
        type,
        payload
      } = unifyObjectStyle(_type, _payload)
    
      const action = { type, payload }
      const entry = this._actions[type]
    // try sub.before 调用前钩子
      try {
        this._actionSubscribers
          .slice()
          .filter(sub => sub.before)
          .forEach(sub => sub.before(action, this.state))
      } catch (e) {
    // ……
      }
    // 调用 action,对于可能存在的异步请求使用 promiseAll 方式调用
      const result = entry.length > 1
        ? Promise.all(entry.map(handler => handler(payload)))
        : entry[0](payload)
    
      return new Promise((resolve, reject) => {
        result.then(res => {
          // …… try sub.after 调用后钩子
          resolve(res)
        }, error => {
          // …… try sub.error 调用出错钩子
          reject(error)
        })
      })
    }

    从这两个方法的实现中也可以看出 mutations、actions 的内部实现方式。

    所有的 mutations 放在同一个对象内部,以名称作为 key,每次 commit 都会获取到对应的值并执行操作。

    actions 操作与 mutations 类似,但是增加了一个辅助的数据 actionSubscribers,用于触发 action 调用前、调用后、出错时的钩子函数。

    辅助函数 mapXXX

    在 Vuex 中,每次操作都要通过 this.$store.dispatch()/commit()

    如果想要批量将 store 中的 state、getters、mutations、actions 等映射到组件内部,可以使用对应的 mapXXX 辅助函数。

    export default {
      computed: {
        ...mapState([]),
        ...mapGetters([])
      },
      methods: {
        ...mapMutations(['increment']), // 将 this.increment 映射到 this.$store.commit('increment')
        ...mapActions({
          add: 'incremnet'  // 传入对象类型,实现重命名的映射关系
        })
      }
    }

    在 Pinia + 组合式 API 下,通过 useStore 获取到 store 后,可以直接读写数据和调用方法,不再需要辅助函数。

    状态管理工具的优势


    最后


    当项目涉及的公共数据较少时,我们可以直接利用 Vue 的响应式 API 来实现一个简单的全局状态管理单例:

    export const createStore = () => {
      const state = reactive({
        count: 0;
      })
      const increment = () => {
        state.count++;
      }
      return {
        increment,
        state: readonly(state)
      }
    }

    为了使代码更容易维护,结构更清晰,通常会将对于状态的修改操作与状态本身放在同一个组件内部。提供方可以抛出一个响应式的 ref 数据以及对其进行操作的方法,接收方通过调用函数对状态进行修改,而非直接操作状态本身。同时,提供方也可以通过 readonly 包裹状态以禁止接收方的直接修改操作。

    (学习视频分享:web前端开发编程基础视频

    以上就是聊聊Vuex与Pinia在设计与实现上的区别的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除

    前端(VUE)零基础到就业课程:点击学习

    清晰的学习路线+老师随时辅导答疑

    自己动手写 PHP MVC 框架:点击学习

    快速了解MVC架构、了解框架底层运行原理

    专题推荐:Vuex Vue.js React.js
    上一篇:详解vue particles.js登录背景实现炫酷的粒子动效! 下一篇:自己动手写 PHP MVC 框架(40节精讲/巨细/新人进阶必看)

    相关文章推荐

    • ❤️‍🔥共22门课程,总价3725元,会员免费学• ❤️‍🔥接口自动化测试不想写代码?• 一文聊聊Vue-Router的实现原理• vue组件中data为啥是函数• (超详细)vue怎么进行路由跳转?三种方式分享• 浅析Vue3动态组件怎么进行异常处理• 聊聊Vue怎么通过JSX动态渲染组件
    1/1

    PHP中文网