Vue 3 带来的 Vuex 的替代方案

一、前言

就像是 React 社区在 HOOK API 出现后很快就使用 useReducer、useContext 代替了 Redux 进行状态管理一样。Vue3 也是时候抛弃 Vuex 进行状态管理了。

在考虑为什么要抛弃 Vuex 之前,我们先来想一下为什么要引入 Vuex?

Vuex 实际上解决的问题是「组件间传递对象」的问题:

在传统的方式里,我们如果要把一个对象从父组件传递到子组件,要使用 prop 进行传递。


如果组件间不是直接的「父子关系」的话(如「爷孙关系」),传递对象的过程要经过整颗组件树————这让我们的代码变得很丑陋,提高了相当多的复杂度。

我们引入 Vuex 就是为了提供一个统一管理组件状态的地方,来让我们的组件之间可以简单的传递对象。

但是引入 Vuex 的本质原因还是:降低代码的复杂度

但是 Vuex 陡峭的学习曲线,令人费解的 Getter、Module、Store、Mutation、Action 等概念,又引入了新的代码复杂度,新的心智负担。

当我真正掌握了它的时候,我并没有惊呼,而是对其产生了深深的排斥。

所以,当现在 Vue3 到来,有了更新、更轻量的依赖注入工具 provide、inject 函数,我们有什么道理不像隔壁的 React 社区学习————用 useReducer、useContext (provide、inject)代替 Redux(Vuex)呢?


二、关于 Vue3 与 Vue Composition API

目前 Vue 3 还处于 Alpha 版本,但是我们已经可以通过使用 @vue-composition 来提前在 Vue2 环境下体验 Vue3 的新特性了。

vue-composition 提供了类似 React Hook 的能力,将 Vue 的抽象层级从「组件级(Component)」降低为「函数级(Function)」。

用了将近一周的时间,说句实话,我感觉到非常的兴奋!

Vue Composition API 的建议学习路线

如果想学习 vue-composition 的同学,可以点击上面的 vue-composition 的使用文档进行学习,学习路线建议如下:

  1. 在 vue2.0 项目中安装 composition-api(请查看 composition-api 的Github)
  2. 学习 composition-api 的 API Reference 文档
  3. 在 vue2.0 项目中使用 composition-api + typescript 重构(推荐使用 typescript 的原因是,都 2020 年了)
  4. 学习 composition-api 的 RFC 文档

三、Vuex 是什么?

打开 Vuex 的官方网站,我们可以看到这样一段描述:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到了 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

我们来画一下重点,看看 Vuex 提供了什么能力:

  1. 集中式存储管理应用的「所有组件」的「状态」
  2. 保证状态以「可预测」的方式「发生变化」
  3. 与调试工具集成,提供功能:time-travel、状态快照导入导出

我们仔细的看一下,然后准备去设计一个新的状态管理模式


第一条:集中式存储管理「所有组件」的「状态」

请注意,这里它并不是要去管理「所有组件」的「所有状态」———— 也就是说我们每个组件中还是可以有自己的「私有状态」的。

这很好理解:
比如在「登录注册页面」中我们的「短信验证码计时器」的状态很明显就是一个「私有状态」。

那我们要解决的就是集中式存储管理这件事情了,我认为该难点在于:

  1. 需要维护「公共状态」的「单例」性(由于 Chrome 使用的是「标记清除」的垃圾回收策略,而不是「引用计数」的垃圾回收策略,所以我们不用担心在 Vue 的生命周期内变量被 GC 回收的问题)
  2. 需要维护「公共状态」的「命名」全局可见
  3. 需要维护「公共状态」全局可「访问」

第二条:保证状态以「可预测」的方式「发生变化」

需要解决的难点就是,状态的变化可以被追溯到:

即:「哪个组件」改变了「什么状态」

第三条:时间旅行与状态快照导入导出

本条的难点在于——是否全局状态就是一个 Vue APP 的快照?

以及是否有一个工具配合你做调试。


四、provide、inject 是什么?

provide、inject 是 vue-composition-api 的一个新功能:依赖注入功能

import { provide, inject } from 'vue'

const ThemeSymbol = Symbol()

const Ancestor = {
  setup() {
    provide(ThemeSymbol, 'dark')
  }
}

const Descendent = {
  setup() {
    const theme = inject(ThemeSymbol, 'light' /* optional default value */)
    return {
      theme
    }
  }
}

这是怎么注入的呢?我们还是看图来说话:

我们都知道 Vue 是一颗「组件树」,我们只要保证是「父节点」 provide,那么它的「子节点」就一定可以通过 inject 获取到。

举例:

  • A provide,B 可以 inject,C 可以 inject,D 可以 inject
  • B provide,D 可以 inject
  • D provide,没有其它节点可以 inject
  • C provide,没有其它节点可以 inject

五、我们结合一下 Vuex 的特点和 provide、inject 的特性来看,新的状态管理应该具有哪些特点

5.1 声明一次,全局可访问

为了实现这样子的特点,我们就需要将「需要共享的状态」事先在我们 Vue 的根节点 App.vue 中通过 provide 声明好了。

而「单例」的需求也在这里得到解决——我们的状态不会被创建多次。

5.2 全局可访问「公共状态」的「命名」

全局可访问即全局可导入,我们仅需要把「公共状态」的「命名」放在一个单一的文件中即可:

// src/store/store.ts
const temporaryPlanList = Symbol()
const dailyPlanList = Symbol()
export default {
  temporaryPlanList,
  dailyPlanList
}

然后再在需要访问的地方导入:比如 App.vue 中 provide

// src/App.vue
<script lang="ts">
import Store from "./store/store"

import { defineComponent, provide, ref } from "@vue/composition-api"
export default defineComponent({
  setup() {
    provide(Store.temporaryPlanList, ref([]))
    provide(Store.dailyPlanList, ref([]))
  }
})
</script>

比如 Plan.vue 中 inject(Plan.vue 是 App.vue 的子节点)

// src/views/Plan.vue
<script lang="ts">
import Store from "./store/store"

import { defineComponent, provide, ref } from "@vue/composition-api"
export default defineComponent({
  setup() {
    const temporaryPlanList = inject(Store.temporaryPlanList)
    const dailyPlanList = inject(Store.dailyPlanList)
  }
})
</script>

5.3 保证「状态」以可预测的方式发生变化

其实.....就是要多设置一个 setter 而已。

就像是 Vuex 中 Store 中存储的状态要靠 mutation 提交才可以更改。

但是这真的有用吗?不是脱了裤子放屁吗?

反正我认为是多此一举,还不如就是直接修改全局变量的状态。

5.4 时间旅行与应用快照

这点需要调试工具配合,目前来看是无法用 inject、provide 替代了。

六、参考文献

https://blog.logrocket.com/use-hooks-and-context-not-react-and-redux/blog.logrocket.com

发布于 03-21

文章被以下专栏收录