一、前言
就像是 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 3 的 Github:https://github.com/vuejs/vue-next
- vue-composition 的 Github:https://github.com/vuejs/composition-api
- vue-composition 的使用文档:https://vue-composition-api-rfc.netlify.com/api.html
vue-composition 提供了类似 React Hook 的能力,将 Vue 的抽象层级从「组件级(Component)」降低为「函数级(Function)」。
用了将近一周的时间,说句实话,我感觉到非常的兴奋!
Vue Composition API 的建议学习路线
如果想学习 vue-composition 的同学,可以点击上面的 vue-composition 的使用文档进行学习,学习路线建议如下:
- 在 vue2.0 项目中安装 composition-api(请查看 composition-api 的Github)
- 学习 composition-api 的 API Reference 文档
- 在 vue2.0 项目中使用 composition-api + typescript 重构(推荐使用 typescript 的原因是,都 2020 年了)
- 学习 composition-api 的 RFC 文档
三、Vuex 是什么?
打开 Vuex 的官方网站,我们可以看到这样一段描述:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到了 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
我们来画一下重点,看看 Vuex 提供了什么能力:
- 集中式存储管理应用的「所有组件」的「状态」
- 保证状态以「可预测」的方式「发生变化」
- 与调试工具集成,提供功能:time-travel、状态快照导入导出
我们仔细的看一下,然后准备去设计一个新的状态管理模式
第一条:集中式存储管理「所有组件」的「状态」
请注意,这里它并不是要去管理「所有组件」的「所有状态」———— 也就是说我们每个组件中还是可以有自己的「私有状态」的。
这很好理解:
比如在「登录注册页面」中我们的「短信验证码计时器」的状态很明显就是一个「私有状态」。
那我们要解决的就是集中式存储管理这件事情了,我认为该难点在于:
- 需要维护「公共状态」的「单例」性(由于 Chrome 使用的是「标记清除」的垃圾回收策略,而不是「引用计数」的垃圾回收策略,所以我们不用担心在 Vue 的生命周期内变量被 GC 回收的问题)
- 需要维护「公共状态」的「命名」全局可见
- 需要维护「公共状态」全局可「访问」
第二条:保证状态以「可预测」的方式「发生变化」
需要解决的难点就是,状态的变化可以被追溯到:
即:「哪个组件」改变了「什么状态」
第三条:时间旅行与状态快照导入导出
本条的难点在于——是否全局状态就是一个 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 替代了。