1 模板语法
Vue使用基于 HTML 的模板语法,能声明式地将其组件实例的数据绑定到DOM。所有Vue 模板可以被符合规范的浏览器和 HTML 解析器解析。Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。也支持使用JSX手写渲染函数,但不会享受到和模板同等级别的编译时优化。
Vue在所有的数据绑定中都支持完整的 JavaScript 表达式,表达式都会被作为 JavaScript,以当前组件实例为作用域解析执行。每个绑定仅支持单一表达式,即一段能够被求值的 JavaScript 代码,一个简单的判断方法是是否可以合法地写在 return 后面。因此,可以在绑定的表达式中使用组件暴露的方法,但绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。
模板中的表达式将被沙盒化,仅能够访问到暴露的有限的常用内置全局对象列表。然而可以自行在app.config.globalProperties上显式地添加它们,供所有的Vue表达式使用。
模板数据绑定形式:
- 最基本的数据绑定形式是文本插值,“Mustache”语法(即双大括号)。双大括号标签会被替换为对应组件实例的属性值,且被解析为纯文本,同时每次属性更改时它也会同步更新。
- Vue指令(以v-开头的特殊attribute)attribute绑定,其中响应式绑定元素的Attribute使用v-bind指令(简写为“ :”),该指令指示Vue将元素的attribute与组件的属性值保持一致:
- 布尔型attribute依据true / false值来决定attribute是否应该存在于该元素上。当对应的组件的属性值是真值或一个空字符串时,元素会包含这个attribute,而当其为其他假值时attribute将被忽略。
- 如果绑定的值是null或者undefined,那么该attribute将会从渲染的元素上移除。
- 动态绑定多个值,通过不带参数(即冒号与等号之间)的v-bind将一个包含多个attribute的 JavaScript 对象绑定到单个元素上。
2 指令
指令attribute的期望值为一个 JavaScript 表达式(v-for、v-on 和 v-slot例外)。一个指令的任务是在其表达式的值变化时响应式地更新DOM。某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。指令参数上也可以使用一个JavaScript表达式包含在一对方括号内作为动态参数,它作为JavaScript表达式被动态执行,计算得到的值会被用作最终的参数:
- 动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。
- 动态参数表达式存在语法限制,比如空格和引号在HTML attribute名称中都是不合法的,如果需要传入一个复杂的动态参数,推荐使用计算属性替换复杂的表达式。
- 当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写。单文件组件内的模板不受此限制。
修饰符是指令参数中以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。.prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault()。
2.1 v-html 和 v-text
v-html 指令,更新元素的 innerHTML,期望的绑定值类型为string。
若想插入 HTML需要使用 v-html 指令,在当前组件实例上,将元素的 innerHTML 与 v-html对应的属性保持同步。不能使用 v-html 来拼接组合模板,因为 Vue 不是一个基于字符串的模板引擎。在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元。在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容。
2.2 v-if 和 v-show
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。v-else-if 提供的是相应于 v-if 的“else if 区块”。它可以连续多次重复使用。也可以使用 v-else 为 v-if 添加一个“else 区块”。一个 v-else 或v-else-if元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别。可以在<template>元素上使用 v-if/v-else-if/v-else,该<template>元素作为多个元素的包装器,且不包含在最终渲染的结果中。v-if 是真正的条件渲染,是惰性的,直到条件第一次为真才会渲染条件块,条件为假则条件块内的事件监听器和子组件被销毁,存在更高的切换开销,所以适合运行时条件切换少的场景。
v-show用法基本类似,不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性,且不能在<template>上使用。v-show 实际是基于CSS的display属性进行切换,无论初始条件如何都会渲染,更高的初始渲染开销,所以适合条件切换频繁的场景。
2.3 v-if 和 v-for
v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名。v-for 也支持使用可选的第二个参数表示当前项的位置索引(v-for="(item, index) in items")。每个 v-for 作用域都可以访问到父级作用域。也可以使用 of 作为分隔符来替代 in。也可以使用 v-for 来遍历一个对象的所有属性,遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定,三个参数分别是属性值,属性名,位置索引。v-for 可以直接接受一个整数值num,会将该模板基于 1...num 的取值范围重复多次。也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。
Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。为了使得可以跟踪每个节点,从而重用和重新排序现有的元素,需要为每个元素对应的块提供一个唯一的 key attribute。当使用 <template v-for> 时,key 应该被放置在<template> 容器上。推荐在任何可行的时候为 v-for 提供一个 key attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者有意采用默认行为来提高性能。key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key。
可以直接在组件上使用 v-for,也需要提供key。但是,这不会自动将任何数据传递给组件,因为组件有自己独立的作用域。为了将迭代后的数据传递到组件中,还需要传递 props。不自动将 item 注入组件的原因是,这会使组件与 v-for 的工作方式紧密耦合。明确其数据的来源可以使组件在其他情况下重用。
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:push、pop、shift、unshift、splice、sort、reverse。变更方法,就是会对调用它们的原数组进行变更。而对于非变更方法filter、concat 、slice,即不会更改原数组,而总是返回一个新数组,需要将旧的数组替换为新的。由于Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。
如果需要显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据,则可以创建返回已过滤或已排序数组的计算属性。在计算属性不可行的情况下 (例如在多层嵌套的 v-for 循环中),可以使用methods方法操作中间态。在计算属性中使用 reverse() 和 sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本。
同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高,Vue3中则相反,v-for 的优先级比 v-if 高。这意味着在 Vue2 中 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名,解决办法是在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 。
<template v-for="todo in todos"><li v-if="!todo.isComplete"> {{ todo.name }} </li> </template>
但如果只有少部分因为 v-if 而不渲染,容易浪费 v-if的计算,因此最佳实践是先使用计算属性 computed 把data处理好,再用 v-for 渲染已处理好的。
3 样式设置
在 Vue 中,class 和 style 表达式的类型可以是字符串、对象或数组。:class(或:style)指令和一般的 class(或style) attribute 共存。在组件上同样支持字符串、数组和数组形式,使用时,组件上传递的会被添加到组件的根元素上且与该元素上已有的合并。如果组件有多个根元素,而组件的$attrs属性的$attrs.class(style)对应于组件上传递class(style),然后在组件内部指定某个根元素上:class进行接收即可。
字符串形式可以给class(style)传递字符串字面量(同HTML的内联),或者通过:class(:style)传递结果为字符串的表达式。
3.1 对象或数组形式绑定 class
对象形式通过给:class (v-bind:class 的缩写) 传递一个对象来动态切换 class,对象的属性名对应于class名,对象的属性值的真假值对应于该class名是否存在,对象既可以写成内联字面量的形式,也可以直接绑定一个对象的名字,或者一个返回对象的计算属性。
数组形式通过给:class绑定一个数组来渲染多个 CSS class,数组中的每个元素可以是对象形式或字符串形式。
3.2 对象或数组形式绑定 style
对象形式通过给:style(v-bind:style 的缩写) 传递一个对象来绑定style,对象的属性键值对分别对应于CSS的属性键值对,其中属性名推荐使用camelCase形式,也可以使用 kebab-cased 形式(即CSS 中的实际名称),对象的属性值对应于CSS属性对的属性值,属性值可以是由多个值构成的数组,仅会渲染浏览器支持的数组中最后一个值。对象既可以写成内联字面量的形式,也可以直接绑定一个对象的名字,或者一个返回对象的计算属性。
数组形式通过给:style绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上。
当在:style中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他它们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
4 事件处理
使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:eventName="handler" 或 @eventName="handler"。事件处理器handler的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似),包括对某个方法的调用。如果需要在内联事件处理器中访问事件参数,可以直接向调用某个方法时直接向该方法传入$event变量,或者使用内联箭头函数(第一个参数即事件参数),
- 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。方法事件处理器会自动接收原生 DOM 事件并触发执行。
模板编译器会通过检查v-on的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。foo() 和 count++ 会被视为内联事件处理器;foo、foo.bar 和 foo['bar'] 会被视为方法事件处理器。
为了使得事件处理器方法能更专注于数据逻辑而不用去处理 DOM 事件的细节,Vue 为v-on提供了用.表示的指令后缀的事件修饰符,包
- .stop 事件将停止捕获和冒泡的传递。
- .prevent 阻止事件默认行为。
- .self 仅当 event.target 是元素本身时才会触发事件处理器。
- .capture添加事件监听器时,使用 `capture` 捕获模式,与原生 addEventListener 事件相对应。
- .once 事件最多被触发一次。
- .passive。事件的默认行为将立即发生而非等待事件处理器执行完成,以防其中包含 event.preventDefault()。一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。请勿同时使用 .passive 和 .prevent,因为 .passive 已经向浏览器表明了不想阻止事件的默认行为。如果这么做了,则 .prevent 会被忽略,并且浏览器会抛出警告。
- 事件修饰符可以使用链式书写,但需要注意调用顺序,因为相关代码是以相同的顺序生成的。比如,@click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为,然后再是元素本身时才会触发事件处理器。而 @click.self.prevent 则先是元素本身时才会触发事件处理器,再是只会阻止对元素本身的点击事件的默认行为。
同样,Vue 允许在 v-on 或 @ 监听按键事件时添加按键修饰符,约束特定按键才触发键盘事件。可以直接使用 KeyboardEvent.key 暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。按键修饰符也可以使用链式书写。Vue 为一些常用的按键提供了别名:
- .enter
- .tab
- .delete (捕获“Delete”和“Backspace”两个按键)
- .esc
- .space
- .up
- .down
- .left
- .right
系统按键修饰符如下:
- ctrl
- .alt
- .shift
- .meta;在 Mac 键盘上,meta 是 Command 键 (⌘)。在 Windows 键盘上,meta 键是 Windows 键 (⊞)。在 Sun 微机系统键盘上,meta 是钻石键 (◆)。在某些键盘上,特别是 MIT 和 Lisp 机器的键盘及其后代版本的键盘,如 Knight 键盘,space-cadet 键盘,meta 都被标记为“META”。在 Symbolics 键盘上,meta 也被标识为“META”或“Meta”。
请注意,系统按键修饰符和常规按键不同。与 keyup 事件一起使用时,该按键必须在事件发出时处于按下状态,比如,keyup.ctrl 只会在仍然按住 ctrl 但松开了另一个键时被触发。若你单独松开 ctrl 键将不会触发。
.exact 修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。比如@click.exact仅当没有按下任何系统按键时触发;@click.ctrl.exact仅当按下 Ctrl 且未按任何其他键时才会触发。@click.ctrl当按下 Ctrl 时,即使同时还按下 Alt 或 Shift 也会触发。
鼠标按键修饰符将处理程序限定为由特定鼠标按键触发的事件,如下:
- .left
- .right
- .middle
5 表单输入绑定
为了避免在前端处理表单时手动将表单输入框的内容与JavaScript的值手动连接绑定和更改事件监听器的麻烦,Vue提供了v-model指令。
v-model可用于各种不同类型的输入,包括 <input>、<textarea>、<select> 元素。v-model 会忽略任何表单元素上初始的 value、checked 或 selected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。应该在 JavaScript 中使用data 选项来声明该初始值。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
1. 文本类型的 <input> 和 <textarea> 元素会绑定 value property 并侦听 input 事件;注意在 <textarea> 中是不支持插值表达式的,而是应该使用 v-model 来替代。
2. <input type="checkbox"> 和 <input type="radio"> 会绑定 checked property 并侦听 change 事件;
3. <select> 会绑定 value property 并侦听 change 事件。如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此建议提供一个空值的禁用选项。<select>的option支持使用 v-for 动态渲染。
v-model也支持布尔值、数组或集合值,可将多个复选框绑定到同一数组或集合值,或将多选 <select> 的值绑定到一个数组。
对于单选按钮,复选框和选择器选项,v-model 绑定的值通常是静态的字符串 (或者对复选框是布尔值),可以通过使用 v-bind 来实现将值绑定到当前组件实例上的动态数据以及将选项值绑定为非字符串的数据类型。
复选框中,true-value 和 false-value 是 Vue 特有的 attributes,仅支持和 v-model 配套使用。v-model的值会在复选框选中(未选中)时设置为true-value(false-value) attributes对应的值,同时true-value 和 false-value支持v-bind来绑定复杂的值。true-value 和 false-value attributes 不会影响 value attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个属性对应的值的其中之一被表单提交,请使用单选按钮作为替代。
单选按钮中,:value绑定的值将在选中的时候被赋值给v-model值。
对于需要使用IME的语言 (中文,日文和韩文等),v-model不会在IME输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用 v-model。
修饰符:
- .lazy;默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。添加 lazy 修饰符将改为在每次 change 事件后更新数据。
- .number;v-model 后添加 .number 修饰符让用户输入自动转换为数字。如果该值无法被 parseFloat() 处理,那么将返回原始值。该修饰符会在输入框有 type="number" 时自动启用。
- .trim;v-model 后添加 .trim 修饰符将默认自动去除用户输入内容中两端的空格。
v-model 可以在组件上使用以实现双向绑定。默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件,组件上v-model可以通过指定一个参数来更prop名称和事件名称,而且通过这种指定参数的方式可以在单个组件实例上创建多个 v-model 双向绑定。组件的 v-model 上所添加的自定义修饰符,可以通过 modelModifiers prop 在组件内访问到,modelModifiers prop 的默认值是一个空对象,当修饰符被组件上的v-model使用时,修饰符作为modeModifiers 对象的一个属性且其值为 true。对于又有参数arg又有修饰符的 v-model 绑定,生成的 prop 名将是 arg + "Modifiers"。在组件上使用v-model需要在组件内通过其中一种方式实现:
(1)将内部原生 <input> 元素的 value attribute 绑定到 modelValue prop,当原生的 input 事件触发一个传递新值作为参数的 update:modelValue 自定义事件。
(2)使用一个可写的,同时具有 getter 和 setter 的 computed 属性绑定到组件内的原生 <input> 元素的v-model上。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件。
6 生命周期
每个Vue组件实例在创建时都需要经历设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM的一系列初始化步骤,并在每个阶段运行称为生命周期钩子的函数,breforeCreate和created,beforeMount和mounted,beforeUpdate和updated,beforeUnmount和unmounted,让开发者有机会在对应的特定阶段运行自己的代码。所有生命周期钩子函数的 this 上下文都会自动指向当前调用它的组件实例,但是,避免用箭头函数来定义生命周期钩子,因为如果这样的话你将无法在函数中通过 this 获取组件实例。
Vue实例里的成员分成两大部分,模版可以直接使用vue实例中的成员:
- 自带的成员:$开头(一些实用方法和属性,便于开发者使用)和下划线_开头 (vue内部使用的成员,不建议开发者使用)。
- 从配置项(props、 data、methods、computed)中注入的成员。自带的成员要用$或者_开头是为了防止与注入的成员重名。
实例生命周期的图表:
7 模板引用(ref)
特殊的 ref attribute支持直接访问底层 DOM 元素,它允许在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用,比如,需要在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。组件挂载结束后引用都会作为属性被暴露在当前组件实例的 this.$refs 对象上,只可以在组件挂载后才能访问模板引用ref,如果在模板中的表达式上访问 $refs.input,在初次渲染时会是 null,因为在初次渲染前该元素还不存在。当在 v-for 中使用模板引用时(需要 v3.2.25 及以上版本),相应的ref对应的值是一个数组,并且ref 数组并不保证与源数组相同的顺序。
ref的值类型可以是字符串值,或者使用:ref绑定为一个内联函数((el)=> {})或methods,会在每次组件更新或卸载时都被调用,函数的第一个参数在更新时是元素引用,卸载时是null。
模板引用(ref或:ref)也可以在子组件上使用,引用中获得的值是组件实例。如果子组件使用的是选项式 API,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权,同时,利用expose 选项可以用于限制对子组件实例的访问。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,也因此只应该在绝对需要时才在子组件上使用模板引用。大多数情况下应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
8 组件
组件允许将 UI 划分为独立的、可重用的部分,Vue 实现了自己的组件模型,使得可以在每个组件内封装自定义内容与逻辑。
当使用构建步骤时,Vue 组件定义在一个单独的 .vue 文件,即单文件组件,简称SFC。 当不使用构建步骤时,一个 Vue 组件在js文件中以一个包含 Vue 特定选项的 JavaScript 对象来定义,其中template选项是一个内联的 JavaScript 字符串或者也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template> 元素),Vue 将会使用其内容作为模板来源并会在运行时编译它。.js 文件里默导出组件,但也可以在一个文件中通过具名导出的方式导出多个组件。
8.1 组件注册
组件将会以其注册时的名字作为模板<template>中的标签名,组件注册分为全局注册和局部注册。
全局注册,使用 Vue 应用实例const app = createApp({})的 app.component() 方法,第一个参数是组件名称,第二个参数是组件的实现,进行全局地注册组件,而不需要额外再导入。app.component() 方法可以被链式调用。每一个组件都维护着自己的状态,因为每个组件都是一个新的实例。全局注册存在的问题:
- 没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”),即它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
局部注册需要在使用它的父组件中显式导入,并在components选项上注册来暴露给模板<template>,并且只能在该父组件中使用。 components 选项对象的 key 名就是注册的组件名,而值就是相应组件的实现。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。请注意:局部注册的组件在后代组件中并不可用。
使用 PascalCase 作为组件名的注册格式。首先PascalCase 是合法的 JavaScript 标识符,这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。其次,<PascalCase /> 在模板中更明显地表明是Vue 组件,而不是原生 HTML 元素或自定义元素 (web components) 。同时,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着以 MyComponent 为名注册的组件,在模板中可以通过 <MyComponent> 或 <my-component> 引用,这样使得注册的Vue组件更能配合不同来源的模板。在组件命名使用kebab-case时建议加上前缀(公司名或项目名缩写),以避免和HTML元素名称冲突。
- 如果是在单文件组件中,推荐为子组件使用 PascalCase 的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的,同时,也可以使用 /> 来关闭一个组件标签。
- 如果是直接在 DOM 中书写模板 (例如原生 <template> 元素的内容),即单文件组件、内联模板字符串 (例如template选项)、<script type="text/x-template">等字符串模板除外,Vue 则必须从 DOM 中获取模板字符串,模板的编译需要遵从浏览器中原生HTML 的解析行为,HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写,此外,HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是 <input /> 和 <img />,意味着当使用 DOM 内的模板时,无论是 PascalCase 形式的组件名称、camelCase 形式的 prop 名称还是 v-on 的事件名称,都需要写为相应等价的 kebab-case (短横线连字符) 形式并显式地关闭这些组件的标签。某些 HTML 元素对于放在其中的元素类型有限制,例如 <ul>,<ol>,<table> 和 <select>,相应的,某些元素仅在放置于特定元素中时才会显示,例如 <li>,<tr> 和 <option>。这将导致在使用带有此类限制元素的组件时自定义的组件将作为无效的内容被忽略,可以当使用在原生 HTML 元素上使用vue:is 将其对应值解析为一个 Vue 组件。
8.2 Props
Props attributes需要在组件上使用props选项显式声明注册,而其他未声明的外部传入将作为透传attributes。可以使用字符串数组或对象来声明 props。对于以对象形式声明中的每个属性,key 是 prop 的名称(声明时应该使用camelCase 形式,传递时虽然可以写为camlCase形式但通常写为 kebab-case 形式便于与HTML attribute对齐),而值则是该 prop 预期类型的构造函数,其优点是一定程度上作为组件的文档,而且在使用组件时传递错误的类型时也会在浏览器控制台中抛出警告(在开发模式下)。
- 所有 prop 默认都是可选的,除非声明了 required: true。
- 除 Boolean 外的未传递的可选 prop 将会有一个默认值 undefined。
- Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改。
- default可以是一个函数,对象或者数组应当用default对应的工厂函数返回默认值。工厂函数会收到组件所接收的原始 props作为参数。而对于type是Function的prop,default对应的是作为默认值的函数。
- validator函数用于自定义校验函数,该函数接收prop值,如果返回true,则校验成功,否则校验失败。
- 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。
- prop 的校验是在组件实例被创建之前,所以实例的属性 (比如 data、computed 等) 将在 default 或 validator 函数中不可用。
- 声明为 Boolean 类型的prop有特殊的类型转换规则:和原生HTML相同的行为即传递时组件上显式写明prop不赋值也为true,不写明则为false;当一个 prop 被声明为允许多种类型时,无论声明类型的顺序如何,Boolean 类型的特殊转换规则都会被应用。
- 校验选项中的 type 可以是以下原生构造函数:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
- 自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。
在组件上传递动态 prop 值需要使用v-bind或缩写 :。使用没有参数的 v-bind,可以将一个对象的所有属性都当作 props 传入。当值被传递给 prop 时,它将成为该组件实例上的一个属性,在模板<template>和组件的 this 上下文中访问。默认情况下,组件的所有 prop 都接受任意类型的值。
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着不应该在子组件中去更改一个 prop,否则,Vue 会在控制台上抛出警告。
- 如果prop被用于传入初始值,且子组件想将其作为局部数据属性,正确做法是新定义一个局部数据属性,从 props 上获取初始值,在子组件内只操作新定义的局部数据属性。
- 如果需要对传入的 prop 值做进一步的转换。正确做法是基于该 prop 值定义一个计算属性。
- 如果需要更改对象 / 数组类型的 props,由于对象和数组是按“引用值”传递,子组件更改对象或数组内部的值,也不会抛出警告且存在性能损耗。正确做法是避免这样的更改,大多数场景下,应该抛出一个事件来通知父组件做出改变。
8.3 事件
组件实例提供了一个自定义事件系统。父组件可以像监听原生 DOM 事件一样使用 v-on 或 @ 来选择性地监听子组件通过调用内置 $emit 方法并传入自定义事件名称抛出的事件,其中,$emit() 方法在当前子组件实例上也同样以 this.$emit() 的形式可用,父组件的自定义事件监听器也支持 .once 修饰符。在父组件的模板中也推荐使用 kebab-case 形式来编写监听器。
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制,因此只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案。
所有传入 $emit() 的额外参数都会被直接传向父组件中对应的监听器。
也可以通过emits 选项来声明需要在子组件需要抛出的事件,它声明子组件可能触发的所有事件,emits 选项还支持对象语法,它允许对触发事件赋值为一个函数来对参数进行验证,函数接受的参数就是抛出事件时传入 this.$emit 的内容,返回一个布尔值来表明事件是否合法。推荐完整地声明所有要触发的事件,以此在代码中作为文档记录组件的用法。同时,事件声明能让 Vue 更好地将事件和透传 attribute 作出区分,可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素,从而避免一些由第三方代码触发的自定义 DOM 事件所导致的边界情况。如果与原生事件的名字相同的事件被定义在 emits 选项中,则监听器只会监听组件触发的该自定义事件而不会再响应对应原生事件。
8.4 插槽
子组件能够接收任意类型的 JavaScript 值作为 props,同时子组件也可以接收模板内容。 Vue 的自定义 <slot> 元素,可以实现向子组件中传递模板内容,<slot> 作为一个占位符,会渲染成父组件传递进来的内容,<slot> 类似于函数定义中的参数使用,传递的内容类似于函数调用的传递的实参。
<slot> 元素是一个插槽出口 (slot outlet),标示了父组件提供的插槽内容 (slot content) 将在哪里被渲染。插槽内容可以是任意合法的模板内容,不局限于文本,可以传入多个元素,甚至是组件。Vue 组件的插槽机制是受原生 Web Component <slot> 元素的启发而诞生,同时还做了一些功能拓展。
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的,即父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
在外部没有提供任何内容的情况下,可以写在 <slot> 标签之间为插槽指定默认内容。 带 name 的插槽被称为具名插槽 (named slots)(slots.slotName()或slots.[soltName]()),用于将多个插槽内容传入到各自目标插槽的出口,类比于slots对象的属性键值对传递到函数内并在特定位置使用。没有提供 name 的 <slot> 出口会隐式地命名为“default”。传递时,需要使用含 v-slot 指令(简写 #)的 <template> 元素,指令的值即为目标插槽的名字,而当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template> 节点都被隐式地视为默认插槽的内容。v-slot也支持动态指令参数。
默认情况下,插槽的内容无法访问到子组件的状态,如果插槽的内容想要同时使用父组件域内和子组件域内的数据。需要使用作用域插槽(slots.scopeSlotName(slotProps)或slots.[scopeSlotName](slotProps))。可以像对组件传递 props 那样,给子组件中定义的插槽的出口上传递 attributes。接收插槽 props时:
- 对于默认插槽(slots.default()),子组件内传入插槽的attributes构成了soltProps对象对应于父组件中子组件标签上的 v-slot 指令的值,该soltProps对象可以在插槽内容内的表达式中访问。作用域插槽类比为一个传入子组件的函数,子组件会将相应的 soltProps 作为参数传给它,和函数的参数类似,也可以在v-slot中使用解构。
- 插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps",插槽出口上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。如果混用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template> 标签。尝试直接为组件添加 v-slot 指令将导致编译错误,这是为了避免因默认插槽的 props 的作用域而困惑。
需要子组件能够保留足够的灵活性,将对子组件中某些元素内容和样式的控制权留给使用该子组件的父组件的场景适合使用作用域插槽。一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件,将这种类型的组件称为无渲染组件(类比于React的高阶组件)。但大部分能用无渲染组件实现的功能都可以通过组合式 API 以另一种更高效的方式实现,并且还不会带来额外组件嵌套的开销,尽管如此,作用域插槽在需要同时封装逻辑、组合视图界面时还是很有用。
8.5 透传 attribute
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器,常见的有class、style 和 id。当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并,该规则也适用于v-on事件监听器,根元素监听器和从父组件继承的监听器都会被触发。而如果子组件内是根节点上渲染另一个组件,则子组件接收的透传 attribute 且只有它会直接继续传给子组件的根组件,如果透传的attribute符合声明,也可以作为 props 传入子组件的根组件。透传进来的 attribute 可以在模板的表达式中直接用 $attrs 对象访问到(在JavaScript中则是使用this.$attrs访问),$attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,而且和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写;v-on 事件监听器将在此对象下被暴露为camlCase形式的函数名称 $attrs.onEventName。可以在组件选项中设置 inheritAttrs: false来拒绝继承attribute。最常见的需要禁用attribute继承的场景就是被透传的attribute 需要应用在根节点以外的其他元素上。
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 需要被显式绑定,否则将会抛出一个运行时警告。
Vue 的 <component> 元素和特殊的 is attribute 可以实现在两个组件间来回切换。被传递给被传给 :is 的值可以是被components选项注册的组件名或导入的组件对象。也可以使用 is attribute 来创建一般的 HTML 元素。当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载,可以通过 <KeepAlive> 组件强制被切换掉的组件仍然保持“存活”的状态。
8.6 依赖注入
provide 和 inject 可以帮助解决prop 逐级透传问题,一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
使用provide 选项为组件后代Provide(提供)数据,对于 provide选项对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。如果需要提供依赖当前组件实例的状态 (比如由 data() 定义的数据属性),那么可以以函数形式使用 provide,然而,请注意这不会使注入保持响应性。除了在组件中提供依赖,还可以利用app.provide在整个应用层面提供依赖,在应用级别提供的数据在该应用内的所有组件中都可以注入,这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。
使用 inject 选项来声明要注入到当前组件的上层组件提供的数据,注入会在组件自身的状态之前被解析,因此可以在 data() 中访问到注入的属性。当以数组形式使用 inject,注入的属性会以同名的 key 暴露到组件实例上。如果想要用一个不同的本地属性名注入该属性,需要使用inject选项对象形式,并且该对象的属性键对应于本地属性名,属性值对象中的from属性对应的值对应于注入名。默认情况下,inject 假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则需要类似于props那样声明默认值(对于非基础类型数据,如果创建开销比较大,或是需要确保每个组件实例需要独立数据的,请使用工厂函数),否则会抛出一个运行时警告。
为保证注入方和供给方之间的响应性链接,需要在注入方使用 computed() 函数提供一个计算属性。computed() 函数常用于组合式 API 风格的组件中,但它同样还可以用于补充选项式 API 风格的某些用例。Vue 3.3以下需要设置 app.config.unwrapInjectedRef = true 以保证注入会自动解包该计算属性。
除了使用字符串作为注入名,如果正在构建大型的应用,包含非常多的依赖提供,或者正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。推荐在一个单独的文件中导出这些注入名 Symbol。
8.7 异步组件
Vue 提供了 defineAsyncComponent 方法来实现在大型项目中拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。defineAsyncComponent 方法接收一个返回 Promise 的加载函数。该Promise 的 resolve 回调方法应该在从服务器获取到组件定义时调用,而 reject(reason) 则在加载失败调用。
ES 模块动态导入也会返回一个Promise,所以多数情况它可以和 defineAsyncComponent 搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此defineAsyncComponent语法 (并且会将它们作为打包时的代码分割点),因此也可以用它来导入 Vue 单文件组件。
defineAsyncComponent最后得到的 AsyncComp组件是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽内容传给内部组件,所以可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。 与普通组件一样,异步组件可以使用 app.component() 全局注册,也可以在局部注册组件时使用 defineAsyncComponent。
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在传递对象形式的高级选项中处理这些状态:
- loader:加载函数,对应于defineAsyncComponent 方法接收一个返回 Promise 的加载函数形式。
- loadingComponent:加载异步组件处于loading状态时使用的组件。
- delay: 展示加载组件前的延迟时间,默认为 200ms。
- errorComponent:加载失败后Promise 抛错时展示的组件。
- timout:加载异步组件超时限制,默认值是Infinity,超时会显示配置的errorComponent报错组件。
异步组件可以搭配内置的 <Suspense> 组件一起使用。
8.8 函数式组件
函数式组件是一种定义自身没有任何状态的组件的方式。它们很像纯函数:接收 props,返回 vnodes。函数式组件在渲染过程中不会创建组件实例 (即没有this),也不会触发常规的组件生命周期钩子。第一个参数是props。第二个参数context具有attrs、emit 和 slots三个属性。
大多数常规组件的配置选项在函数式组件中都不可用,除了 props 和 emits,可以通过给函数式组件添加对应的属性来声明它们。如果这个 props 选项没有被定义,那么被传入函数的 props 对象就会像 attrs 一样会包含所有 attribute。除非指定了 props 选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。对于有明确 props 的函数式组件,attribute 透传的原理与普通组件基本相同。然而,对于没有明确指定 props 的函数式组件,只有 class、style 和 onXxx事件监听器将默认从attrs中继承。在这两种情况下,可以将 inheritAttrs 设置为 false 来禁用属性继承。
函数式组件可以像普通组件一样被注册和使用。如果将一个函数作为第一个参数传入 渲染函数h,它将会被当作一个函数式组件来对待。
9 自定义指令
Vue 除了支持内置的一系列指令,还允许注册自定义的指令 (Custom Directives)。Vue中重用代码的方式:组件、组合式函数和自定义指令。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑,而自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。自定义指令比原生HTML attribute 更有用,因为它不仅仅可以在页面加载完成后生效,还可以在 Vue 动态插入元素后生效。
和组件类似,自定义指令在模板中使用前必须先注册。可以使用 directives 选项进行自定义指令的局部注册,也可以使用app.directive进行全局注册。和内置指令类似,自定义指令的参数也可以是动态的。
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind 这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
一个指令的定义对象可以提供以下几种钩子函数 (都是可选的):
指令的钩子会传递以下几种参数:
- el:指令绑定到的元素。这可以用于直接操作 DOM。
- binding:一个包含以下属性对象。
- value:使用时传递给指令的值。如果指令需要多个值,可以向它传递一个 JavaScript 对象字面量,因为指令可以接收任何合法的 JavaScript 表达式。
- oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
- arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
- modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
- instance:使用该指令的组件实例。
- dir:指令的定义对象,即myDirective。
- vnode:代表绑定元素的底层 VNode。
- prevNode:上一次渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。
除了 el 外,其他参数都是只读的,不要更改它们,若需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现。
自定义指令的简化形式是app.directive('color', (el, binding) => {// 这会在 `mounted` 和 `updated` 时都调用}),因为对于自定义指令来说很常见的情况是仅仅需要在 mounted 和 updated 上实现相同的行为,除此之外并不需要其他钩子。
当在组件上使用自定义指令时,和透传 attributes 类似,它会始终应用于组件的根节点。如果组件含有多个根节点,指令将会被忽略且抛出一个警告。和 attribute 不同,指令不能通过 v-bind="$attrs" 来指定传递给某个元素。总的来说,不推荐在组件上使用自定义指令。
10 插件
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。定义一个插件可以是拥有 install() 方法的对象,也可以直接是安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数。使用时通过app.use(myPlugin, {/* 可选的选项 */})给插件myPlugin传递可选的options。插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:
- 通过 app.component() 和 app.directive() 注册一到多个全局组件或自定义指令。
- 通过 app.provide() 使一个资源可被注入进整个应用。
- 向 app.config.globalProperties 中添加一些全局实例属性或方法
- 可能上述三种都包含的功能库 (例如 vue-router)。
编写一个简单的 i18n(国际化Internationalization的缩写)插件。首先,建议在一个单独的文件中创建并导出插件对象,以保证更好地管理逻辑。使用时通过在任意模板中调用(即支持全局调用,通过在安装函数中将该翻译函数添加到 app.config.globalProperties 上来实现)接收以 . 作为分隔符的 key 字符串作为参数的翻译函数,该翻译函数内部会在用户提供的翻译字典(在插件被安装时作为 app.use() 的额外参数options中传入)中查找,并返回翻译得到的值。在插件安装函数中,还可以将插件接收到的 options 参数通过provide提供给整个应用,让任何组件inject后都能使用这个翻译字典对象。请谨慎使用全局属性,如果在整个应用中使用不同插件注入的太多全局属性,很容易让应用变得难以理解和维护。
11 内置组件
内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。
11.1 Transition
<Transition>会在一个元素或组件进入和离开 DOM 时应用动画。此外,还可以切换 CSS class 或用状态绑定样式来应用动画。<Transition> 可以将进入和离开动画应用到默认插槽内容上,<Transition> 仅支持单个元素或组件作为其插槽内容,而且如果内容是一个组件,这个组件必须仅有一个根元素。进入或离开可以由以下的条件之一触发:
- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素 <component> 切换的动态组件
当一个 <Transition> 组件中的元素被插入或移除时:
(1)Vue 会自动检测插槽内容是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class 会在适当的时机被添加和移除。可以给 <Transition> 组件传一个 name prop 来声明一个过渡效果名,对于一个有名字的过渡效果,对它起作用的过渡 class 会以其名字而不是 v 作为前缀。<Transition> 一般都会搭配原生 CSS 过渡(transition CSS 属性)或原生CSS动画一起使用。原生 CSS 动画和 CSS transition 的应用方式基本相同,除了*-enter-from 不是在元素插入后立即移除,而是在一个 animationend 事件触发时被移除。也可以向 <Transition> 传递特定props(enter-from-class、enter-active-class、enter-to-class、leave-from-class、leave-active-class、leave-to-class) 来指定自定义的过渡 class,传入的这些 class 会覆盖相应阶段的默认 class 名,这在想要在 Vue 的动画机制下集成其他的第三方 CSS 动画库(比如Animate.css)时非常有用。仅仅使用transition和animation的其中之一,Vue 可以自动探测到正确的类型,因为Vue 会附加事件监听器transitionend 或 animationend,以便知道过渡何时结束。然而或许想要在同一个元素上同时使用它们两个,此时需要显式地传入 type prop (值是animation 或 transition)来声明,告诉 Vue 本身需要关心的是哪种类型。尽管过渡 class 仅能应用在 <Transition> 的直接子元素上,我们还是可以使用深层级的 CSS 选择器,在深层级的元素上触发过渡效果。默认情况下,<Transition> 组件会通过监听过渡根元素上的第一个 transitionend 或者 animationend 事件来尝试自动判断过渡何时结束。而在嵌套的过渡中,期望的行为应该是等待所有内部元素的过渡完成,此时可以通过向 <Transition> 组件传入 duration prop 来显式指定过渡的持续时间 (以毫秒为单位)。总持续时间应该匹配延迟加上内部元素的过渡持续时间,或者duration prop也支持用对象的形式传入,分开指定进入和离开所需的时间。会触发 CSS 布局变动的属性像 height 或者 margin,执行它们的动画效果更昂贵,需要谨慎使用,而 transform 和 opacity这些属性在动画过程中不会影响到 DOM 结构,因此不会每一帧都触发昂贵的 CSS 布局重新计算,而且大多数的现代浏览器都可以在执行 transform 动画时利用 GPU 进行硬件加速,可以在 CSS-Triggers (https://csstriggers.com/)网站查询哪些属性会在执行动画时触发 CSS 布局变动。6 个应用于进入与离开过渡或动画效果的默认CSS class:
- v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
- v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
- v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
- v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
- v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
- v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除
(2)如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。通过监听 <Transition> 组件事件的方式在过渡过程中挂上钩子函数。钩子可以与 CSS 过渡或动画结合使用,也可以单独使用。在使用仅由 JavaScript 执行的动画时,最好是添加一个 :css="false" prop。这显式地向 Vue 表明可以跳过对 CSS 过渡的自动探测。除了性能稍好一些之外,还可以防止 CSS 规则意外地干扰过渡效果。
(3)如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。
过渡效果是可以被封装复用的。要创建一个可被复用的过渡,需要为 <Transition> 组件创建一个包装组件,并向内传入插槽内容。如果想在某个节点初次渲染时应用一个过渡效果,你可以添加 appear prop。除了通过 v-if / v-show 切换一个元素,也可以通过 v-if / v-else / v-else-if 在几个组件间进行切换,只要确保任一时刻只会有一个元素被渲染即可。可以通过向 <Transition> 传入一个 mode prop 来实现是先执行离开动画还是进入动画。<Transition> 也可以作用于动态组件(<component :is="..."/>)之间的切换。<Transition> 的 props (比如 name) 也可以是动态绑定的,这样可以根据状态变化动态地应用不同类型的过渡或动画,即可以提前定义好多组 CSS 过渡或动画的 class,然后在它们之间动态切换。也可以根据组件的当前状态在 JavaScript 过渡钩子中应用不同的行为,创建动态过渡的终极方式还是创建可复用的过渡组件,并让这些组件根据动态的 props 来改变过渡的效果。
11.2 TransitionGroup
<TransitionGroup> 会在一个 v-for 列表中的部分元素或组件被插入,移动,或移除时应用动画。当在 DOM 模板中使用时,组件名需要写为 <transition-group>。
<TransitionGroup> 支持和 <Transition> 基本相同的 props、CSS 过渡 class 和 JavaScript 钩子监听器,但有以下几点区别:
- 默认情况下,它不会渲染一个容器元素。但可以通过传入 tag prop 来指定一个元素作为容器元素来渲染。
- 过渡模式在这里不可用,因为不再是在互斥的元素之间进行切换。
- 列表中的每个元素都必须有一个独一无二的 key attribute。
- CSS 过渡 class 会被应用在列表内的元素上,而不是容器元素上。
可以通过添加一些额外的 CSS 规则来解决当某一项被插入或移除时,它周围的元素会立即发生“跳跃”而不是平稳地移动的问题,包括将离开的元素从布局流中删除来确保能够正确地计算移动的动画,以及对移动中的元素应用的过渡。
把列表中每一个元素的索引渲染为该元素上的一个 data attribute,然后通过在 JavaScript 钩子中读取元素的 data attribute,对不同元素的动画效果添加一个递进的延迟时间,可以实现带渐进延迟的列表动画。
11.3 KeepAlive
<KeepAlive> 的功能是在多个组件间动态切换时缓存被移除的组件实例。在 DOM 模板中使用时,它应该被写为 <keep-alive>。
默认情况下,一个组件实例在被替换掉后会被销毁,比如<component> 元素来实现动态组件。这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。
在切换时创建新的组件实例通常是有意义的,但有时想要组件能在被“切走”的时候保留它们的状态。可以用 <KeepAlive> 内置组件将这些动态组件包装起来解决这个问题。
<KeepAlive> 默认会缓存内部的所有组件实例,但可以通过prop include 和 exclude(这两个属性值都可以是一个以英文逗号分隔的字符串、一个正则表达式,或是包含这两种类型的一个数组)根据子组件的 name 选项来进行匹配包含和排除需要被缓存的组件实例,因此子组件必须显式声明name 选项才能被有条件的缓存。而在 3.2.34 或以上的版本中,使用 <script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。
可以通过传入 max prop 来限制可被缓存的最大组件实例数。<KeepAlive> 的行为在指定了 max 后类似一个 LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
当一个组件实例从 DOM 上移除但因为被 <KeepAlive> 缓存而仍作为组件树的一部分时,它将变为不活跃状态而不是被卸载。当一个组件实例作为缓存树的一部分插入到 DOM 中时,它将重新被激活。一个持续存在的组件可以通过 activated选项(在首次挂载、以及每次从缓存中被重新插入的时候调用) 和 deactivated (在从 DOM 上移除、进入缓存以及组件卸载时调用)选项来注册相应的两个状态的生命周期钩子。这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。
11.4 Teleport
<Teleport> 可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方,这类场景最常见的是全屏的模态框。 <Teleport> 使得不需要顾虑 DOM 结构(模态框放在非全局的容器节点下)的问题:
- position: fixed 能够相对于浏览器窗口放置有一个条件,那就是不能有任何祖先元素设置了 transform、perspective 或者 filter 样式属性。也就是说如果想要用 CSS transform 为模态框的容器节点设置动画,就会不小心破坏模态框的布局。
- 这个模态框的 z-index 受限于它的容器元素。如果有其他元素与容器节点重叠并有更高的 z-index,则它会覆盖住我们的模态框。
<Teleport> 挂载时,传送的 to 目标必须已经存在于 DOM 中。理想情况下,这应该是整个 Vue 应用 DOM 树外部的一个元素。如果目标元素也是由 Vue 渲染的,你需要确保在挂载 <Teleport> 之前先挂载该元素。
<Teleport> 只改变了渲染的 DOM 结构,它不会影响组件间的逻辑关系,即如果 <Teleport> 包含的组件始终和使用 <teleport> 的组件保持逻辑上的父子关系,传入的 props 、触发的事件以及来自父组件的注入均会按预期工作,同时,子组件将在 Vue Devtools 中嵌套在父级组件下面,而不是放在实际内容移动到的地方。
<Teleport> 支持动态绑定disabled prop 来满足切换是否禁用 <Teleport>的场景。多个 <Teleport> 组件可以将其内容挂载在同一个目标元素上,而渲染的顺序就是简单的顺次追加,后挂载的将排在目标元素里更后面的位置上。
11.5 Suspense
<Suspense> 用来在组件树中协调对异步依赖的处理,它使得可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态,避免每个异步依赖去单独处理自己的加载、报错和完成状态。<Suspense> 可以等待的异步依赖:
- 带有异步 setup() 钩子的组件,包括使用 <script setup> 中有顶层 await 表达式的组件。
- 异步组件。异步组件默认就是“suspensible”的,因此如果异步组件父级关系链上存在 <Suspense>,则该异步组件自己的加载、报错、延时和超时等选项都将被忽略,其加载状态将由 <Suspense> 控制。异步组件也可以通过在选项中指定 suspensible: false 表明不用 Suspense 控制,并让组件始终自己控制其加载状态。
<Suspense> 组件有两个插槽:#default 和 #fallback,都只允许一个直接子节点。在初始渲染时,<Suspense> 将在内存中渲染其默认的插槽内容(#default),但如果在这个过程中遇到任何异步依赖,则会进入挂起状态。在挂起状态期间,展示的是后备内容(#fallback)。当所有遇到的异步依赖都完成后,<Suspense> 会进入完成状态,才将展示出默认插槽(#default)的内容,如果在初次渲染时没有遇到异步依赖,<Suspense> 会直接进入完成状态。进入完成状态后,只有当默认插槽(#default)的根节点被替换时,<Suspense> 才会再次回到挂起状态,而组件树中新的更深层次的异步依赖不会造成 <Suspense> 回退到挂起状态。
发生这种回退时,后备内容(#fallback)不会立即展示出来,<Suspense> 在等待新内容和异步依赖完成时,会先展示默认插槽(#default)的先前内容,直到在等待渲染新内容耗时超过 timeout prop 之后才切换为展示后备内容(#fallback),因此,若 timeout 值为 0 将导致在替换默认内容时立即显示后备内容(#fallback)。
<Suspense> 组件会触发三个事件:pending、resolve 和 fallback。pending 事件是在进入挂起状态时触发。resolve 事件是在 default 插槽完成获取新内容时触发。fallback 事件则是在 fallback 插槽的内容显示时触发。
<Suspense> 组件自身目前还不提供错误处理,不过可以使用 errorCaptured 选项或者 onErrorCaptured() 钩子,在使用到 <Suspense> 的父组件中捕获和处理异步错误。
通常会将 <Suspense> 和 <Transition>、<KeepAlive> 等组件与 Vue Router 中的 <RouterView> 组件结合使用,但嵌套的顺序很重要:<RouterView> -> <Transition> -> <KeepAlive> -> <Suspense>。与异步组件不同,Vue Router 使用动态导入来对懒加载组件进行了内置支持,因此这些懒加载组件本身目前不会触发 <Suspense>。但是它们仍然可以有异步组件作为后代,因此可以使得这些懒加载组件包含异步组件来照常触发 <Suspense>。
<Suspense> 是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。