从 Vue2 到 Vue3(二):语法
从 Vue2 到 Vue3(二):语法
其实讲了那么多原理,目前还是不知道怎么写 Vue3,由是简单分享一下如何写的问题
起步
这是 Vue2 的写法
1 | import Vue from 'vue' |
Vue3 的组合式 API 使用函数而不是声明选项的方式书写 Vue 组件,这也反应在了创建 Vue 实例上,工具方法需要引入,而不是直接挂载在 Vue 类上,这样做也有利于打包时的 TreeShaking 精确判断不必要的代码片段并将之移除
1 | import { createApp } from 'vue' |
选项式到组合式
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API[1]:
-
响应式 API:例如
ref()
和reactive()
,使我们可以直接创建响应式状态、计算属性和侦听器。 -
生命周期钩子:例如
onMounted()
和onUnmounted()
,使我们可以在组件各个生命周期阶段添加逻辑。 -
依赖注入:例如
provide()
和inject()
,使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。
那么为什么要有组合式 API?—— 1.更好的逻辑复用和2.更灵活的代码组织[2]
第1点,在 Vue2 中使用 mixins 多了会有这样的痛点:组件中使用了一些方法和变量,但是在文件中找不到它们的声明,于是只能寻着 mixins 一个一个找,费时费力不说,还有可能在定义变量或方法时不经意覆盖了本不该覆盖的属性,而且这样让多个 mixins 隐形耦合在一起,不利于多处复用[3]
声明:以下提到的Vue3
的写法均指使用单文件组件及组合式 API时<script setup>
语法下的写法,相比于普通的 <script>
语法,它具有更多优势:[4]
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
那么从现在 Vue 的单文件组件的风格转变为组合式 API吧。
props
Vue2 这么定义组件 props
1 | // 最简方式 |
而 Vue3 使用defineProps
方法定义组件 props
1 | // 最简方式 |
这些定义好的 props 可以直接在模板 template 中使用,Vue 已经做好了解析,但是如果想要在组合式 API 的别处使用,则需要接受withDefaults
或者defineProps
返回的值,如下:
1 | const props = defineProps<{ loading?: boolean }>() |
另外,Vue3 中,父组件传入的 props 严禁子组件修改,即使是对象的深层属性或者数组增减也一样,因此对于这种数据,应该显式使用defineModel
声明为双向绑定的值,defineModel
稍后讨论
data 和 methods
在选项式 API 里,data 需要是一个函数,为了防止多实例数据互相污染,而在组合式 API 里,没有显式的 data 和 methods,顶层的绑定会被暴露给模板,如下:
1 | <script setup> |
要使用响应式,则使用ref()
包裹数据,同时 ref 也支持预先定义数据类型:
1 | const msg = ref('hello') |
如果不预先定义数据类型,则 ref 会推断内部数据类型,这在内部数据属性不完全的时候会有类型校验问题,假设有一个对象类型如下:
1 | type DemoObj = { |
而在定义数据的时候,还不确定 description 的内容,于是有可能这样写:
1 | const demo = ref({ name: 'demo' }) |
于是当想要设置 description 时,IDE 会报错:
此时改为
1 | const demo = ref<DemoObj>({ name: 'demo' }) |
或者使用类型断言(类型断言纯粹是一个编译时语法,同时,它也是一种为编译器提供关于如何分析代码的方法)
1 | const demo = ref({ name: 'demo' } as DemoObj) |
即可消除报错
computed
组合式 API 的computed
是一个方法,需要从 vue 中引入,内部接收一个类似选项式 API 的方法或者对象,如下:
1 | import { ref, computed } from 'vue' |
watch
组合式 API 中的watch
很像原本的$watch
,但是有以下两个主要的区别:
- watch 的第一个参数,不再支持键路径,它应该是一个响应式对象、返回一个值的函数、ref或者这三种类型的值的数组
- watch 的选项在原本的
immediate
和deep
之外,还增加了以下选项:flush
:调整回调函数的刷新机制,这个稍后讨论once
:只执行一次回调函数onTrack
/onTrigger
:调试侦听器的依赖
对于常用的监听路由参数的情况,应该以函数方式传入第一个参数,避免监听整个路由对象:
1 | import { watch } from 'vue' |
Vue3 还提供了一个watchEffect
方法用于自动追踪所有依赖,并在依赖改变时触发副作用函数,watchEffet
接收两个参数,第一个即是副作用函数,第二个是 watch 的选项,不过不包括once
、immediate
、deep
。在使用时,watch
,watchEffect
有三个区别:
- watch 更明确由哪个状态触发的侦听器,而 watchEffect 自动追踪依赖变化触发侦听器
- watch 可以访问侦听状态变化的前后值,watchEffect 则不可以
- watch 是懒执行副作用,而 watchEffect 会立刻执行副作用函数
默认情况下,侦听器将在组件渲染之前执行,在生命周期中,它在onBeforeUpdate之前调用。如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: ‘post’ 选项:[5]
1 | import { watch, watchEffect } from 'vue' |
还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前立即同步触发,而不是等到下一个事件循环才执行:[6]
1 | import { watch, watchEffect } from 'vue' |
同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值,但应避免在可能多次同步修改的数据源 (如数组) 上使用。
在<script setup>
中同步创建的侦听器会自动绑定到宿主组件实例上,实例卸载时侦听器会自动停止。但是如果异步创建了侦听器,则必须通过返回值获取到其停止方法,并合适的时机手动停止它以防止内存泄漏,如:
1 | import { watchEffect } from 'vue' |
emit
为了在声明emits
选项时获得完整的类型推导支持,可以使用defineEmits
API,其使用方法如下:
1 | // 最简方法 |
model
不像 Vue2,v-model
通常只有一个绑定的值,Vue3 支持通过名字指定多个v-model
,在不指定名字时,model 的默认名字是modelValue
,这里为了省事,搬一下 Vue3 文档的示例:
1 | // 声明 "modelValue" prop,由父组件通过 v-model 使用 |
其他
- 对于选项式 API 中的其他选项,可以使用
defineOptions
编译宏来声明,而不必使用单独的<script>
块:
1 | // 可以使用组合式 API 完成定义的选项无法使用这个宏定义 |
- 组合式 API 不再默认暴露组件实例的所有属性和方法,因此无法通过在控制台中使用 Vue DevTools 像之前一样
$vm.xxx
访问变量或方法,父组件也无法直接通过$refs.childRef.someMethod()
调用子组件的方法,如果特别需要暴露实例的变量和方法:
1 | import { ref } from 'vue' |
1 | <!-- 使用 --> |
-
Vue3 官方推荐使用
pinia
而不是vuex
作为状态管理工具 -
生命周期基本类似于 Vue2,只是在 beforeCreate 前增加了 setup 的步骤,所有的生命周期钩子也需要声明引入,如
import { onMounted } from 'vue'
-
在模板渲染上下文中,只有顶级的 ref 属性才会被解包[7]。如果某一个变量本身不是响应式,但是其内部包含了一个响应式的属性,则在模板中使用时,需要手动解包:
<span>{{ obj.refValue.value }}</span>
-
在定义变量或方法之前就使用它们,会引起编译失败。这个问题在以前使用选项式基本不会出现,但是使用了组合式以后很容易在随意排布方法和变量的时候,习惯性将用到的方法后置,从而在之前调用时产生报错
另外,TS 相关的知识,请参考TypeScript 入门教程等文章
IDE
其实关于 IDE 的问题,不该写到这里,但是实际开发中还是会遇到很多莫名其妙地报红问题,所以总结到这里,以 VS Code 为例
- 如果项目使用了 Vue3,那么应该安装
Vue Language Features (Volar)
插件 并在工作区暂时禁用Vetur
- 切换到 Vue 文件,在 VS Code 右下角的状态栏中,正确选择 TypeScript 版本为当前工作区的版本,而不是 VS Code 内置的版本
- 如果确认自己的代码无误,但 VS Code 仍然在报错,则应该考虑以下方案:
- 如果报错在 TS 文件中,在 VS Code 中执行
TypeScript: Reload Project
命令 - 如果报错在 Vue 文件中,在 VS Code 中执行
Volar: Reload Project
命令 - 如果上述操作还没有解决问题,则重启当前 VS Code 窗口
- 如果报错在 TS 文件中,在 VS Code 中执行