前言
本片笔记仅结合个人理解,记录本人所生疏/不了解的部分。
生命周期
- 初始化事件修饰符、生命周期
beforeCreate
:创建实例之前,数据代理还未开始,获取不到 dom 节点- 初始化响应式系统,数据检测、数据代理
created
:创建实例之后,methods、data 等已被绑定至 vue 实例,dom 还未生成,获取不到 dom 节点- 实例中是否有 template 选项:
- 有:使用 render 函数渲染 template
- 否:将挂载的 el 元素的 innerHtml 编译为 template
beforeMount
:组件挂载之前,虚拟 dom 已经生成,真实 dom 还未生成- 将通过 template 编译好的 html 替换至挂载的 el(如 #app)的 innerHtml,即将虚拟 dom 转化为真实 dom
mounted
:真实 dom 已经生成,模板编译已完成,页面已经展示,可以获取到 dom 节点- 当数据发生改变时:
beforeUpdate
:真实 dom 未被替换,页面未被更新,虚拟 dom 已经更新完毕- 对比新旧不同,根据不同节点,虚拟 dom 渲染为真实 dom
updated
:dom 更新之后,修改后的页面已经被展示
- 当页面被卸载时,即
app.unmount()
被调用时,即完全销毁一个实例,解绑事件,清除和其他组件的链接beforeUnmount
:还未进行销毁,data、methods 等仍可以被调用unmounted
:实例销毁之后
组合式 api
组合式 api 被使用的位置为 setup
组件选项,其组件创建之前执行,其执行位置早于 beforeCreate
。
在 vue3 中,其取代了 beforeCreate
与 created
。因此,在 setUp 方法中,不存在 beforeCreate
与 created
的钩子函数。
多个应用实例
可通过 css 选择器针对页面上的多个元素挂载不同的 vue 实例
1 | Vue.createApp( { /* 选项 */ } ).mount( "#countainer-1" ); |
这对于只想使用 vue 控制一个大型项目中的一小部分模块来说,是非常有效的。
v-bind
1 | const objectOfAttrs = { |
使用如下方式向 div 绑定 id
与 class
。
1 |
|
同名简写
attribute 的名称与 JS 变量的名称相同,可以省略 attribute 的值
1 |
|
被自动忽略的布尔型 Attr
我们知道,在 html 中,对于一些诸如 disabled
这样的 html 中的布尔型 attribute,
不论其被设置为什么值,只要该属性存在,就会被认为是 true
。
而在 vue 中,当我们为这种 attribute
绑定的值为 false
时,该属性会在最终生成的 html 中被自动忽略。
1 |
|
受限的全局访问
模板表达式的环境是被沙盒化处理的,仅能使用一些被 vue 定义允许使用的全局对象,
自行在 window
上添加的对象是不可使用的。
可以通过 app.config.globalProperties
来手动添加供模板表达式使用。
1 | // 设置一个全局变量 author,其值为 mari |
1 | <!-- 将会被渲染为 mari --> |
需要注意的是,如果通过这种方法设置的全局属性与组件自己的属性存在冲突,则以后者为准
平日以为全局变量只能在
<script>
中通过getCurrentInstance
获取,步骤繁琐还没用。原来一直都误解了(记录的原因)以及记录下在
<script>
中获取全局变量的方式
1 const authorProp = getCurrentInstance()?.appContext.config.globalProperties.author;
指令动态参数
使用 []
来将指令参数设置为表达式。
1 | <a v-bind:[attributeName]="url"> ... </a> |
动态参数存在以下限制:
- 仅允许为字符串或
null
,当为null
时,意为显式移除该绑定; - 不允许使用空格和引号等;
- 使用内嵌模板即
template
属性时,避免使用大写字符,浏览器会强制转化为小写导致出错。.vue
单文件组件不受此限制。
模板调用函数
1 | <span :title="toTitleDate(date)"> |
当在 template 中使用组件抛出的函数时,该函数不应该产生任何副作用,如改变数据或触发异步操作。
这是因为绑定在表达式中的方法在组件每次更新时都会被重新调用。
class 绑定
可以给 :class
绑定一个数组来渲染多个 CSS class,且在数组中支持以嵌套对象方式来决定是否启用指定 class。
1 |
|
style 绑定
允许为 :style
绑定一个包含多个样式对象的数组。他们将会被合并后渲染至同一个元素。
1 |
|
自动前缀
在 :style
中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
样式多值
可以对同一个样式属性以数组形式提供多个值,数组将会渲染浏览器支持的最后一个值。
1 |
|
在上面示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
v-if
v-if 是惰性的:如果在初次渲染时条件值为 false
,则不会做任何事。条件区块只有当条件首次变为 true
时才被渲染。
v-for
遍历对象
除了遍历数组外,还可以使用 v-for
对一个对象的所有属性进行遍历,此时存在三个参数:
- 参数 1:对象的键值
- 参数 2:对象的键名
- 参数 3:位置索引
遍历的顺序会基于该对象的 Object.keys()
的返回值。
1 | const AuthorInfo = reactive( { |
1 | <p v-for="( value, key, index ) in AuthorInfo">{{ index }}. {{ key }}: {{ value }}</p> |
上面的 template 将会会被渲染为:
1 | 0. name: AsukaMari |
使用范围值
v-for
可以直接接受一个整数值,此时会基于 1 ~ 给定值
的范围重复多次,与普通 v-for 遍历数组一样接收两个参数:当前值与
位置索引,当前值初始值从 1
开始而非 0
。
1 | <p v-for="( value, index ) in 10">{{ index }}. {{ value }}</p> |
替换渲染数组
存在如下直接将响应式对象的值替换为新数组的方式:
1 | // `items` 是一个数组的 ref |
此时 Vue 并不会丢弃现有的 DOM 并重新渲染整个列表。Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。
v-if 与 v-for
当这俩存在于同一个元素上时,v-if
会被首先执行。
需要注意的是,这俩同时使用是不推荐的。
响应式相关
DOM 更新时机
更改响应式状态后,DOM 会自动更新。然而并不会立即更新,而是缓冲至更新周期的 “下个时机” 以确保无论进行多次修改,每个组件都只更新一次。
若要保证 DOM 更新完成再执行操作,可以使用 nextTick()
全局 API。
深层响应式与浅层响应式
Vue 3 中,ref
与 reactive
都是默认深层响应式的,即使改变深层次的对象或数组,改动也依然能被检测到。
而针对希望使用浅层响应式的场景,可以分别使用 shallowRef
与 shallowReactive
来进行对应的包装。
1 | // 深层响应式 |
reactive()
reactive()
返回的是原始对象的 Proxy 代理对象(没错就是 ES6 的 Proxy),同时响应式对象的深层嵌套对象也依然是代理对象。
为了保证访问代理的一致性,存在如下规则:
- 当对同一个对象多次执行
reactive()
时,返回同一个 Proxy 对象; - 当对对象的 Proxy 代理对象执行
reactive()
时,返回其自身
局限性
由于 JavaScript 的引用机制,reactive
存在些许局限性。
reactive()
仅对对象类型有效,即对象、数组和 Map、Set 这样的集合类型。- 不能随意的修改对响应式对象的引用,如将响应式对象指向其他的变量、作为函数实参传入、解构赋值,均会失去原变量的响应式。
ref()
在 vue3 中,通过 ref()
方法来将变量转化为响应式对象,该对象包含一个 value
属性指向赋予响应时对象的值,它可以创建任意类型的响应式 ref,弥补了 reactive()
对类型的限制。。
这么做的用意是为了保证 JavaScript 中不同数据类型的行为统一。
我们知道 JavaScript 分为引用类型和基本类型,而对象作为引用类型,使用对象进行包装基本类型后可以安全的为其传递值而不用担心失去响应式的问题。
当 ref 对象的 .value
为对象类型时,将会用 reactive()
自动转换该值。
相比 reactive() 的优越性
一个包含对象类型值的 ref 可以响应式的替换整个对象。
1 | const objectRef = ref( { count: 0 } ); |
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
1 | const obj = { |
在 template 中的解包
当 ref 在 template 中作为顶层属性被使用时,会被自动 “解包”,不需要使用 .value
。
而对于非顶层属性,则不会被自动解包,如下面的 object:
1 | const obj = { foo: ref( 1 ) }; |
对于如下 template,不会正常工作,将得到 [object Object]1
1 | <span>{{ obj.foo + 1 }}</span> |
应手动补齐 .value
才能得到正确结果 2
1 | <span>{{ obj.foo.value + 1 }}</span> |
而当文本插值即 {{ }}
内表达式的计算最终值为一个 ref 时,它同样将会被自动解包,这是文本插值的一个遍历功能。
如对于上面的 obj,如下 template 写法等同于 {{ obj.foo.value }}
,最终渲染结果为 1
,。
1 | <span>{{ obj.foo }}</span> |
在响应式对象中的解包
当 ref 被套在响应式对象中,作为属性访问并修改时,会被自动解包,像一般属性一样不需要 .value
1 | const count = ref( 0 ); |
需要注意的是,对于数组和集合类型的响应式对象,其中的 ref 不会被自动解包。
1 | const books = reactive( [ ref( "Vue 3 Guide" ) ] ); |
toRefs
将对象解析为新的对象,该对象解构后不会失去响应式。
可以看作该方法对对象的每个属性进行了一次类似 ref()
的转化,将其转化为一个包含 value
属性的对象。
计算属性
计算属性返回了一个计算属性 ref,与其他一般的 ref 类似。
计算属性的 getter 中应该只进行计算,不应存在任何的副作用。使用会改变原数组的行为如 reverse()
和 sort()
时应创建一个原数组的副本。
1 | // 错误 |
应该避免在任何情况下修改计算属性返回值,该值应被视为已读的。
与方法的不同之处
虽然使用方法也能达到与计算属性同样的效果,但不同的是计算属性值会基于其响应式依赖被缓存。
计算属性仅会在响应式依赖发生改变时才会重新计算,否则计算属性无论访问多少次都会立刻返回先前的计算结果,不必重复执行 getter 函数。
因此,对于非响应式依赖,计算属性永远都不会更新。例如下面示例中的 Date.now()
:
1 | const now = computed( () => Date.now() ); |
相比之下,每次重渲染发生时方法总是会被再次执行。
可写的计算属性
计算属性默认是只读的,尝试修改计算属性时会收到一个运行时警告。但我们可以通过配置 computed option,提供 getter 和 setter 来创建一个可写的计算属性。
下面的案例中,fullName 在页面中的渲染将会在一秒后从 Asuka Mari
变更为 Tachibana Kanade
。
1 | const firstName = ref( "Asuka" ); |
事件处理
事件处理器分为内联事件处理器和方法事件处理器。
内联事件处理器接受一段 JavaScript 语句,与 onclick
类似。而方法事件处理器则接受一个方法名,指代对某个方法的调用。
1 | <!-- 内联事件处理器 --> |
需要注意的是,也是笔者在此记录的原因。即我们常用的向方法内传入一个参数的写法,根据写法不同,其本质为不同的事件处理器。
1 | <!-- 内联事件处理器 --> |
exact
修饰符
exact
修饰符用于确保精确匹配,即只有在精确匹配时才会触发事件。
1 | <!-- 只要按下 ctrl 时点击就能触发,不论是否同时还按有其他的键 --> |
表单输入绑定
IME(输入法)的影响
输入框等使用需要使用 IME 的语言输入时,默认 v-model
(非组件自定义) 并不会在拼字阶段触发更新。
若需要在拼字阶段触发,需要手动拆分为 value
和 @input
的绑定。
选择器未匹配时的情况
当 v-model
绑定的值未匹配到任何绑定值时,<select>
会被渲染为一个未选择的状态。而在 IOS 上,这会导致用户无法选择第一项。
此时建议提供一个空值的禁用选项。
1 | <select v-model="selected"> |
选择器选项的值
选择器的选项可以使用非字符串类型,例如引用类型
1 | <select v-model="selected"> |
当选择后,selected
会被设置为响应式对象类型 { number: 123 }
。
.lazy
修饰符
对于 <input>
输入框,默认情况下可以理解(因为 IME 拼字阶段并不会触发)其绑定的事件为 @input
,即每次输入均会触发。
使用 .lazy
修饰符标记将其改为 @change
事件触发,这样将会在输入完毕失去焦点的时候更新数据。
1 | <input v-model.lazy="msg" /> |
侦听器
watch
watch 第一个参数为监听数据源,允许的类型为:ref(包括计算属性)、响应式对象、getter 函数,多个数据源组成的数组。
1 | const x = ref( 0 ); |
需要注意的是,对于响应式对象的属性值,仅当该属性值为对象时可以直接作为数据源监听。否则即使其被 ref
包装也无法用于 watch 监听,因为 ref
在 reactive
中会被自动解包为普通类型。
1 | const authorInfo = reactive( { |
而对于非对象的属性值,应使用 getter 的方式进行监听。
1 | watch( () => authorInfo.age, () => { |
深层监听
深层监听即:该回调函数在所有嵌套的变更时都会被触发。
当 watch 侦听的数据源为响应对象时,将会隐式的创建一个深层侦听器。
1 | const obj = reactive( { count: 0 } ); |
而当数据源为 getter 函数时,则只有在返回不同对象时才会触发回调,为浅层监听。此时若希望使用深层监听,需要开启 deep
选项。
1 | // 浅层监听 |
由于深层监听需要遍历侦听对象中的所有嵌套属性,因此开销较大,若无必要需求请尽量使用 getter 的方式来实现浅层监听。
watchEffect
如果我们只是想监控所用到的响应式对象,当状态发生变化时进行一些副作用操作,且在初次加载时就执行一次(这是很常见的一个需求),那使用 watch
来实现的话可能会有些繁琐了。
尤其在监听多个响应式对象时,我们可能会不得不写成如下几种方式。
1 | watch( [ v1, v2 ], () => { |
此时我们可以使用 watchEffect
来简化这个需求:
1 | watchEffect( () => { |
watchEffect
类似计算属性,只要回调函数内使用到的响应式对象发生变化,就会触发一次回调函数。且在组件初始化时会立即执行一次。
获取监听的对象具体属性值的新旧值
watch 在监听中存在如下情况:
1 | const state = reactive( { test: 1 } ); |
一秒后页面上打印结果如下:
1 | Proxy {test: 2} Proxy {test: 2} |
可以见到,新旧两值是完全相同的,watch 并不能对具体的属性值做出新旧值的记录,这时候可以使用 watchEffect
。
对于回调函数,watchEffect
存在一个参数,该参数为一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前与侦听器被清除时被调用。
1 | const state = reactive( { test: 1 } ); |
以上 demo 执行结果为
1 | Proxy {test: 1} |
回调的触发时机
watch
与 watchEffect
均有对此的可选配置项。
默认情况下,被创建的侦听器回调都会在当前 Vue 组件更新之前被调用,此时访问的 DOM 将是被 Vue 更新之前的状态。
此行为可通过修改可选配置项中的 flush
进行修改,默认情况下,该值为 pre
。
1 | watch( source, callback, { |
而无论是 pre
还是 post
,watch 均会等待到下一个渲染周期(即 nextTick
被调用之前)执行回调函数,来起到效率提高与防抖的作用。
对于如下示例,控制台中将会仅打印一次 2
。
1 | const a = ref( 0 ); |
若期望在任何更新之前均同步触发回调函数,则可以使用 flush
的 "sync"
可选值。不过由于这样影响效率并失去了防抖效果,通常情况下 vue 不建议使用此可选值。
对于 post
与 sync
,均有一个别名 api 可以直接使用:
1 | watchPost( source, callback ); |
watch 执行顺序
对于如下示例:
1 | const [ a, b, c ] = [ ref( 0 ), ref( 0 ), ref( 0 ) ]; |
可以看到,终端打印顺序为 c b a
。而使用 post
模式时,打印顺序又变为了正常的 a b c
。
先打一个 todo 在这里,具体原因需要翻阅源码查看
停止侦听器
在 setup
中使用同步语句创建的侦听器,会自动绑定到组件实例上,并在组件卸载时自动停止,因此无需关心怎样去停止它。
而当侦听器在异步回调中创建时,则不会绑定到当前组件上,为了防止内存泄漏,必须要手动停止它。
1 | import { watchEffect } from 'vue' |
使用 watch
与 watchEffect
返回的函数来手动停止侦听器
1 | const unwatch = watchEffect( () => { |
DOM 内编写模板
vue 在获取模板内容时,存在三种方式:单文件组件、字符串模板、原生 DOM 模板,这里主要讨论第三种。
当根组件没有设置 template
选项时,Vue 会自动使用容器的 innerHTML
作为模板,这就是 DOM 内模板。
1 |
|
组件相关
组件命名与书写规范
默认情况下,vue 建议使用 PascalCase
形式来命名组件,这样可以与 HTML 元素区分开来。但在 DOM 中编写模板 时,则不得不遵守一些 HTML 的规则,其主要集中在三点:
不区分大小写
HTML 不区分大小写,若使用 PascalCase
或 camelCase
会因为无法匹配导致无法渲染组件,因此在模板中需要使用 kebab-case
形式来命名组件与 props。
1 | <!-- 错误 --> |
不允许省略闭合标签
HTML 只允许一小部分特殊元素省略其关闭标签,因此模板中组件必须要手动添加闭合标签
1 | <!-- 错误 --> |
限制元素位置
HTML 存在元素位置限制,例如 table
元素只能包含 tr
元素,此时直接使用组件代替 tr
的位置会导致无法渲染。
此时可以使用 is
属性结合 vue:组件名
的方式来解决这个问题。
1 | <!-- 错误 --> |
只有在 DOM 模板中才需要添加
vue:
前缀,正常单文件组件模板与字符串模板中不需要
对于注册组件的组件名时,当并未使用 DOM 模板 时, 推荐使用 PascalCase
,这代表这并非是一个 HTML 组件。
下面将仅针对组合式 api 的 setup
语法糖进行整理,需要注意的是 <script setup>
不能和 src
attribute 一起使用。
组件注册
组件其实就是一个对象或者单文件组件,通过 App.component()
可以被注册为全局组件,而通过 components
选项可以注册为局部组件。
1 | const App = { |
模板引用 ref
v-for 中的模板引用
在 v-for
中使用模板引用 ref 时,对应 ref 的值为一个数组,其包含列表中的所有元素。
但需要注意的是,该数组并不能保证与 v-for 所遍历的数据保持一致的顺序。
函数模板引用
模板引用 ref 的值允许为一个函数,此时需要使用动态的 v-bind
绑定即 :ref
,该函数将在被绑定的元素每次更新与被卸载时调用。
函数接收一个参数,即被绑定的 DOM 元素(不是 vNode
),当元素被卸载触发时,参数值为 null
。参考如下示例:
1 | <template> |
将会间隔一秒循环打印如下结果:
1 | <div>测试</div> |
组件 ref
与 vue2
不同的是,当组件使用了 setup
语法糖时,组件是默认私有的,此时父组件无法通过模板引用 ref 来访问到子组件的任何东西。
此时需要子组件通过 defineExpose
宏显式抛出,这类宏函数不需要显式引入,官网示例。
props
声明
在 setup 语法糖中,使用 defineProps
宏来进行 props 的声明,其参数与选项式 api 的 props 参数没有区别。该宏函数只能在 <script setup>
的顶级作用域下使用。
defineProps
宏函数默认支持自动通过参数推导类型,此时被称为运行时声明。
1 | const props = defineProps( { |
可以通过泛型来更直接精确的为 props 定义类型,这被称为类型声明:
1 | const props = defineProps<{ |
该类型定义可以使用现成的类型接口,但需要注意的是,暂时不支持外部导入类型的使用。
3.3 版本后开始支持外部类型导入,但不支持对类型整体的条件判定/泛型等复杂类型
1 | // 正确 |
基于类型的声明并不拥有为 props 设定默认值的能力,此时需要额外的 withDefaults
宏来实现此能力:
1 | interface IProps { |
注:运行时声明与类型声明只能两者取其一,同时使用会报错,后面的 emits 的声明同理。
传递
对于 props 的传递,使用 PascalCase 并没有多大优势,为了和 HTML attribute 对齐,通常会将其写为 kebab-case 形式。
1 |
|
props 允许使用对象的方式来一次性传递多个 props,例如有如下数据:
1 | const author = { |
下面两种渲染方式是完全等价的:
1 |
|
emits 声明
在 setup 语法糖中,使用 defineEmits
宏来进行 props 的声明,其同样存在 运行时声明
与 类型声明
两种方式。该宏函数只能在 <script setup>
的顶级作用域下使用。
1 | // 运行时声明 |
props 与 emits 的名称格式转换
对于 v-on
和 v-bind
绑定的名称,vue 会自动将其转换为 camelCase
形式,因此即使子组件中定义的是 camelCase
形式的名称,父组件中也可以使用 kebab-case
形式来绑定。
1 | <!-- 父组件 --> |
v-model
默认情况下,v-model
在组件上使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件。在自定义组件上使用 v-model 时,将会被解析为如下形式:
1 |
|
可以通过给 v-model
指定参数来更改默认行为:
1 |
|
介于指定参数的特性,我们得以为单个组件使用多个 v-model
,彼此之前不会发生冲突:
1 | <MariPost v-model="value" v-model:name="authorName" v-model:age="authorAge" /> |
自定义 v-model 修饰符
可以为 v-model 指定自定义修饰符,例如指定 .upper
来将值自动转为大写:
1 | <MariPost v-model.upper="value" /> |
它将会为组件传递一个名为 modelModifiers
的 props,其默认值为一个空对象。此时在该对象中存在 upper
属性,值为 true
。
1 | const props = withDefaults( defineProps<{ |
我们可以根据此值来进行一些我们所需要的操作,如这里的自动转为大写:
1 | <script setup> |
而对于 v-model 指定了参数如 v-model:name.upper
,自动生成的 props 名称将从 modelModifiers
改为 arg + "Modifiers"
,在这里为 nameModifiers
。
1 | const props = withDefaults( defineProps<{ |
defineModel
鉴于上面的实现方式过于复杂,从 Vue 3.4 版本开始,Vue 提供了 defineModel()
宏来便捷的实现 v-model
。
本质上还是 moduleValue
与 update:modelValue
那一套的语法糖,其使用方式与 3.4- 的对照如下:
1 | // 声明 |
而当我们期望其接受一个参数时,实现也非常简单:
1 | // 组件上可以直接使用 v-model:name="" |
defineModel
还支持接受一个配置对象,该对象为 PropOptions
(props
配置项) 与 DefineModelOptions
(get
和 set
函数) 两个类型的集合。
其中 get
和 set
函数与常规取值函数完全一致:读 model 值时触发 get()
,修改 model 值时触发 set()
。
手动修改
get()
的返回值仅会影响读取获取的值,并不会影响到model
本身的值。
1 | const val = defineModel<string>( { |
而对于自定义 v-model 修饰符,可以通过对 defineModel
的返回值进行解构。
其中我们可以通过TS 泛型的第二个参数来标明可选的修饰符,多个可选修饰符使用联合类型标记:
1 | const [ val, modifiers ] = defineModel<string, "upper" | "lower">(); |
为此,我们可以很容易的简化 自定义 v-model 修饰符 中将输入值转为大写的例子:
1 | // 二十多行代码简化成五行,赞美 |
透传 Attributes
单根元素组件
对于以单个元素为根节点渲染的组件,当为它传递其并未声明为 props
或 emits
的属性时,
将会被直接添加到根元素上,这就是属性透传。属性如 style
、class
、placeholder
等,v-on
事件同样支持透传。
对于 style
和 class
,当子组件已存在该值时,将会与父组件传递的值合并。
当子组件 A 根节点为另一个子组件 B 时,将会继续透传至 B 子组件。需要注意的是,若 A 中以 props
或 emits
的形式声明了该属性,则该属性会被 A 吃掉
,不会继续向 B 透传。
禁用透传
在 vue 3.3 之前,可以通过设置 defineComponent()
所接受对象的 inheritAttrs
为 false
来禁用透传。
而由于 <script setup>
语法糖不需要使用 defineComponent()
,啧需要额外定义一个 script
标签来进行此属性的声明,同时这两个 <script>
标签必须使用相同的语言,如 lang="ts"
。
1 | <script lang="ts"> |
而在 3.3 之后,我们可以直接使用 defineOptions
宏来进行声明。
1 | <script lang="ts" setup> |
多根元素组件
对于存在多个根元素的组件,不会自动触发属性透传。此时若传递了未声明的 props
或 emits
,将会得到一个 vue 的警告:
1 | [Vue warn]: Extraneous non-props attributes (placeholder) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. |
使用 $attrs
指定透传位置
当自动透传被禁止时(inheritAttrs: false
或为多根元素组件),可以使用 $attrs
来自行指定透传位置。
1 |
|
此时当多根元素组件传递未声明的 props
或 emits
时,不会出现警告。
这个 $attrs
包含了除手动声明的 props
与 emits
之外的所有的被附加在组件上的 attribute,包括 v-on
监听器。
有几点需要注意一下:
$attrs
中保留了透传 attr 的原始大小写,像author-name
这样的 props 只能使用$attrs['author-name']
来获取,这点与组件 props 不同- 对于诸如
@click
这样的v-on
绑定的事件,被暴露为$attrs
上的一个名为onClick
的属性,其他事件同理。
在 JavaScript 中访问透传属性
在 <script setup>
中可以使用 useAttrs()
来访问所有的透传属性。
1 | import { computed, useAttrs } from "vue"; |
或在 setup()
的上下文对象中获取。
1 | import { defineComponent } from "vue"; |
需要注意的是,为了节省性能,
attrs
并不是响应式的。
插槽 slot
具名插槽
对于具名插槽,需要使用一个含指定名称 v-slot
指令的 <template>
标签,默认插槽无需使用。
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。如下:
1 | <MariPost> |
条件插槽
vue3 提供了一个表示父组件所传入的插槽的对象:$slots,其类型定义如下:
1 | interface ComponentPublicInstance { |
对象键值仅包含父组件所使用到的插槽名,借此,我们可以结合 v-if
来实现根据插槽是否存在来渲染对应内容。
1 | <template> |
动态插槽名
插槽允许设置动态指令参数,格式为:
1 | <MariPost> |
插槽上的 name
是一个保留属性,不会作为 props 传递给插槽。下面的具名作用域插槽实际接收到的内容为 { author: "AsukaMari" }
1 | <slot name="header" author="AsukaMari"></slot> |
defineSlots
通过 defineSlots()
可以对组建的插槽手动定义 TypeScript 类型,包括所有插槽的名称、作用域插槽所提供的 props 内容。
其接受一个类型字面量。字面量键名为插槽 name
;字面量键值为一个函数类型,接受一个 props
参数,用于标记所提供的 props 内容。
其返回值与手动调用 useSlot() 的结果一致。
1 | <!-- 子组件 --> |
使用 defineSlots()
注明类型后,在子组件插槽中定义 props
与在父组件中使用时均会获得类型提示。
插槽复用
同一个 name
的插槽可以多次使用,并支持传入不同的作用域属性。
1 | <!-- 子组件 MariPost --> |
依赖注入
在跨组件通讯时,若传递的组件层数过多,操作会变得非常繁琐且难以维护。为此 vue 提供了 provide
和 inject
两个 api 解决这个问题。
1 | // 父级组件 |
后代组件使用 inject
时,必须确保相关 key 值的 provide
已经被调用,这就要求两个组件必须为祖先后代关系。
默认值
当父级组件未提供 provie()
时,子组件直接通过 inject()
尝试获取会得到一个 vue 警告:
1 | [Vue warn]: injection "xxx" not found. |
此时我们可以通过为 inject()
设置一个默认值来解决这个问题。
1 | const data = inject( "provide-data", "default-value" ); |
有的时候,我们需要提供一个诸如实例化类的默认值。此时即时该默认值未被使用到,也会不可避免地进行一次实例化,造成性能损耗。
这时我们可以使用工厂函数来创建默认值,此时需要传递第三个参数为 true
,即告诉 inject()
默认值为一个工厂函数,否则会被当作普通方法处理。
1 | const data = inject( "provide-data", () => new Data(), true ); |
禁止修改
尽管后代组件是无法修改通过 inject()
获取到的值的,但如果 provide()
传递的是一个引用类型的值(对象数组、响应式对象),此时后代组件扔然可以修改其中的属性。
1 | // 父级组件 |
上面例子可以发现,data
被后代组件从 { name: "mari" }
修改为了 { name: "seto" }
。为了避免这种情况,
我们可以使用 readonly
来防止修改。
1 | import { readonly, provide } from "vue"; |
此时后代组件再尝试修改属性是,会得到一个 vue 警告:
1 | [Vue warn] Set operation on key "name" failed: target is readonly. |
TypeScript 类型标记
Vue 提供了一个 InjectionKey
类型来供我们标记 provide()
与 inject()
的 key 值的类型。
使用这个类型的 key 值被提供给 provide()
与 inject()
使用时,将会获得相应的类型提示与限制。
1 | // provideType.ts |
该类型同样会对 inject()
的默认值做出约束,当 inject()
提供默认值时,得到的数据类型中将不再包含 undefined
1 | // 后代组件 |
需要注意的是,处于未知原因,目前并不会对工厂函数方式的默认值进行校验
当使用字符串类型的 key 时,需要手动通过泛型的方式为 inject()
定义类型:
1 | // 祖先组件 |
异步组件
开发中我们可能会遇到一些诸如从远程获取的组件,vue 提供了 defineAsyncComponent()
来实现这种异步组件的加载。
1 | const AsyncComp = defineAsyncComponent( () => { |
我们可以通过定时器来模拟一个异步组件的导入,例如下面的示例中,组件 AsyncComp
将在五秒后被渲染到页面上:
1 | <script setup> |
利用这个特性,我们还可以用来实现组件的按需加载,每个模块只有用到它了才会加载,避免无效性能开销。
我们可以结合 import()
返回 Promise 对象的特点来实现这个功能,
1 | const AsyncComp = defineAsyncComponent( () => import( "@/views/login/AsyncComp.vue" ) ); |
加载与错误状态
既然是异步,那就永远都离不开等待与潜在的报错问题。
事实上,为了处理这两种情况 defineAsyncComponent()
提供了更高级的可选配置项:
1 | import { defineAsyncComponent } from "vue"; |
为此我编写了一个小案例,项目演示地址
组合式函数
组合式函数说白了就是拆分出来的公共逻辑,但可以使用 vue 的响应式 api,并只允许在 setup
中使用。
其有几个约定规范:
- 命名:使用
useXXX
的格式命名 - 参数:可以接收参数,包括响应式参数。为此可以使用 toValue() 来处理传递进来的参数。
- 返回值:始终返回一个包含多个
ref
的非响应式对象
自定义指令
vue 官方建议:仅在所需功能只能使用 DOM 操作来实现时才使用自定义指令,来获得更高效、对 SSR 更友好的应用。
注册
在 <script setup>
内部时,只要是以 v
开头的驼峰式命名的变量,均可以被用作一个自定义指令
1 | const vFocus = { |
而在不能使用 <script setup>
时,例如选项式 api 写法时,则需要使用 directives
选项来注册:
1 | export default { |
在应用层级上全局注册也是一种常见的需求:
1 | const app = createApp( App ); |
对于以上三种方式,均可以直接在模板上使用 v-focus
指令,来实现元素的自动聚焦效果:
1 | <template> |
简写
通常情况下,自定义指令仅需要在 mounted
和 updated
两个钩子上实现相同行为,其他钩子并不需要。
如果满足这样的需求,可以使用自定义指令的简写方式:
1 | // 原写法 |
在组件上使用
在组件上使用时,与透传 attributes 类似,会被直接用于根节点。但自定义指令并没有 $attrs
那样的可以自行指定作用区域的对象。
因此自定义指令不能被应用到多根元素的组件,此时指令会被忽略,且 vue 会抛出一个警告。
api 扫盲
isRef()
接收一个任意参数,返回 boolean
类型。用于检验目标是否是 ref
。
1 | isRef( ref( "test" ) ); // true |
由于其返回值是一个 is
类型判定,因此可用作类型过滤。
unref()
参数是 ref,就返回它的 value 值,反之返回本体。其实就是针对下面操作的一个语法糖:
1 | value = isRef( refValue ) ? refValue.value : refValue; |
toValue()
与 unref() 类似,但 toValue()
还会规范化 getter 函数。
当接受一个 getter 函数时,toValue()
会将其执行并返回结果,若返回结果为响应式对象,toValue()
并不会将其解构。
1 | toValue( ref( "test" ) ); // "test" |
性能优化
Props 稳定性
在 Vue 中,子组件只会在其至少一个 props被改变时才会发生更新。例如有以下示例:
1 | <ListItem |
在这个示例中,它使用了 id
和 active-id
来让组件在内部自行判断自己是否为当前激活项。这样固然没有问题,且从开发层面上是比较直观的。
但这样会导致每次 activeId
改变时,所有的 ListItem
的 props 都发生了变化,导致所有 ListItem
都会重新渲染更新。
而理想情况下我们只需要让当前激活项重新渲染,因此我们可以略微修改,仅让当前激活项的 props 发生变化:
1 | <ListItem |
这样使得状态比对逻辑被移入了父组件,牺牲了一定的开发和可读性体验,但换来了更好的性能。
v-once
对于上面这种情况,有一些元素自身依赖运行时数据,但它后续无需再进行更新。此时可以使用 v-once
指令修饰,被修饰的整个子树都将会在未来的更新中被跳过。
1 | <!-- 元素 --> |