Vue3入门
全新的 Vue 版本
在 2022 年 2 月 7 日, Vue 3 代替了 Vue 2 成为 Vue 的默认版本,也就是运行 npm i vue
默认会安装 Vue 3 了,无需再和以前一样,需要指定 vue@next
才可以安装到 Vue 3 。包括 vue-router
、 vuex
、vue-loader
和 @vue/test-utils
等相关的生态,同样不需要指定 next 版本了,都配合 Vue 3 指定了新的 latest 默认版本。
官方文档
只罗列 Vue 3.0 相关的官网,有中文版的优先都放中文版。
名称 | 官网文档 |
---|---|
Vue 3 | 点击访问 |
Vue Composition API | 点击访问 |
Vue Router | 点击访问 |
Vuex | 点击访问 |
Pinia | 点击访问 |
Vue CLI | 点击访问 |
Vite | 点击访问 |
使用 Vue 2
如果还需要使用 Vue 2 ,则在安装的时候需要手动指定 Tag 为 legacy
或者 v2-latest
才能安装到 Vue 2 :
1 | # 安装 2.6.x 的最新版本 |
注意到 Vue 2 配对了两个不同的 Tag ,分别对应 2.7 系列和 2.6 系列。
Vue 2.7 系列是在 Vue 2 的基础上,对标 Vue 3 的功能支持所作的升级,主要是面向想使用 Vue 3 的新特性、但顾虑于产品对旧浏览器的支持而无法贸然升级的开发者。
Vue 2.7 与 Vue 2.6 之前的旧版本在使用上略有不同,具体可以查看 Vue 2 的 更新记录 了解具体的差异化。
使用 Vite 创建项目
Vite 是由 Vue 作者尤雨溪先生带领团队开发的一个构建工具,它利用浏览器原生支持 ES 模块的特点,极大提升了开发体验,自 2021 年 1 月份发布 2.0 版本以来,发展非常快,整体情况非常稳定,前景非常乐观。
Create Vite
create-vite 是 Vite 官方推荐的一个脚手架工具,可以创建基于 Vite 的不同技术栈基础模板。
运行以下命令创建模板项目,再按照命令行的提示操作(选择 vue
技术栈进入),即可创建一个基于 Vite 的基础空项目。
1 | npm create vite |
不过这个方式创建的项目非常基础,如果需要用到 Router 、 Vuex 、 ESLint 等程序,都需要再单独安装和配置,所以推荐使用 Create Preset 。
Create Vue
create-vue 是 Vue 官方推出的一个新脚手架,用以代替基于 Webpack 的 Vue CLI ,它可以创建基于 Vite 的 Vue 基础模板。
运行以下命令创建模板项目,然后根据命令行的提示操作即可。
1 | npm init vue@3 |
Create Preset
create-preset 是 Awesome Starter 的 CLI 脚手架,提供快速创建预设项目的能力,可以创建一些有趣实用的项目启动模板,也可以用来管理的常用项目配置。
简单使用: 可以通过包管理器直接创建配置,然后按照命令行的提示操作,即可创建开箱即用的模板项目。
- 在这里选择
vue
技术栈进入,选择 vue3-ts-vite 创建一个基于 Vite + Vue 3 + TypeScript 的项目启动模板。
1
npm create preset
- 在这里选择
全局安装
也可以像使用
@vue/cli
一样,全局安装到本地,通过preset init
命令来创建项目。推荐全局安装它,用起来更方便,请先全局安装:1
2
3
4
5npm install -g create-preset
#可以通过下面这个命令来检查安装是否成功,如果成功,将会得到一个版本号:
preset -v
0.13.1然后可以通过
--template
选项直接指定一个模板创建项目,在这里使用vue3-ts-vite
模板创建一个名为hello-vue3
的项目:1
preset init hello-vue3 --template vue3-ts-vite
常用的项目模板也可以绑定为本地配置,点击 Create Preset 官方文档 查看完整使用教程。
管理项目配置
不论使用上方哪种方式创建项目,在项目的根目录下都会有一个名为 vite.config.js
或 vite.config.ts
的项目配置文件(其扩展名由项目使用 JavaScript 还是 TypeScript 决定)。
里面会有一些预设好的配置,可以在 Vite 官网的配置文档 查阅更多的可配置选项。
使用 @vue/cli 创建项目
如果不习惯 Vite ,依然可以使用 Vue CLI 作为开发脚手架,Vue CLI 使用的构建工具是基于 Webpack 。
更新 CLI 脚手架
请先全局安装,把脚手架更新到最新版本(最低版本要求在 4.5.6
以上才能支持 Vue 3 项目的创建)。
1 | npm install -g @vue/cli |
使用 CLI 创建 3.x 项目
Vue CLI 全局安装后,可以在命令行输入 vue
进行操作,创建项目使用的是 create
命令:
1 | vue create hello-vue3 |
由于要使用 TypeScript ,所以需要选择最后一个选项来进行自定义搭配,通过键盘的上下箭头进行切换选择:
1 | Vue CLI v5.0.4 |
多选菜单可以按空格选中需要的依赖,总共选择了下面这些选项:
1 | Vue CLI v5.0.4 |
选择 Vue 版本,要用 Vue 3 所以需要选择 3.x :
1 | ? Choose a version of Vue.js that you want to start the project with |
是否选择 Class 语法的模板,在 Vue 2 版本为了更好的支持 TypeScript ,通常需要使用 Class 语法,由于 Vue 3 有了对 TypeScript 支持度更高的 Composition API ,因此选择 n
,也就是 “否” :
1 | ? Use class-style component syntax? (y/N) n |
Babel 可以把新版本的 JavaScript 语句转换为兼容性更好的低版本 Polyfill 写法,所以选 y
确认使用:
1 | ? Use Babel alongside TypeScript |
接下来是选择路由模式,选 y
启用 History 模式,选 n
使用 Hash 模式,可根据项目情况选择。建议先选 y
确认。
1 | ? Use history mode for router? |
选择一个 CSS 预处理器,可以根据自己的喜好选择,不过鉴于目前开源社区组件常用的都是 Less ,所以也建议选择 Less 作为入门的预处理器工具。
1 | ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported |
Lint 规则,用来代码检查,写 TypeScript 离不开 Lint ,可以根据自己喜好选择,也可以先选择默认,这里先默认第一个:
1 | ? Pick a linter / formatter config: (Use arrow keys) |
Lint 的校验时机,一个是在保存时校验,一个是在提交 commit 的时候才校验,这里也选默认:
1 | ? Pick additional lint features: (Press <space> to select, |
项目配置文件,笔者更习惯保存为独立文件:
1 | ? Where do you prefer placing config for Babel, ESLint, etc.? |
是否保存为未来的项目配置,存起来方便以后快速创建:
1 | ? Save this as a preset for future projects? Yes |
至此,项目创建完成!可以通过 npm run serve
开启热更进行开发调试,通过 npm run build
构建打包上线。
管理项目配置
Vue CLI 的配置文件是 vue.config.js
,可以参考官网的说明文档调整各个选项配置:配置参考 - Vue CLI 。
Hello Vue3
如果想早点开始 Vue 3 的世界,可以通过以下命令直接创建一个启动项目:
1 | # 使用 `vue3-ts-vite` 模板创建一个名为 `hello-vue3` 的项目 |
至此,通过脚手架已经搭好了一个可直接运行的基础项目,已经可以正常的 npm run dev
和 npm run build
了,项目配置和编辑器也都弄好了,是不是可以开始写代码了呢?
请不要着急,还需要了解一点东西,就是如何初始化一个 Vue 3 项目。
因为在实际开发过程中,还会用到各种 npm 包,像很多 UI 框架、功能插件的引入都是需要在 Vue 初始化阶段处理。
甚至有时候还要脱离脚手架,采用 CDN 引入的方式来开发,所以开始写组件之前,还需要了解一下在 Vue 3 项目中,初始化阶段对比 Vue 2 的一些变化。
回顾 Vue 2-vue-2
Vue 2 在导入各种依赖之后,通过 new Vue()
执行 Vue 的初始化,相关的 Vue 生态和插件,有的是使用 Vue.use()
来进行初始化,有的是作为 new Vue()
的入参:
1 | import Vue from 'vue' |
入口文件
项目的初始化都是在入口文件集中处理,Vue 3 的目录结构对比 Vue 2 没有变化,入口文件依然还是 main.ts 这个文件。但是 Vue 3 在初始化的时候,做了不少的调整,代码写法和 Vue 2 完全不同。因为统一了相关生态的启用方式,不再像 Vue 2 时期那样多方式共存,显得比较杂乱。
在 Vue 3 ,使用 createApp
执行 Vue 的初始化,另外不管是 Vue 生态里的东西,还是外部插件、 UI 框架,统一都是由 use()
进行激活,非常统一和简洁:
1 | import { createApp } from 'vue' |
全新的 setup 函数
在开始编写 Vue 组件之前,需要了解两个全新的前置知识点:
- 全新的
setup
函数,关系到组件的生命周期和渲染等问题 - 写 TypeScript 组件离不开的
defineComponent
API
setup 的含义
Vue 3 的 Composition API 系列里,推出了一个全新的 setup
函数,它是一个组件选项,在创建组件之前执行,一旦 props 被解析,便作为组合式 API 的入口点。
说的通俗一点,就是在使用 Vue 3 生命周期的情况下,整个组件相关的业务代码,都可以放在
setup
里执行。因为在setup
之后,其他的生命周期才会被启用。
基本语法:
1 | // 这是一个基于 TypeScript 的 Vue 组件 |
可以发现在这段代码里还导入了一个 defineComponent
API ,也是 Vue 3 带来的新功能,下文的 defineComponent 的作用将介绍其用法。
在使用 setup
的情况下,请牢记一点:不能再用 this
来获取 Vue 实例,也就是无法和 Vue 2 一样,通过 this.foo
、 this.bar()
这样来获取实例上的数据,或者执行实例上的方法。
setup 的参数使用
setup
函数包含了两个入参:
参数 | 类型 | 含义 | 是否必传 |
---|---|---|---|
props | object | 由父组件传递下来的数据 | 否 |
context | object | 组件的执行上下文 | 否 |
- 第一个参数
props
:- 它是响应式的,当父组件传入新的数据时,它将被更新。
请不要解构它,这样会让数据失去响应性,一旦父组件发生数据变化,解构后的变量将无法同步更新为最新的值。
可以使用 Vue 3 全新的响应式 API
toRef / toRefs
进行响应式数据转换,下文将会介绍全新的响应式 API 的用法。
第二个参数
context
:context
只是一个普通的对象,它暴露三个组件的 Property :属性 类型 作用 attrs 非响应式对象 未在 Props 里定义的属性都将变成 Attrs slots 非响应式对象 组件插槽,用于接收父组件传递进来的模板内容 emit 方法 触发父组件绑定下来的事件
因为
context
只是一个普通对象,所以可以直接使用 ES6 解构。- 平时使用可以通过直接传入
{ emit }
,即可用emit('xxx')
来代替使用context.emit('xxx')
,另外两个功能也是如此。 - 但是
attrs
和slots
请保持attrs.xxx
、slots.xxx
的方式来使用其数据,不要进行解构,虽然这两个属性不是响应式对象,但对应的数据会随组件本身的更新而更新。
- 平时使用可以通过直接传入
defineComponent 的作用
defineComponent
是 Vue 3 推出的一个全新 API ,可用于对 TypeScript 代码的类型推导,帮助开发者简化掉很多编码过程中的类型声明。
比如,原本需要这样才可以使用 setup
函数:
1 | import { Slots } from 'vue' |
每个组件都这样进行类型声明,会非常繁琐,如果使用了 defineComponent
,就可以省略这些类型声明:
1 | import { defineComponent } from 'vue' |
代码量瞬间大幅度减少,只要是 Vue 本身的 API , defineComponent
都可以自动推导其类型,这样开发者在编写组件的过程中,只需要维护自己定义的数据类型就可以了,可专注于业务。
组件的生命周期
在了解了 Vue 3 组件的两个前置知识点后,不着急写组件,还需要先了解组件的生命周期,这个知识点非常重要,只有理解并记住组件的生命周期,才能够灵活地把控好每一处代码的执行,使程序的运行结果可以达到预期。
升级变化
从 Vue 2 升级到 Vue 3 ,在保留对 Vue 2 的生命周期支持的同时,Vue 3 也带来了一定的调整。
Vue 2 的生命周期写法名称是 Options API (选项式 API ), Vue 3 新的生命周期写法名称是 Composition API (组合式 API )。
Vue 3 组件默认支持 Options API ,而 Vue 2 可以通过 @vue/composition-api 插件获得 Composition API 的功能支持(其中 Vue 2.7 版本内置了该插件, 2.6 及以下的版本需要单独安装)。
为了减少理解成本,笔者将从读者的使用习惯上,使用 “ Vue 2 的生命周期” 代指 Options API 写法,用 “ Vue 3 的生命周期” 代指 Composition API 写法。
关于 Vue 生命周期的变化,可以从下表直观地了解:
Vue 2 生命周期 | Vue 3 生命周期 | 执行时间说明 |
---|---|---|
beforeCreate | setup | 组件创建前执行 |
created | setup | 组件创建后执行 |
beforeMount | onBeforeMount | 组件挂载到节点上之前执行 |
mounted | onMounted | 组件挂载完成后执行 |
beforeUpdate | onBeforeUpdate | 组件更新之前执行 |
updated | onUpdated | 组件更新完成之后执行 |
beforeDestroy | onBeforeUnmount | 组件卸载之前执行 |
destroyed | onUnmounted | 组件卸载完成后执行 |
errorCaptured | onErrorCaptured | 当捕获一个来自子孙组件的异常时激活钩子函数 |
可以看到 Vue 2 生命周期里的 beforeCreate
和 created
,在 Vue 3 里已被 setup
替代。
熟悉 Vue 2 的开发者应该都知道 Vue 有一个全局组件 <KeepAlive />
,用于在多个组件间动态切换时缓存被移除的组件实例,当组件被包含在 <KeepAlive />
组件里时,会多出两个生命周期钩子函数:
Vue 2 生命周期 | Vue 3 生命周期 | 执行时间说明 |
---|---|---|
activated | onActivated | 被激活时执行 |
deactivated | onDeactivated | 切换组件后,原组件消失前执行 |
虽然 Vue 3 依然支持 Vue 2 的生命周期,但是不建议混搭使用,前期可以继续使用 Vue 2 的生命周期作为过渡阶段慢慢适应,但还是建议尽快熟悉并完全使用 Vue 3 的生命周期编写组件。
使用 3.x 的生命周期
在 Vue 3 的 Composition API 写法里,每个生命周期函数都要先导入才可以使用,并且所有生命周期函数统一放在 setup
里运行。
如果需要达到 Vue 2 的 beforeCreate
和 created
生命周期的执行时机,直接在 setup
里执行函数即可。
以下是几个生命周期的执行顺序对比:
1 | import { defineComponent, onBeforeMount, onMounted } from 'vue' |
最终将按照生命周期的顺序输出:
1 | // 1 |
组件的基本写法
如果想在 Vue 2 里使用 TypeScript 编写组件,需要通过 Options API 的 Vue.extend 语法,或者是另外一种风格 Class Component 的语法声明组件,其中为了更好地进行类型推导, Class Component 语法更受开发者欢迎。
但是 Class Component 语法和默认的组件语法相差较大,带来了一定的学习成本,对于平时编写 JavaScript 代码很少使用 Class 的开发者,适应时间应该也会比较长。
因此 Vue 3 在保留对 Class Component 支持的同时,推出了全新的 Function-based Component ,更贴合 JavaScript 的函数式编程风格,这也是接下来要讲解并贯穿全文使用的 Composition API 新写法。
Composition API 虽然也是一个步伐迈得比较大的改动,但其组件结构并没有特别大的变化,区别比较大的地方在于组件生命周期和响应式 API 的使用,只要掌握了这些核心功能,上手 Vue 3 非常容易!
回顾 Vue 2
在 Vue 2 ,常用以下三种写法声明 TypeScript 组件:可在 Vue 2 官网的 TypeScript 支持 一章了解更多配置说明。
适用版本 | 基本写法 | 视图写法 |
---|---|---|
Vue 2 | Vue.extend | Template |
Vue 2 | Class Component | Template |
Vue 2 | Class Component | TSX |
使用 Vue.extend API 声明组件:
1 | // 这是一段摘选自 Vue 2 官网的代码演示 |
为了更好地获得 TypeScript 类型推导支持, 使用 Class Component 的写法,这是 Vue 官方推出的一个装饰器插件(需要单独安装):
1 | // 这是一段摘选自 Vue 2 官网的代码演示 |
了解 Vue 3
Vue 3 从设计初期就考虑了 TypeScript 的支持,其中 defineComponent
这个 API 就是为了解决 Vue 2 对 TypeScript 类型推导不完善等问题而推出的。
在 Vue 3 ,至少有以下六种写法可以声明 TypeScript 组件:
适用版本 | 基本写法 | 视图写法 | 生命周期版本 | 官方是否推荐 |
---|---|---|---|---|
Vue 3 | Class Component | Template | Vue 2 | × |
Vue 3 | defineComponent | Template | Vue 2 | × |
Vue 3 | defineComponent | Template | Vue 3 | √ |
Vue 3 | Class Component | TSX | Vue 2 | × |
Vue 3 | defineComponent | TSX | Vue 2 | × |
Vue 3 | defineComponent | TSX | Vue 3 | √ |
其中
defineComponent + Composition API + Template
的组合是 Vue 官方最为推荐的组件声明方式,接下来的内容都会以这种写法作为示范案例,也推荐开发者在学习的过程中,使用该组合进行入门。
编写Hello World 组件
使用 Composition API 编写一个最简单的 Hello World 组件:
1 | <!-- Template 代码和 Vue 2 一样 --> |
可以看到 Vue 3 的组件也是 <template />
+ <script />
+ <style />
的三段式组合,上手非常简单。
其中 Template 沿用了 Vue 2 时期类似 HTML 风格的模板写法, Style 则是使用原生 CSS 语法或者 Less 等 CSS 预处理器编写。
但需要注意的是,在 Vue 3 的 Composition API 写法里,数据或函数如果需要在 <template />
中使用,就必须在 setup
里将其 return
出去,而仅在 <script />
里被调用的函数或变量,不需要渲染到模板则无需 return
。
响应式数据的变化
响应式数据是 MVVM 数据驱动编程的特色, Vue 的设计也是受 MVVM 模型的启发,相信大部分开发者选择 MVVM 框架都是因为数据驱动编程比传统的事件驱动编程要来得方便,而选择 Vue ,则是方便中的方便。作为最重要的一个亮点, Vue 3 的响应式数据在设计上和 Vue 2 有着很大的不同。
回顾 Vue 2
Vue 2 是使用了 Object.defineProperty
API 的 getter/setter
来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Object.defineProperty - MDN 。
下面使用 Object.defineProperty
实现一个简单的双向绑定 demo ,亲自敲代码试一下可以有更多的理解:
1 |
|
这个小 demo 实现了这两个功能:
- 输入框的输入行为只修改
vm.text
的数据,但会同时更新 output 标签的文本内容 - 点击按钮修改
vm.text
的数据,也会触发输入框和 output 文本的更新
当然 Vue 做了非常多的工作,而非只是简单的调用了 Object.defineProperty
,可以在官网 深入 Vue 2 的响应式原理 一章了解更多 Vue 2 的响应式原理。
了解 Vue 3
Vue 3 是使用了 Proxy
API 的 getter/setter
来实现数据的响应性,这个方法的具体用法可以参考 MDN 的文档: Proxy - MDN 。
同样的,也来实现一个简单的双向绑定 demo ,这次使用 Proxy
来实现:
1 |
|
这个 demo 实现的功能和使用 Object.defineProperty
的 demo 是完全一样的,也都是基于 setter
的行为完成数据更新的实现,那么为什么 Vue 3 要舍弃 Object.defineProperty
,换成 Proxy
呢?
主要原因在于 Object.defineProperty
有以下的不足:
- 无法侦听数组下标的变化,对例如
arr[i] = newValue
这样的操作无法实时响应 - 无法侦听数组长度的变化,例如通过
arr.length = 10
去修改数组长度,无法响应 - 只能侦听对象的属性,对于整个对象则需要遍历,针对多级对象更是要通过嵌套来深度侦听
- 使用
Object.assign()
等方法给对象添加新属性时,也不会触发更新 - 更多细节上的问题 …
这也是为什么 Vue 2 要提供一个 Vue.set API 的原因,可以在官网 Vue 2 中检测变化的注意事项 一章了解更多说明。
而这些问题在 Proxy
都可以得到解决,可以在官网 深入 Vue 3 的响应式原理 一章了解更多这部分的内容。
用法上的变化
相对于 Vue 2 在 data
里声明后即可通过 this.xxx
调用响应式数据,在 Vue 3 的生命周期里没有了 Vue 实例的 this
指向,需要导入 ref
、reactive
等响应式 API 才能声明并使用响应式数据。
1 | // 这里导入的 `ref` 是一个响应式 API |
响应式 API 之 ref
类型声明
ref
是最常用的一个响应式 API,它可以用来定义所有类型的数据,包括 Node 节点和组件。
没错,在 Vue 2 常用的以 this.$refs.xxx
取代 document.querySelector('.xxx')
来获取 Node 节点的方式,也是使用了这个 API 。
API 本身的类型
先看 API 本身, ref
API 是一个函数,通过接受一个泛型入参,返回一个响应式对象,所有的值都通过 .value
属性获取,这是 API 本身的 TS 类型:
1 | // `ref` API 的 TS 类型 |
因此在声明变量时,是使用尖括号 <>
包裹其 TS 类型,紧跟在 ref
API 之后:
1 | // 显式指定 `msg.value` 是 `string` 类型 |
再回看该 API 本身的类型,其中使用了 T
泛型,这表示在传入函数的入参时,可以不需要手动指定其 TS 类型, TypeScript 会根据这个 API 所返回的响应式对象的 .value
属性的类型,确定当前变量的类型。
因此也可以省略显式的类型指定,像下面这样声明变量,其类型交给 TypeScript 去自动推导:
1 | // TypeScript 会推导 `msg.value` 是 `string` 类型 |
对于声明时会赋予初始值,并且在使用过程中不会改变其类型的变量,是可以省略类型的显式指定的。
而如果有显式的指定的类型,那么在一些特殊情况下,初始化时可以不必赋值,这样 TypeScript 会自动添加 undefined
类型:
1 | const msg = ref<string>() |
因为入参留空时,虽然指定了 string
类型,但实际上此时的值是 undefined
,所以实际上这个时候的 msg.value
是一个 string | undefined
的联合类型。
对于声明时不知道是什么值,在某种条件下才进行初始化的情况,就可以省略其初始值,但是切记在调用该变量的时候对 .value
值进行有效性判断。
而如果既不显式指定类型,也不赋予初始值,那么会被默认为 any
类型,除非真的无法确认类型,否则不建议这么做。
API 返回值的类型
细心的开发者还会留意到 ref
API 类型里面还标注了一个返回值的 TS 类型:
1 | interface Ref<T> { |
它是代表整个 Ref 变量的完整类型:
- 上文声明 Ref 变量时,提到的
string
类型都是指msg.value
这个.value
属性的类型 - 而
msg
这个响应式变量,其本身是Ref<string>
类型
如果在开发过程中需要在函数里返回一个 Ref 变量,那么其 TypeScript 类型就可以这样写(请留意 Calculator
里的 num
变量的类型):
1 | // 导入 `ref` API 函数 |
上面这个简单的例子演示了如何手动指定 Ref 变量的类型,对于逻辑复用时的函数代码抽离、插件开发等场景非常有用!当然大部分情况下可以交给 TypeScript 自动推导,但掌握其用法,在必要的时候就派得上用场了!
变量的定义
在了解了如何对 Ref 变量进行类型声明之后,面对不同的数据类型,相信都得心应手了!但不同类型的值之间还是有少许差异和注意事项,例如上文提及该 API 可以用来定义所有类型的数据,包括 Node 节点和组件,具体可以参考下文的示例。
基本类型
对字符串、布尔值等基本类型的定义方式,比较简单:
1 | // 字符串 |
引用类型
对于对象、数组等引用类型也适用,比如要定义一个对象:
1 | // 先声明对象的格式 |
定义一个普通数组:
1 | // 数值数组 |
定义一个对象数组:
1 | // 声明对象的格式 |
DOM 元素与子组件
除了可以定义数据,ref
也有熟悉的用途,就是用来挂载节点,也可以挂在子组件上,对应在 Vue 2 时常用的 this.$refs.xxx
,起到获取 DOM 元素信息的作用。
模板部分依然是熟悉的用法,在要引用的 DOM 上添加一个 ref
属性:
1 | <template> |
在 <script />
部分有三个最基本的注意事项:
- 在
<template />
代码里添加的ref
属性的值,对应<script />
里使用ref
API 声明的变量的名称; - 请保证视图渲染完毕后再执行 DOM 或组件的相关操作(需要放到生命周期的
onMounted
或者nextTick
函数里,这一点在 Vue 2 也是一样); - 该 Ref 变量必须
return
出去才可以给到<template />
使用,这一点是 Vue 3 生命周期的硬性要求,子组件的数据和方法如果要给父组件操作,也要return
出来才可以。
配合上面的 <template />
,来看看 <script />
部分的具体例子:
1 | import { defineComponent, onMounted, ref } from 'vue' |
关于 DOM 和子组件的 TS 类型声明,可参考以下规则:
节点类型 | 声明类型 | 参考文档 |
---|---|---|
DOM 元素 | 使用 HTML 元素接口 | HTML 元素接口 |
子组件 | 使用 InstanceType 配合 typeof 获取子组件的类型 |
typeof 操作符 |
单纯使用
typeof Child
虽然可以获得 Child.vue 组件的 Props 和方法等提示,但目前在 VSCode 的类型推导还不够智能,缺乏更有效的代码补全支持。上文使用的
InstanceType<T>
是 TypeScript 提供的一个工具类型,可以获取构造函数类型的实例类型,因此将组件的类型声明为InstanceType<typeof Child>
,不仅可以得到更完善的类型提示,在编程过程中还可以让编辑器提供更完善的代码补全功能。
变量的读取与赋值
前面在介绍 API 类型的时候已经了解,通过 ref
声明的变量会全部变成对象,不管定义的是什么类型的值,都会转化为一个 Ref 对象,其中 Ref 对象具有指向内部值的单个 Property .value
。
也就是说,任何 Ref 对象的值都必须通过 xxx.value
才可以正确获取。
请牢记上面这句话,初用 Vue 3 的开发者很多 BUG 都是由这个问题引起的(包括笔者刚开始使用 Vue 3 的那段时间,嘿嘿)。
读取变量
平时对于普通变量的值,读取的时候都是直接调用其变量名即可:
1 | // 读取一个字符串 |
而 Ref 对象的值的读取,切记!必须通过 .value
!
1 | // 读取一个字符串 |
为变量赋值
普通变量需要使用 let
声明才可以修改其值,由于 Ref 对象是个引用类型,所以可以使用 const
声明,直接通过 .value
修改。
1 | // 声明一个字符串变量 |
因此日常业务中,像在对接服务端 API 的接口数据时,可以自由地使用 forEach
、map
、filter
等方法操作 Ref 数组,或者直接重置它,而不必担心数据失去响应性。
1 | const data = ref<string[]>([]) |
为什么突然要说这个呢?因为涉及到下一部分的知识,关于 reactive
API 在使用上的注意事项。
响应式 API 之 reactive
reactive
是继 ref
之后最常用的一个响应式 API 了,相对于 ref
,它的局限性在于只适合对象、数组。
使用 reactive
的好处就是写法跟平时的对象、数组几乎一模一样,但它也带来了一些特殊注意点,请留意赋值部分的特殊说明。
类型声明与定义
reactive
变量的声明方式没有 ref
的变化那么大,基本上和普通变量一样,它的 TS 类型如下:
1 | function reactive<T extends object>(target: T): UnwrapNestedRefs<T> |
可以看到其用法还是比较简单的,下面是一个 Reactive 对象的声明方式:
1 | // 声明对象的类型 |
下面是 Reactive 数组的声明方式:
1 | const uids: number[] = reactive([1, 2, 3]) |
还可以声明一个 Reactive 对象数组:
1 | // 对象数组也是先声明其中的对象类型 |
变量的读取与赋值
虽然 reactive
API 在使用上没有像 ref
API 一样有 .value
的心智负担,但也有一些注意事项要留意。
处理对象
Reactive 对象在读取或者修改字段的值时,与普通对象是一样的,这部分没有太多问题。
1 | // 声明对象的类型 |
处理数组
但是 Reactive 数组和普通数组会有一些区别。
普通数组在 “重置” 或者 “修改值” 时都可以直接操作:
1 | // 定义一个普通数组 |
但在 Vue 3 ,如果使用 reactive
定义数组,则不能这么处理,必须只使用那些不会改变引用地址的操作。比如要从服务端 API 接口获取翻页数据时,通常要先重置数组,再异步添加数据,如果使用常规的重置,会导致这个变量失去响应性:
1 | let uids: number[] = reactive([1, 2, 3]) |
要让数据依然保持响应性,则必须在关键操作时,不破坏响应性 API ,以下是推荐的操作方式,通过重置数组的 length
长度来实现数据的重置:
1 | const uids: number[] = reactive([1, 2, 3]) |
特别注意
不要对 Reactive 数据进行 ES6 的解构 操作,因为解构后得到的变量会失去响应性。
比如这些情况,在 2s 后都得不到新的 name 信息:
1 | import { defineComponent, reactive } from 'vue' |
响应式 API 之 toRef 与 toRefs
相信各位开发者看到这里时,应该已经对 ref
和 reactive
API 都有所了解了,为了方便开发者使用, Vue 3 还推出了两个与之相关的 API : toRef
和 toRefs
,都是用于 reactive
向 ref
转换。
各自的作用
这两个 API 在拼写上非常接近,顾名思义,一个是只转换一个字段,一个是转换所有字段,转换后将得到新的变量,并且新变量和原来的变量可以保持同步更新。
API | 作用 |
---|---|
toRef | 创建一个新的 Ref 变量,转换 Reactive 对象的某个字段为 Ref 变量 |
toRefs | 创建一个新的对象,它的每个字段都是 Reactive 对象各个字段的 Ref 变量 |
光看概念可能不容易理解,来看下面的例子,先声明一个 reactive
变量:
1 | interface Member { |
然后分别看看这两个 API 应该怎么使用。
使用 toRef
先看这个转换单个字段的 toRef
API ,了解了它的用法之后,再去看 toRefs
就很容易理解了。
API 类型和基本用法
toRef
API 的 TS 类型如下:
1 | // `toRef` API 的 TS 类型 |
通过接收两个必传的参数(第一个是 reactive
对象, 第二个是要转换的 key
),返回一个 Ref 变量,在适当的时候也可以传递第三个参数,为该变量设置默认值。
以上文声明好的 userInfo
为例,如果想转换 name
这个字段为 Ref 变量,只需要这样操作:
1 | const name = toRef(userInfo, 'name') |
等号左侧的 name
变量此时是一个 Ref 变量,这里因为 TypeScript 可以对其自动推导,因此声明时可以省略 TS 类型的显式指定,实际上该变量的类型是 Ref<string>
。
所以之后在读取和赋值时,就需要使用 name.value
来操作,在重新赋值时会同时更新 name
和 userInfo.name
的值:
1 | // 修改前先查看初始值 |
这个 API 也可以接收一个 Reactive 数组,此时第二个参数应该传入数组的下标:
1 | // 这一次声明的是数组 |
设置默认值
如果 Reactive 对象上有一个属性本身没有初始值,也可以传递第三个参数进行设置(默认值仅对 Ref 变量有效):
1 | interface Member { |
数组也是同理,对于可能不存在的下标,可以传入默认值避免项目的逻辑代码出现问题:
1 | const words = reactive(['a', 'b', 'c']) |
其他用法
这个 API 还有一个特殊用法,但不建议在 TypeScript 里使用。
在 toRef
的过程中,如果使用了原对象上面不存在的 key
,那么定义出来的 Ref 变量的 .value
值将会是 undefined
。
1 | // 众所周知, Petter 是没有女朋友的 |
如果对这个不存在的 key
的 Ref 变量进行赋值,那么原来的 Reactive 对象也会同步增加这个 key
,其值也会同步更新。
1 | // 赋值后,不仅 Ref 变量得到了 `Marry` , Reactive 对象也得到了 `Marry` |
为什么强调不要在 TypeScript 里使用呢?因为在编译时,无法通过 TypeScript 的类型检查:
如果不得不使用这种情况,可以考虑使用 any 类型:
1 | // 将该类型直接指定为 `any` |
使用 toRefs
在了解了 toRef
API 之后,来看看 toRefs
的用法。
API 类型和基本用法
先看看它的 TS 类型:
1 | function toRefs<T extends object>( |
与 toRef
不同, toRefs
只接收了一个参数,是一个 reactive
变量。
1 | interface Member { |
此时这个新的 userInfoRefs
变量,它的 TS 类型就不再是 Member
了,而应该是:ToRefs<Member>
1 | // 导入 `toRefs` API 的类型 |
也可以重新编写一个新的类型来指定它,因为每个字段都是与原来关联的 Ref 变量,所以也可以这样声明:
1 | // 导入 `ref` API 的类型 |
当然实际上日常使用时并不需要手动指定其类型, TypeScript 会自动推导,可以节约非常多的开发工作量。
和 toRef
API 一样,这个 API 也是可以对数组进行转换:
1 | const words = reactive(['a', 'b', 'c']) |
此时新数组的类型是 Ref<string>[]
,不再是原来的 string[]
类型。
解构与赋值
转换后的 Reactive 对象或数组支持 ES6 的解构,并且不会失去响应性,因为解构后的每一个变量都具备响应性。
1 | // 为了提高开发效率,可以直接将 Ref 变量直接解构出来使用 |
这一点和直接解构 Reactive 变量有非常大的不同,直接解构 Reactive 变量,得到的是一个普通的变量,不再具备响应性。
这个功能在使用 Hooks 函数非常好用(在 Vue 3 里也叫可组合函数, Composable Functions ),还是以一个计算器函数为例,这一次将其修改为内部有一个 Reactive 的数据状态中心,在函数返回时解构为多个 Ref 变量:
1 | import { reactive, toRefs } from 'vue' |
这样在调用 useCalculator
函数时,可以通过解构直接获取到 Ref 变量,不需要再进行额外的转换工作。
1 | // 解构出来的 `num` 和 `step` 都是 Ref 变量 |
为什么要进行转换
关于为什么要出这么两个 API ,官方文档没有特别说明,不过经过笔者在业务中的一些实际使用感受,以及在写上一节 reactive
的 特别注意,可能知道一些使用理由。
关于 ref
和 reactive
这两个 API 的好处就不重复了,但是在使用的过程中,各自都有不方便的地方:
ref
API 虽然在 <template />
里使用起来方便,但是在 <script />
里进行读取 / 赋值的时候,要一直记得加上 .value
,否则 BUG 就来了。
reactive
API 虽然在使用的时候,因为知道它本身是一个对象,所以不会忘记通过 foo.bar
这样的格式去操作,但是在 <template />
渲染的时候,又因此不得不每次都使用 foo.bar
的格式去渲染。
那么有没有办法,既可以在编写 <script />
的时候不容易出错,在写 <template />
的时候又比较简单呢?
于是, toRef
和 toRefs
因此诞生。
什么场景下比较适合使用它们
从便利性和可维护性来说,最好只在功能单一、代码量少的组件里使用,比如一个表单组件,通常表单的数据都放在一个对象里。
当然也可以把所有的数据都定义到一个 data
里,再去 data
里面取值,但是没有必要为了转换而转换,否则不如使用 Options API 风格。
在业务中的具体运用
继续使用上文一直在使用的 userInfo
来当案例,以一个用户信息表的小 demo 做个演示。
在 <script />
部分:
先用
reactive
定义一个源数据,所有的数据更新,都是修改这个对象对应的值,按照对象的写法维护数据再通过
toRefs
定义一个给<template />
使用的对象,这样可以得到一个每个字段都是 Ref 变量的新对象在
return
的时候,对步骤 2 里的toRefs
对象进行解构,这样导出去就是各个字段对应的 Ref 变量,而不是一整个对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import { defineComponent, reactive, toRefs } from 'vue'
interface Member {
id: number
name: string
age: number
gender: string
}
export default defineComponent({
setup() {
// 定义一个 reactive 对象
const userInfo = reactive({
id: 1,
name: 'Petter',
age: 18,
gender: 'male',
})
// 定义一个新的对象,它本身不具备响应性,但是它的字段全部是 Ref 变量
const userInfoRefs = toRefs(userInfo)
// 在 2s 后更新 `userInfo`
setTimeout(() => {
userInfo.id = 2
userInfo.name = 'Tom'
userInfo.age = 20
}, 2000)
// 在这里解构 `toRefs` 对象才能继续保持响应性
return {
...userInfoRefs,
}
},
})在
<template />
部分:由于return
出来的都是 Ref 变量,所以在模板里可以直接使用userInfo
各个字段的key
,不再需要写很长的userInfo.name
了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<template>
<ul class="user-info">
<li class="item">
<span class="key">ID:</span>
<span class="value">{{ id }}</span>
</li>
<li class="item">
<span class="key">name:</span>
<span class="value">{{ name }}</span>
</li>
<li class="item">
<span class="key">age:</span>
<span class="value">{{ age }}</span>
</li>
<li class="item">
<span class="key">gender:</span>
<span class="value">{{ gender }}</span>
</li>
</ul>
</template>
需要注意的问题
请注意是否有相同命名的变量存在,比如上面在 return
给 <template />
使用时,在解构 userInfoRefs
的时候已经包含了一个 name
字段,此时如果还有一个单独的变量也叫 name
,就会出现渲染上的数据显示问题。
此时它们在 <template />
里哪个会生效,取决于谁排在后面,因为 return
出去的其实是一个对象,在对象里,如果存在相同的 key
,则后面的会覆盖前面的。
下面这种情况,会以单独的 name
为渲染数据:
1 | return { |
而下面这种情况,则是以 userInfoRefs
里的 name
为渲染数据:
1 | return { |
所以当决定使用 toRef
和 toRefs
API 的时候,请注意这个特殊情况!
函数的声明和使用
在了解了响应式数据如何使用之后,接下来就要开始了解函数了。
在 Vue 2 ,函数通常是作为当前组件实例上的方法在 methods
里声明,然后再在 mounted
等生命周期里调用,或者是在模板里通过 Click 等行为触发,由于组件内部经常需要使用 this
获取组件实例,因此不能使用箭头函数。
1 | export default { |
在 Vue 3 则灵活了很多,可以使用普通函数、 Class 类、箭头函数、匿名函数等等进行声明,可以将其写在 setup
里直接使用,也可以抽离在独立的 .js
/ .ts
文件里再导入使用。
需要在组件创建时自动执行的函数,其执行时机需要遵循 Vue 3 的生命周期,需要在模板里通过 @click
、@change
等行为触发,和变量一样,需要把函数名在 setup
里进行 return
出去。
下面是一个简单的例子,方便开发者更直观地了解:
1 | <template> |
数据的侦听
侦听数据变化也是组件里的一项重要工作,比如侦听路由变化、侦听参数变化等等。
Vue 3 在保留原来的 watch
功能之外,还新增了一个 watchEffect
帮助更简单地进行侦听。
watch
在 Vue 3 ,新版的 watch
和 Vue 2 的旧版写法对比,在使用方式上变化非常大!
回顾 Vue 2
在 Vue 2 是这样用的,和 data
、 methods
都在同级配置:
1 | export default { |
并且类型繁多,选项式 API 的类型如下:
1 | watch: { [key: string]: string | Function | Object | Array} |
联合类型过多,意味着用法复杂,下面是个很好的例子,虽然出自 官网 的用法介绍,但过于繁多的用法也反映出来对初学者不太友好,初次接触可能会觉得一头雾水:
1 | export default { |
另外需要注意的是,不能使用箭头函数来定义 Watcher 函数 (例如
searchQuery: newValue => this.updateAutocomplete(newValue)
)。因为箭头函数绑定了父级作用域的上下文,所以
this
将不会按照期望指向组件实例,this.updateAutocomplete
将是undefined
。
Vue 2 也可以通过 this.$watch()
这个 API 的用法来实现对某个数据的侦听,它接受三个参数: source
、 callback
和 options
。
1 | export default { |
了解 Vue 3
在 Vue 3 的组合式 API 写法, watch
是一个可以接受 3 个参数的函数(保留了 Vue 2 的 this.$watch
这种用法),在使用层面上简单了很多。
1 | import { watch } from 'vue' |
下面的内容都基于 Vue 3 的组合式 API 用法展开讲解。
API 的 TS 类型
在了解用法之前,先对它的 TS 类型声明做一个简单的了解, watch 作为组合式 API ,根据使用方式有两种类型声明:
基础用法的 TS 类型
1
2
3
4
5
6
7
8// watch 部分的 TS 类型
// ...
export declare function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// ...批量侦听的 TS 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14// watch 部分的 TS 类型
// ...
export declare function watch<
T extends MultiWatchSources,
Immediate extends Readonly<boolean> = false
>(
sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// MultiWatchSources 是一个数组
declare type MultiWatchSources = (WatchSource<unknown> | object)[]
// ...
但是不管是基础用法还是批量侦听,可以看到这个 API 都是接受三个入参, 并返回一个可以用来停止侦听的函数
参数 是否可选 含义 source 必传 数据源 callback 必传 侦听到变化后要执行的回调函数 options 可选 一些侦听选项
要侦听的数据源
在上面API 的 TS 类型已经对 watch
API 的组成有一定的了解了,这里先对数据源的类型和使用限制做下说明。如果不提前了解,在使用的过程中可能会遇到 “侦听了但没有反应” 的情况出现。
watch
API 的第 1 个参数 source
是要侦听的数据源,它的 TS 类型如下:
1 | // watch 第 1 个入参的 TS 类型 |
可以看到能够用于侦听的数据,是通过响应式 API定义的变量( Ref<T>
),或者是一个 计算数据( ComputedRef<T>
),或者是一个 getter 函数 ( () => T
)。
所以要想定义的 watch 能够做出预期的行为,数据源必须具备响应性或者是一个 getter ,如果只是通过 let
定义一个普通变量,然后去改变这个变量的值,这样是无法侦听的。
如果要侦听响应式对象里面的某个值(这种情况下对象本身是响应式,但它的 property 不是),需要写成 getter 函数,简单的说就是需要写成有返回值的函数,这个函数 return 要侦听的数据, e.g.
() => foo.bar
。
侦听后的回调函数
在上面 API 的 TS 类型 介绍了 watch API 的组成,和数据源一样,先了解一下回调函数的定义。
watch API 的第 2 个参数 callback
是侦听到数据变化时要做出的行为,它的 TS 类型如下
1 | // watch 第 2 个入参的 TS 类型 |
乍一看它有三个参数,但实际上这些参数不是自己定义的,而是 watch API 传递的,所以不管用或者不用,它们都在那里:
参数 | 作用 |
---|---|
value | 变化后的新值,类型和数据源保持一致 |
oldValue | 变化前的旧值,类型和数据源保持一致 |
onCleanup | 注册一个清理函数 |
注意:第一个参数是新值,第二个才是原来的旧值!
如同其他 JS 函数,在使用 watch 的回调函数时,可以对这三个参数任意命名,比如把 value
命名为觉得更容易理解的 newValue
。
另外,默认情况下,watch
是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。
基础用法
来到这里,对 2 个必传的参数都有一定的了解了,先看看基础的用法,也就是日常最常编写的方案,只需要先关注前 2 个必传的参数。
1 | // 不要忘了导入要用的 API |
如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,那么可以使用 批量侦听 。特殊的情况下,可以搭配 侦听的选项 做一些特殊的用法,详见下面部分的内容。
批量侦听
如果有多个数据源要侦听,并且侦听到变化后要执行的行为一样,第一反应可能是这样来写:
- 抽离相同的处理行为为公共函数
- 然后定义多个侦听操作,传入这个公共函数
1 | import { defineComponent, ref, watch } from 'vue' |
这样写其实没什么问题,不过除了抽离公共代码的写法之外, watch API 还提供了一个批量侦听的用法,和基础用法的区别在于,数据源和回调参数都变成了数组的形式。
数据源:以数组的形式传入,里面每一项都是一个响应式数据。
回调数:原来的
value
和newValue
也都变成了数组,每个数组里面的顺序和数据源数组排序一致。
可以看下面的这个例子更为直观:
1 | import { defineComponent, ref, watch } from 'vue' |
watch侦听的选项
在 API 的 TS 类型 里提到, watch API 还接受第 3 个参数 options ,可选的一些侦听选项。
它的 TS 类型如下:
1 | // watch 第 3 个入参的 TS 类型 |
options
是一个对象的形式传入,有以下几个选项:
选项 | 类型 | 默认值 | 可选值 | 作用 |
---|---|---|---|---|
deep | boolean | false | true | false | 是否进行深度侦听 |
immediate | boolean | false | true | false | 是否立即执行侦听回调 |
flush | string | ‘pre’ | ‘pre’ | ‘post’ | ‘sync’ | 控制侦听回调的调用时机 |
onTrack | (e) => void | 在数据源被追踪时调用 | ||
onTrigger | (e) => void | 在侦听回调被触发时调用 |
其中 onTrack
和 onTrigger
的 e
是 debugger 事件,建议在回调内放置一个 debugger 语句 以调试依赖,这两个选项仅在开发模式下生效。
deep 默认是 false
,但是在侦听 reactive 对象或数组时,会默认为 true
。
侦听选项之 deep
deep
选项接受一个布尔值,可以设置为 true
开启深度侦听,或者是 false
关闭深度侦听,默认情况下这个选项是 false
关闭深度侦听的,但也存在特例。
设置为 false
的情况下,如果直接侦听一个响应式的 引用类型 数据(e.g. Object
、 Array
… ),虽然它的属性的值有变化,但对其本身来说是不变的,所以不会触发 watch 的 callback 。
下面是一个关闭了深度侦听的例子:
1 | import { defineComponent, ref, watch } from 'vue' |
类似这种情况,需要把 deep
设置为 true
才可以触发侦听。
可以看到上面的例子特地用了ref API,这是因为通过 reactive API义的对象无法将 deep
成功设置为 false
(这一点在目前的官网文档未找到说明,最终是在 watch API 的源码 上找到了答案)。
1 | // ... |
这个情况就是上面所说的 “特例” ,可以通过 isReactive
API 来判断是否需要手动开启深度侦听。
1 | // 导入 isReactive API |
侦听选项之 immediate
watch 默认是惰性的,也就是只有当被侦听的数据源发生变化时才执行回调。
这句话是什么意思呢?来看一下这段代码,为了减少deep选项的干扰,换一个类型,换成 string
数据来演示,请留意注释:
1 | import { defineComponent, ref, watch } from 'vue' |
可以看到,数据在初始化的时候并不会触发侦听回调,如果有需要的话,通过 immediate
选项来让它直接触发。
immediate
选项接受一个布尔值,默认是 false
,可以设置为 true
让回调立即执行。
1 | import { defineComponent, ref, watch } from 'vue' |
注意,在带有 immediate 选项时,不能在第一次回调时取消该数据源的侦听。
侦听选项之 flush
flush
选项是用来控制[侦听回调的调用时机,接受指定的字符串,可选值如下,默认是 'pre'
。
可选值 | 回调的调用时机 | 使用场景 |
---|---|---|
‘pre’ | 将在渲染前被调用 | 允许回调在模板运行前更新了其他值 |
‘sync’ | 在渲染时被同步调用 | 目前来说没什么好处,可以了解但不建议用… |
‘post’ | 被推迟到渲染之后调用 | 如果要通过 ref 操作 DOM 元素与子组件 ,需要使用这个值来启用该选项,以达到预期的执行效果 |
对于 'pre'
和 'post'
,回调使用队列进行缓冲。回调只被添加到队列中一次。
即使观察值变化了多次,值的中间变化将被跳过,不会传递给回调,这样做不仅可以提高性能,还有助于保证数据的一致性。
更多关于 flush 的信息,请参阅 回调的触发时机 。
停止侦听
如果在 setup或者script-setup里使用 watch 的话, 组件被卸载的时候也会一起被停止,一般情况下不太需要关心如何停止侦听。
不过有时候可能想要手动取消, Vue 3 也提供了方法。
随着组件被卸载一起停止的前提是,侦听器必须是 同步语句 创建的,这种情况下侦听器会绑定在当前组件上。
如果放在
setTimeout
等 异步函数 里面创建,则不会绑定到当前组件,因此组件卸载的时候不会一起停止该侦听器,这种时候就需要手动停止侦听。
当在定义一个 watch 行为的时候,它会返回一个用来停止侦听的函数。这个函数的 TS 类型如下:
1 | export declare type WatchStopHandle = () => void |
用法很简单,做一下简单了解即可:
1 | // 定义一个取消观察的变量,它是一个函数 |
但是也有一点需要注意的是,如果启用了immediate 选项,不能在第一次触发侦听回调时执行它。
1 | // 注意:这是一段错误的代码,运行会报错 |
会收获一段报错,告诉 unwatch
这个变量在初始化前无法被访问:
1 | Uncaught ReferenceError: Cannot access 'unwatch' before initialization |
目前有两种方案可以让实现这个操作:
方案一:使用 var
并判断变量类型,利用 var 的变量提升 来实现目的。
1 | // 这里改成 var ,不要用 const 或 let |
不过 var
已经属于过时的语句了,建议用方案二的 let
。
方案二:使用 let
并判断变量类型。
1 | // 如果不想用 any ,可以导入 TS 类型 |
侦听效果清理
在 侦听后的回调函数部分提及到一个参数 onCleanup
,它可以帮注册一个清理函数。
有时 watch 的回调会执行异步操作,当 watch 到数据变更的时候,需要取消这些操作,这个函数的作用就用于此,会在以下情况调用这个清理函数:
- watcher 即将重新运行的时候
- watcher 被停止(组件被卸载或者被手动停止侦听)
TS 类型:
1 | declare type OnCleanup = (cleanupFn: () => void) => void |
用法方面比较简单,传入一个回调函数运行即可,不过需要注意的是,需要在停止侦听之前注册好清理行为,否则不会生效。
在停止侦听里的最后一个 immediate 例子的基础上继续添加代码,请注意注册的时机:
1 | let unwatch: WatchStopHandle |
watchEffect
如果一个函数里包含了多个需要侦听的数据,一个一个数据去侦听太麻烦了,在 Vue 3 ,可以直接使用 watchEffect API 来简化的操作。
API 的 TS 类型
这个 API 的类型如下,使用的时候需要传入一个副作用函数(相当于 watch 的 侦听后的回调函数),也可以根据的实际情况传入一些可选的侦听选项 。
和 watch API 一样,它也会返回一个用于停止侦听 的函数。
1 | // watchEffect 部分的 TS 类型 |
副作用函数也会传入一个清理回调作为参数,和 watch 的 侦听效果清理一样的用法。可以理解为它是一个简化版的 watch ,具体简化在哪里呢?请看下面的用法示例。
用法示例
它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
1 | import { defineComponent, ref, watchEffect } from 'vue' |
和 watch 的区别
虽然理论上 watchEffect
是 watch
的一个简化操作,可以用来代替 批量侦听 ,但它们也有一定的区别:
watch
可以访问侦听状态变化前后的值,而watchEffect
没有。watch
是在属性改变的时候才执行,而watchEffect
则默认会执行一次,然后在属性改变的时候也会执行。
第二点的意思,看下面这段代码可以有更直观的理解:
使用 watch :
1 | export default defineComponent({ |
使用 watchEffect :
1 | export default defineComponent({ |
可用的侦听选项
虽然用法和 watch 类似,但也简化了一些选项,它的侦听选项 TS 类型如下:
1 | // 只支持 base 类型 |
对比 watch API ,它不支持deep和 immediate,请记住这一点,其他的用法是一样的。
watchPostEffect
watchEffec API 使用 flush: 'post'
选项时的别名
TIP Vue v3.2.0 及以上版本才支持该 API 。
watchSyncEffe
watchEffect API 使用 flush: 'sync'
选项时的别名。
TIP Vue v3.2.0 及以上版本才支持该 API 。
数据的计算
和 Vue 2.0 一样,数据的计算也是使用 computed
API ,它可以通过现有的响应式数据,去通过计算得到新的响应式变量,用过 Vue 2.0 的开发者应该不会太陌生,但是在 Vue 3.0 ,在使用方式上也是变化非常大!
TIP 这里的响应式数据,可以简单理解为通过 ref API 、 reactive API 定义出来的数据,当然 Vuex 、Vue Router 等 Vue 数据也都具备响应式,可以在 响应式数据的变化 了解。
用法变化
先从一个简单的用例来看看在 Vue 新旧版本的用法区别:
假设定义了两个分开的数据 firstName
名字和 lastName
姓氏,但是在 template 展示时,需要展示完整的姓名,那么就可以通过 computed
来计算一个新的数据:
回顾 Vue 2
在 Vue 2.0 ,computed
和 data
在同级配置,并且不可以和 data
里的数据同名重复定义:
1 | // 在 Vue 2 的写法: |
这样在需要用到全名的地方,只需要通过 this.fullName
就可以得到 Bill Gates
。
了解 Vue 3
在 Vue 3.0 ,跟其他 API 的用法一样,需要先导入 computed
才能使用:
1 | // 在 Vue 3 的写法: |
可以把这个用法简单的理解为,传入一个回调函数,并 return
一个值,对,它需要有明确的返回值。
需要注意的是:
- 定义出来的
computed
变量,和 Ref 变量的用法一样,也是需要通过.value
才能拿到它的值- 但是区别在于,默认情况下
computed
的value
是只读的
类型声明
之前说过,在defineComponent里,会自动帮推导 Vue API 的类型,所以一般情况下,是不需要显式的去定义 computed
出来的变量类型的。
在确实需要手动指定的情况下,也可以导入它的类型然后定义:
1 | import { computed } from 'vue' |
要返回一个字符串,就写 ComputedRef<string>
;返回布尔值,就写 ComputedRef<boolean>
;返回一些复杂对象信息,可以先定义好的类型,再诸如 ComputedRef<UserInfo>
去写。
1 | // 这是 ComputedRef 的类型声明: |
优势对比和注意事项
在继续往下看之前,先来了解一下这个 API 的一些优势和注意事项(如果在 Vue 2 已经有接触过的话,可以跳过这一段,因为优势和需要注意的东西比较一致)。
优势对比
看到这里,相信刚接触的开发者可能会有疑问,既然 computed
也是通过一个函数来返回值,那么和普通的 function
有什么区别,或者说优势?
- 性能优势
- 这一点在 官网文档 其实是有提到的:数据的计算是基于它们的响应依赖关系缓存的,只在相关响应式依赖发生改变时它们才会重新求值。
- 也就是说,只要原始数据没有发生改变,多次访问
computed
,都是会立即返回之前的计算结果,而不是再次执行函数;而普通的function
调用多少次就执行多少次,每调用一次就计算一次。 - 至于为何要如此设计,官网文档也给出了原因:为什么需要缓存?假设有一个性能开销比较大的计算数据 list,它需要遍历一个巨大的数组并做大量的计算。然后可能有其他的计算数据依赖于 list。如果没有缓存,将不可避免地多次执行 list 的 getter!如果不希望有缓存,请用function 来替代。
- 书写统一
- 假定 foo1 是 Ref 变量, foo2 是
computed
变量, foo3 是普通函数返回值 - 看到这里的开发者应该都已经清楚 Vue 3 的 Ref 变量是通过
foo1.value
来拿到值的,而computed
也是通过foo2.value
,并且在 template 里都可以省略.value
,在读取方面,他们是有一致的风格和简洁性。 - 而 foo3 不管是在 script 还是 template ,都需要通过
foo3()
才能拿到结果,相对来说会有那么一丢丢别扭。
- 假定 foo1 是 Ref 变量, foo2 是
注意事项
有优势当然也就有一定的 “劣势” ,当然这也是 Vue 框架的有意为之,所以在使用上也需要注意一些问题:
只会更新响应式数据的计算
假设要获取当前的时间信息,因为不是响应式数据,所以这种情况下就需要用普通的函数去获取返回值,才能拿到最新的时间。
1
2
3
4
5
6
7
8
9const nowTime = computed(() => new Date())
console.log(nowTime.value)
// 输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
// 2s 后依然是跟上面一样的结果
setTimeout(() => {
console.log(nowTime.value)
// 还是输出 Sun Nov 14 2021 21:07:00 GMT+0800 (GMT+08:00)
}, 2000)
数据是只读的
通过 computed 定义的数据,它是只读的,这一点在类型声明已经有所了解。
如果直接赋值,不仅无法变更数据,而且会收获一个报错。
1
TS2540: Cannot assign to 'value' because it is a read-only property.
虽然无法直接赋值,但是在必要的情况下,依然可以通过
computed
的setter
来更新数据。
setter 的使用
通过 computed 定义的变量默认都是只读的形式(只有一个 getter ),但是在必要的情况下,也可以使用其 setter 属性来更新数据。
基本格式
当需要用到 setter 的时候, computed
就不再是一个传入 callback 的形式了,而是传入一个带有 2 个方法的对象。
1 | // 注意这里computed接收的入参已经不再是函数 |
这里的 get
就是 computed
的 getter ,跟原来传入 callback 的形式一样,用于 foo.value
的读取,所以这里必须有明确的返回值。
这里的 set
就是 computed
的 setter ,它会接收一个参数,代表新的值,当通过 foo.value = xxx
赋值的时候,赋入的这个值,就会通过这个入参传递进来,可以根据的业务需要,把这个值赋给相关的数据源。
TIP 请注意,必须使用
get
和set
这 2 个方法名,也只接受这 2 个方法。
使用示范
1 | // 还是这2个数据源 |
应用场景
计算 API 的作用,官网文档只举了一个非常简单的例子,那么在实际项目中,什么情况下用它会让更方便呢?
简单举几个比较常见的例子吧,加深一下对 computed
的理解。
数据的拼接和计算
如上面的案例,与其每个用到的地方都要用到 firstName + ' ' + lastName
这样的多变量拼接,不如用一个 fullName
来的简单。
当然,不止是字符串拼接,数据的求和等操作更是合适,比如说做一个购物车,购物车里有商品列表,同时还要显示购物车内的商品总金额,这种情况就非常适合用计算数据。
复用组件的动态数据
在一个项目里,很多时候组件会涉及到复用,比如说:“首页的文章列表 vs 列表页的文章列表 vs 作者详情页的文章列表” ,特别常见于新闻网站等内容资讯站点,这种情况下,往往并不需要每次都重新写 UI 、数据渲染等代码,仅仅是接口 URL 的区别。
这种情况就可以通过路由名称来动态获取要调用哪个列表接口:
1 | const route = useRoute() |
当然,这种情况也可以在父组件通过 props
传递接口 URL 。
获取多级对象的值
经常遇到这样的情况:要在 template 显示一些多级对象的字段,而某些字段不一定有,需要做一些判断。虽然有 v-if
,但是嵌套层级一多,模板代码会难以维护。
如果把这些工作量转移给计算数据,结合 try / catch
,就无需在 template 里处理很多判断了。
1 | // 例子比较极端,但在 Vuex 这种大型数据树上,也不是完全不可能存在 |
这样在 template 里要拿到 foo 的值,完全不需要关心中间一级又一级的字段是否存在,只需要区分是不是默认值。
不同类型的数据转换
有时候会遇到一些需求,类似于:让用户在输入框里按一定的格式填写文本,比如用英文逗号 ,
隔开每个词,然后保存时用数组的格式提交给接口。
这个时候 computed
的 setter 就发挥妙用了,只需要一个简单的 computed
,就可以代替 input
的 change
事件或者 watch
侦听,可以减少很多业务代码的编写。
1 | <template> |
所以在实际业务开发中,开发者可以多考虑一下是否可以使用 computed
代替 watch
,避免过多的数组侦听带来项目性能的下降。
指令
指令是 Vue 模板语法里的特殊标记,在使用上和 HTML 的 data-* 属性十分相似,统一以 v-
开头( e.g. v-html
)。
它以简单的方式实现了常用的 JavaScript 表达式功能,当表达式的值改变的时候,响应式地作用到 DOM 上。
内置指令
Vue 提供了一些内置指令可以直接使用,例如:
1 | <template> |
内置指令在使用上都非常的简单,可以在官方文档的 内置指令 一章查询完整的指令列表和用法,在模板上使用时,请了解 指令的模板语法 。
TIP:有两个指令可以使用别名:
v-on
的别名是@
,使用@click
等价于v-on:click
v-bind
的别名是:
,使用:src
等价于v-bind:src
自定义指令
如果 Vue 的内置指令不能满足业务需求,还可以开发自定义指令。
相关的 TS 类型
在开始编写代码之前,先了解一下自定义指令相关的 TypeScript 类型。
自定义指令有两种实现形式,一种是作为一个对象,其中的写法比较接近于 Vue 组件,除了 getSSRProps 和 deep 选项外,其他的每一个属性都是一个 钩子函数。
1 | // 对象式写法的 TS 类型 |
另外一种是函数式写法,只需要定义成一个函数,但这种写法只在 mounted
和 updated
这两个钩子生效,并且触发一样的行为。
1 | // 函数式写法的 TS 类型 |
这是每个钩子函数对应的类型,它有 4 个入参:
1 | // 钩子函数的 TS 类型 |
钩子函数第二个参数的类型:
1 | // 钩子函数第二个参数的 TS 类型 |
可以看到自定义指令最核心的就是 “钩子函数” 了,接下来来了解这部分的知识点。
钩子函数
和 组件的生命周期 类似,自定义指令里的逻辑代码也有一些特殊的调用时机,在这里称之为钩子函数:
钩子函数 | 调用时机 |
---|---|
created | 在绑定元素的 attribute 或事件侦听器被应用之前调用 |
beforeMount | 当指令第一次绑定到元素并且在挂载父组件之前调用 |
mounted | 在绑定元素的父组件被挂载后调用 |
beforeUpdate | 在更新包含组件的 VNode 之前调用 |
updated | 在包含组件的 VNode 及其子组件的 VNode 更新后调用 |
beforeUnmount | 在卸载绑定元素的父组件之前调用 |
unmounted | 当指令与元素解除绑定且父组件已卸载时,只调用一次 |
因为自定义指令的默认写法是一个对象,所以在代码风格上遵循 Options API 的生命周期命名,而非 Vue 3 的 Composition API 风格。
钩子函数在用法上就是这样子:
1 | const myDirective = { |
在相关的 TS 类型]已了解,每个钩子函数都有 4 个入参:
参数 | 作用 |
---|---|
el | 指令绑定的 DOM 元素,可以直接操作它 |
binding | 一个对象数据,见下方的单独说明 |
vnode | el 对应在 Vue 里的虚拟节点信息 |
prevVNode | Update 时的上一个虚拟节点信息,仅在 beforeUpdate 和 updated 可用 |
其中用的最多是 el
和 binding
。
el
的值就是通过document.querySelector
拿到的那个 DOM 元素。binding
是一个对象,里面包含了以下属性:
属性 | 作用 |
---|---|
value | 传递给指令的值,例如 v-foo="bar" 里的 bar ,支持任意有效的 JS 表达式 |
oldValue | 指令的上一个值,仅对 beforeUpdate 和 updated 可用 |
arg | 传给指令的参数,例如 v-foo:bar 里的 bar |
modifiers | 传给指令的修饰符,例如 v-foo.bar 里的 bar |
instance | 使用指令的组件实例 |
dir | 指令定义的对象(就是上面的 const myDirective = { /* ... */ } 这个对象) |
在了解了指令的写法和参数作用之后,来看看如何注册一个自定义指令。
局部注册
自定义指令可以在单个组件内定义并使用,通过和 setup 函数]同级别的 directives
选项进行定义,可以参考下面的例子和注释:
1 | <template> |
上面是对象式的写法,也可以写成函数式:
1 | export default defineComponent({ |
TIP 局部注册的自定义指令,默认在子组件内生效,子组件内无需重新注册即可使用父组件的自定义指令。
全局注册
自定义指令也可以注册成全局,这样就无需在每个组件里定义了,只要在入口文件 main.ts
里启用它,任意组件里都可以使用自定义指令。
deep 选项
除了钩子函数,在相关的 TS 类型里还可以看到有一个 deep 选项,它是一个布尔值,作用是:
如果自定义指令用于一个有嵌套属性的对象,并且需要在嵌套属性更新的时候触发 beforeUpdate
和 updated
钩子,那么需要将这个选项设置为 true
才能够生效。
1 | <template> |
插槽
Vue 在使用子组件的时候,子组件在 template 里类似一个 HTML 标签,可以在这个子组件标签里传入任意模板代码以及 HTML 代码,这个功能就叫做 “插槽” 。
默认插槽
默认情况下,子组件使用 <slot />
标签即可渲染父组件传下来的插槽内容,例如:
在父组件这边:
1 | <template> |
在子组件这边:
1 | <template> |
默认插槽非常简单,一个 <slot />
就可以了。
具名插槽
有时候可能需要指定多个插槽,例如一个子组件里有 “标题” 、 “作者”、 “内容” 等预留区域可以显示对应的内容,这时候就需要用到具名插槽来指定不同的插槽位。
子组件通过 name
属性来指定插槽名称:
1 | <template> |
父组件通过 template
标签绑定 v-slot:name
格式的属性,来指定传入哪个插槽里:
1 | <template> |
v-slot:name
有一个别名 #name
语法,上面父组件的代码也相当于:
1 | <template> |
TIP 在使用具名插槽的时候,子组件如果不指定默认插槽,那么在具名插槽之外的内容将不会被渲染。
默认内容
可以给 slot
标签添加内容,例如 <slot>默认内容</slot>
,当父组件没有传入插槽内容时,会使用默认内容来显示,默认插槽和具名插槽均支持该功能。
注意事项
有一条规则需要记住:
- 父组件里的所有内容都是在父级作用域中编译的
- 子组件里的所有内容都是在子作用域中编译的
CSS 样式与预处理器
Vue 组件的 CSS 样式部分,Vue 3 保留着和 Vue 2 完全一样的写法。
编写组件样式表
最基础的写法,就是在 .vue
文件里添加一个 <style />
标签,即可在里面写 CSS 代码了。
1 | <template> |
动态绑定 CSS
动态绑定 CSS ,在 Vue 2 就已经存在了,在此之前常用的是 :class
和 :style
,现在在 Vue 3 ,还可以通过 v-bind
来动态修改了。
使用 :class 动态修改样式名
它是绑定在 DOM 元素上面的一个属性,跟 class="class-name"
这样的属性同级别,它非常灵活!
TIP 使用
:class
是用来动态修改样式名,也就意味着必须提前把样式名对应的样式表先写好!
假设已经提前定义好了这几个变量:
1 | <script lang="ts"> |
如果只想绑定一个单独的动态样式,可以传入一个字符串:
1 | <template> |
如果有多个动态样式,也可以传入一个数组:
1 | <template> |
还可以对动态样式做一些判断,这个时候传入一个对象:
1 | <template> |
多个判断的情况下,记得也用数组套起来:
1 | <template> |
那么什么情况下会用到 :class
呢?
最常见的场景,应该就是导航、选项卡了,比如要给一个当前选中的选项卡做一个突出高亮的状态,那么就可以使用 :class
来动态绑定一个样式。
1 | <template> |
这样就简单实现了一个点击切换选项卡高亮的功能。
使用 :style 动态修改内联样式
如果觉得使用 :class
需要提前先写样式,再去绑定样式名有点繁琐,有时候只想简简单单的修改几个样式,那么可以通过 :style
来处理。
默认的情况下,都是传入一个对象去绑定:
key
是符合 CSS 属性名的 “小驼峰式” 写法,或者套上引号的短横线分隔写法(原写法),例如在 CSS 里,定义字号是font-size
,那么需要写成fontSize
或者'font-size'
作为它的键。value
是 CSS 属性对应的 “合法值”,比如要修改字号大小,可以传入13px
、0.4rem
这种带合法单位字符串值,但不可以是13
这样的缺少单位的值,无效的 CSS 值会被过滤不渲染。
1 | <template> |
如果有些特殊场景需要绑定多套 style
,需要在 script
先定义好各自的样式变量(也是符合上面说到的那几个要求的对象),然后通过数组来传入:
1 | <template> |
使用 v-bind 动态修改 style
当然,以上两种形式都是关于 <script />
和 <template />
部分的操作,如果觉得会给模板带来一定的维护成本的话,不妨考虑这个新方案,将变量绑定到 <style />
部分去。
TIP 请注意这是一个在
3.2.0
版本之后才被归入正式队列的新功能!如果需要使用它,请确保的vue
的版本号在3.2.0
以上,最好是保持最新版本。
先来看看基本的用法:
1 | <template> |
如上面的代码,将渲染出一句红色文本的 Hello World!
这其实是利用了现代浏览器支持的 CSS 变量来实现的一个功能(所以如果打算用它的话,需要提前注意一下兼容性噢,点击查看:CSS Variables 兼容情况 )。
它渲染到 DOM 上,其实也是通过绑定 style
来实现,可以看到渲染出来的样式是:
1 | <p class="msg" data-v-7eb2bc79="" style="--7eb2bc79-fontColor:#ff0000;"> |
对应的 CSS 变成了:
1 | .msg[data-v-7eb2bc79] { |
理论上 v-bind
函数可以在 Vue 内部支持任意的 JavaScript 表达式,但由于可能包含在 CSS 标识符中无效的字符,因此官方是建议在大多数情况下,用引号括起来,如:
1 | .text { |
由于 CSS 变量的特性,因此对 CSS 响应式属性的更改不会触发模板的重新渲染(这也是和 :class
与 :style
的最大不同)。
TIP 不管有没有开启 style-scoped ,使用
v-bind
渲染出来的 CSS 变量,都会带上scoped
的随机 hash 前缀,避免样式污染(永远不会意外泄漏到子组件中),所以请放心使用!
如果对 CSS 变量的使用还不是很了解的话,可以先阅读一下相关的基础知识点。
样式表的组件作用域
CSS 不像 JS ,是没有作用域的概念的,一旦写了某个样式,直接就是全局污染。所以 BEM 命名法 等规范才应运而生。
但在 Vue 组件里,有两种方案可以避免出现这种污染问题:一个是 Vue 2 就有的 <style scoped>
,一个是 Vue 3 新推出的 <style module>
。
Style Scoped
Vue 组件在设计的时候,就想到了一个很优秀的解决方案,通过 scoped
来支持创建一个 CSS 作用域,使这部分代码只运行在这个组件渲染出来的虚拟 DOM 上。
使用方式很简单,只需要在 <style />
上添加 scoped
属性:
1 | <!-- 注意这里多了一个 `scoped` --> |
编译后,虚拟 DOM 都会带有一个 data-v-xxxxx
这样的属性,其中 xxxxx
是一个随机生成的 Hash ,同一个组件的 Hash 是相同并且唯一的:
1 | <div class="msg" data-v-7eb2bc79> |
而 CSS 则也会带上与 HTML 相同的属性,从而达到样式作用域的目的。
1 | .msg[data-v-7eb2bc79] { |
使用 scoped
可以有效的避免全局样式污染,可以在不同的组件里面都使用相同的 className,而不必担心会相互覆盖,不必再定义很长很长的样式名来防止冲突了。
TIP 添加
scoped
生成的样式,只作用于当前组件中的元素,并且权重高于全局 CSS ,可以覆盖全局样式
Style Module
这是在 Vue 3 才推出的一个新方案,和 <style scoped>
不同,scoped 是通过给 DOM 元素添加自定义属性的方式来避免冲突,而 <style module>
则更为激进,将会编译成 CSS Modules 。
对于 CSS Modules 的处理方式,也可以通过一个小例子来更直观的了解它:
1 | /* 案例来自阮一峰老师的博文《CSS Modules 用法教程》 */ |
可以看出,是通过比较 “暴力” 的方式,把编写的 “好看的” 样式名,直接改写成一个随机 Hash 样式名,来避免样式互相污染。
所以回到 Vue 这边,看看 <style module>
是怎么操作的。
1 | <template> |
于是,将渲染出一句红色文本的 Hello World!
。
TIP
- 使用这个方案,需要了解如何 使用 :class 动态修改样式名
- 如果单纯只使用
<style module>
,那么在绑定样式的时候,是默认使用$style
对象来操作的- 必须显示的指定绑定到某个样式,比如
$style.msg
,才能生效- 如果单纯的绑定
$style
,并不能得到 “把全部样式名直接绑定” 的期望结果- 如果指定的 className 是短横杆命名,比如
.user-name
,那么需要通过$style['user-name']
去绑定
也可以给 module
进行命名,然后就可以通过命名的 “变量名” 来操作:
1 | <template> |
TIP 需要注意的一点是,一旦开启
<style module>
,那么在<style module>
里所编写的样式都必须手动绑定才能生效,没有被绑定的样式虽然也会被编译,但不会主动生效到 DOM 上。
原因是编译出来的样式名已经变化,而原来的 DOM 未指定对应的样式名,或者指定的是编译前的命名,所以并不能匹配到正确的样式。
useCssModule
这是一个全新的 API ,面向在 script 部分操作 CSS Modules 。
在上面的CSS Modules部分可以知道,可以在 style
定义好样式,然后在 template
部分通过变量名来绑定样式。
那么如果有一天有个需求,需要通过 v-html
来渲染 HTML 代码,那这里的样式岂不是凉凉了?当然不会!
Vue 3 提供了一个 Composition API useCssModule
来帮助在 setup
函数里操作的 CSS Modules (对,只能在 setup或者 script setup 里使用)。
基本用法:
多绑定几个样式,再来操作:
1 | <template> |
可以看到打印出来的 style
是一个对象:
key
是在<style modules>
里定义的原始样式名value
则是编译后的新样式名
1 | { |
所以来配合 模板字符串 的使用,看看刚刚说的,要通过 v-html
渲染出来的内容应该如何绑定样式:
1 | <template> |
是不是也非常简单?可能刚开始不太习惯,但写多几次其实这个功能也蛮好玩的!
另外,需要注意的是,如果指定了 modules 的名称,那么必须传入对应的名称作为入参才可以正确拿到这些样式:
比如指定了一个 classes 作为名称:
1 | <style module="classes"> |
那么需要通过传入 classes 这个名称才能拿到样式,否则会是一个空对象:
1 | const style = useCssModule('classes') |
TIP 在
const style = useCssModule()
的时候,命名是随意的,跟在<style module="classes">
这里指定的命名没有关系。
深度操作符
使用 scoped 后,父组件的样式将不会渗透到子组件中,也不能直接修改子组件的样式。
如果确实需要修改子组件的样式,必须通过 ::v-deep
(完整写法) 或者 :deep
(快捷写法) 操作符来实现。
TIP
- 旧版的深度操作符是
>>>
、/deep/
和::v-deep
,现在>>>
和/deep/
已进入弃用阶段(虽然暂时还没完全移除)- 同时需要注意的是,旧版
::v-deep
的写法是作为组合器的方式,写在样式或者元素前面,如:::v-deep .class-name { /* ... */ }
,现在这种写法也废弃了。
现在不论是 ::v-deep
还是 :deep
,使用方法非常统一,来假设 .b 是子组件的样式名:
1 | <style scoped> |
编译后:
1 | .a[data-v-f3f3eg9] .b { |
TIP 可以看到,新的 deep 写法是作为一个类似 JS “函数” 那样去使用,需要深度操作的样式或者元素名,作为 “入参” 去传入。
同理,如果使用 Less 或者 Stylus 这种支持嵌套写法的预处理器,也是可以这样去深度操作的:
1 | .a { |
另外,除了操作子组件的样式,那些通过 v-html
创建的 DOM 内容,也不受作用域内的样式影响,也可以通过深度操作符来实现样式修改。
使用 CSS 预处理器
在工程化的现在,可以说前端都几乎不写 CSS 了,都是通过 sass
、less
、stylus
等 CSS 预处理器来完成样式的编写。
为什么要用 CSS 预处理器?放一篇关于三大预处理器的点评,新开发者可以做个简单了解,具体的用法在对应的官网上有非常详细的说明。
可以查看了解:浅谈 CSS 预处理器,Sass、Less 和 Stylus
在 Vue 组件里使用预处理器非常简单,像 Vite 已内置了对预处理器文件的支持(可处理 .less
、 .scss
之类的预处理器扩展名文件),因此只需要安装对应的依赖到项目里。
这里以 Less 为例,先安装该预处理器:
1 | # 因为是在开发阶段使用,所以添加到 `devDependencies` |
接下来在 Vue 组件里,只需要在 <style />
标签上,通过 lang="less"
属性指定使用哪个预处理器,即可直接编写对应的代码:
1 | <style lang="less" scoped> |
编译后的 css 代码:
1 | .msg { |
预处理器也支持 scoped
。
Vue Devtools
Vue Devtools 是一个浏览器扩展,支持 Chrome 、 Firefox 等浏览器,需要先安装才能使用。
点击安装:Vue Devtools 的浏览器扩展
当在 Vue 项目通过 npm run dev
等命令启动开发环境服务后,访问本地页面(如: http://localhost:3000/
),在页面上按 F12 唤起浏览器的控制台,会发现多了一个名为 vue
的面板。
面板的顶部有一个菜单可以切换不同的选项卡,菜单数量会根据不同项目有所不同,例如没有安装 Pinia 则不会出现 Pinia 选项卡,这里以其中一部分选项卡作为举例。
Components
Components 是以结构化的方式显示组件的调试信息,可以查看组件的父子关系,并检查组件的各种内部状态:
Routes
Routes 可以查看当前所在路由的配置信息:
Timeline
Timeline 是以时间线的方式追踪不同类型的数据,例如事件:
Pinia
Pinia 是可以查看当前组件引入的全局状态情况: