Vue 3 基础
介绍 Vue 3 的基础使用方法,主要针对与 Vue 2 的不同点。
安装引入
可以使用 CDN 引入最新版的 Vue 3 框架
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
然后就可以访问暴露的 Vue
对象
也可以使用 npm 安装
# 最新稳定版
$ npm install vue@latest
并在入口文件 main.js
或 main.ts
中引入 Vue(或以解构的方式引入 createApp
函数)
import { createApp } from 'vue'
初始化
使用 Vue
的方法 .createApp()
创建一个 Vue 实例应用,它接受一个对象,该对象的属性就是关于根组件的配置选项。
根组件的 props
方法 createApp()
还可以接受第二个(可选)参数,作为根组件的 props
function createApp(rootComponent: Component, rootProps?: object): App
注意
Vue 应用实例 app
会暴露一些方法,例如
app.config
允许配置一些应用级的选项js// 定义一个应用级的错误处理器,用来捕获所有子组件上的错误 app.config.errorHandler = (err) => { /* 处理错误 */ }
请确保在挂载应用实例之前完成所有应用配置app.component()
注册全局组件
然后再使用实例的方法 .mount(el)
将 Vue 实例其挂载到指定的 DOM 上,这样就将数据渲染进页面,而且是响应式的。
参考 el
参考 el
是一个实际的 DOM 元素或是一个 CSS 选择器字符串,选中的元素会作为 Vue 应用的容器
<div>
<div id="app"></div>
</div>
<script>
// 初始化 Vue 实例
const app = Vue.createApp({
data() {
return {
content: 'Hello World!'
}
}
template: '<div>{{ content }}</div>'
})
// 将实例挂载到页面
app.mount('#app')
</script>
被挂载的应用并不会替换元素,即当我们挂载一个应用时,其渲染内容会替换在 mount(el)
中指定的元素 el
的 innerHTML
(作为子元素,而不是替换 el
元素)
提示
如果根组件不提供模板 template
选项时,Vue 会自动使用容器 el
的 innerHTML
作为模板,所以也可以直接通过在挂载容器内编写模板
<div id="app">
<!-- 直接在应用的容器中编写模板 -->
<button @click="count++">{{ count }}</button>
</div>
import { createApp } from 'vue'
const app = createApp({
// 根组件的配置对象中没有 `template` 选项
data() {
return {
count: 0
}
}
})
// 挂载应用
app.mount('#app')
说明
Vue 应用的实例所暴露的大多数方法都会返回自身(同一个实例),以便进行链式调用的方式对同一实例进行多种全局配置
Vue.createApp({})
.component('SearchInput', SearchInputComponent) // 注册全局组件
.directive('focus', FocusDirective) // 注册全局自定义指令
.use(LocalePlugin) // 使用插件
当调用方法 .mount()
将 Vue 实例挂载到页面后,该方法返回的不是应用本身,而是根组件实例,它是一个 Proxy
形式的对象,就是 MVVM 设计模式中的 vm
(即视图和数据的连接层)
const app = Vue.createApp({
data() {
return {
content: 'Hello World!'
}
},
template: '<div>{{ content }}</div>'
});
const vm = app.mount('#app');
组件实例暴露了一些组件相关的 property,可以在组件的模板中访问这些方法,也可以在组件的选项中访问它们,甚至能够在开发模式下使用浏览器的开发者工具在终端中访问。
这些 property 都有一个 $
前缀,以避免与用户定义的 property 名称冲突。
console.log(vm.$data.content)
// 如果该代码是在组件的选项式 API `data` 中,还可以直接访问
console.log(vm.content)
提示
一个页面可以多次调用 createApp()
创建多个 Vue 实例,并挂载到页面的相应的 DOM 元素上
模板语法
将 Vue 应用挂载到页面后,在该 DOM 节点内部的 HTML 就支持模板语法 Mustache,即使用双花括号包含的部分,在其中支持使用单个 JavaScript 表达式
<p>{{ content }}</p>
Tip
可以使用 v-once
指令执行一次性的插值,即只将数据的初始值渲染到页面上
这个值之后就不改变,一般针对的场景是将初始值显示到页面,但是该值不需要响应性变化/更新,使用该指令可以提升页面性能
<span v-once>这个将不会改变: {{ msg }}</span>
Tip
使用 v-html
指令可以将指令的值作为 HTML 进行渲染,作为该节点的 innerHTML
内容,但不能在其中再进行数据绑定(即 v-html
指令的值只能是静态的 HTML 内容)。
const RenderHtmlApp = {
data() {
return {
rawHtml: '<span style="color: red">This should be red.</span>'
}
}
}
Vue.createApp(RenderHtmlApp).mount('#example1')
<div id="example1">
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
</div>
原来页面的 <span>
元素的内容将会被替换成为 rawHtml
property 的值,直接作为 HTML,会忽略解析 property 值中的数据绑定,所以不能使用 v-html
来复合局部模板(因为 Vue 不是基于字符串的模板引擎)。
对于用户界面,组件会更适合作为可重用和可组合的基本单位。
DOM 模板解析注意事项
如果你想在 HTML 的 DOM 元素里直接书写 Vue 模板(Vue 从 DOM 中获取模板字符串),由于浏览器的原生 HTML 解析行为限制,有一些需要注意的事项
说明
这些约束规则只适用于直接在 DOM 中编写模板的情况,如果使用以下来源的字符串模板,就不需要顾虑这些限制
- 单文件组件
- 内联模板字符串,如
template: '...'
<script type="text/x-template">
- 闭合标签:HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是
<input>
和<img>
,所以必须显式地写出关闭标签,即<my-component></my-component>
(而不能是<my-component/>
) - 区分大小写:HTML 标签和属性名称是不分大小写的(浏览器会把任何大写的字符解释为小写),所以一些名称要转换为相应等价的 kebab-case (短横线连字符) 形式
- 如果组件名称采用 PascalCase 形式,如
<MyComponent />
,则在使用组件时需要采用的名称是<my-component></my-component>
- 如果 prop 名称采用 camelCase 形式,如
postTitle
,则在使用组件时需要设置的 prop 是<my-component post-title="hello!"></my-component>
- 如果自定义的事件名称采用 camelCase 形式,如
updatePost
,则在使用组件时需要监听的事件名称是<my-component @update-post="updatePostHandler"></my-component>
- 如果组件名称采用 PascalCase 形式,如
- 元素位置的限制:因为对于
<ul>
、<ol>
、<table>
和<select>
这些元素,其内部允许放置的直接子元素是有严格限制的,如果嵌入其他元素会被视为无效的内容,而提升到外部,造成最终渲染问题。
如果我们需要在这些元素中使用组件作为直接子元素,则可以在「合法」的子元素上使用属性is
来指定渲染的实际内容,这时属性is
用在原生的 HTML 元素上(如<tr>
),该属性值需要使用vue:
作为前缀,以表示解析的实际上是一个 Vue 组件html<table> <tr is="vue:blog-post-row"></tr> </table>
指令
指令是为元素所添加的带有 v-
前缀的特殊 attribute,其作用一般是为了更方便地对 DOM 进行操作,Vue 提供多种内置指令,如 v-if
、v-bind
、v-on
、v-model
指令可以有简写形式,可能需要配合参数 argument 使用,还可以设置修饰符
说明
使用指令时,如果要同时使用参数 arg
和修饰符 modifier
,应该按照 v-directiveName:argument.modifier
先后次序使用(因为 arg
只有一个;而 modifier
可以串联多个,应该放在最后)
动态参数
参数在指令后通过一个冒号 :
隔开,一般是一个字符串,也支持采用 JavaScript 表达式动态生成参数
动态参数需要用方括号 []
包裹
<template>
<a v-bind:[attributeName]="url"> ... </a>
<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
</template>
动态参数中表达式的值应当是一个字符串,或者是 null
(显式移除该绑定),如果表达式的结果是其他非字符串的值会触发警告。
注意
在 HTML 文件中 (直接写在 HTML 文件里的模板) ,动态参数表达式因为某些字符的缘故有一些语法限制
- 不能使用空格和引号html
<!-- 这会触发一个编译器警告 --> <a :['foo' + bar]="value"> ... </a>
- 避免在名称中使用大写字母,因为浏览器会强制将其转换为小写,例如
<a :[someAttr]="value"> ... </a>
其中指令会被浏览器转换为:[someattr]
如果你的组件拥有 “someAttr” 属性而非 “someattr”,这段代码将不会工作。
所以在 HTML 文件中如果需要使用复杂的动态参数,推荐使用计算属性来作为动态参数,而不是直接在模板中编写复杂的表达式
如果采用单文件组件来书写模板,则不受以上限制
自定义指令
Vue 允许注册自定义指令,主要是为了复用关于 DOM 访问/操作的逻辑。
在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令,在模板中以 v-focus
的形式使用;而在没有使用 <script setup>
的情况下,自定义指令需要通过(组件的配置对象) directives
选项进行局部注册;或通过 app.directive(directiveName, hooks)
进行全局注册
例如自定义了一个名为 directiveName
的指令(作为后缀),然后可以在 Vue 的模板中使用该自定义的指令 <div v-directiveName="value">
推荐
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。
其他情况下应该尽可能地使用 v-bind
这样的内置指令来声明式地使用模板(以数据驱动/操作视图),这样更高效,也对服务端渲染更友好。
例如创建一个自定义指令 vFocus
当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
针对以上场景,使用 Vue 自定指令比使用 HTML 的原生属性 autofocus 更有用,因为它不仅仅可以在页面加载完成后生效,还可以在 Vue 动态插入元素后生效
指令钩子
在 <script setup>
中,可以直接以 v
开头的驼峰式命名的变量来设置指令的定义对象,其中包含一系列(可选的)钩子函数
<script setup>
const vMyDirective = {
created(el, binding, vnode, prevVnode) {},
beforeMount(el, binding, vnode, prevVnode) {},
mounted(el, binding, vnode, prevVnode) {},
beforeUpdate(el, binding, vnode, prevVnode) {},
updated(el, binding, vnode, prevVnode) {},
beforeUnmount(el, binding, vnode, prevVnode) {},
unmounted(el, binding, vnode, prevVnode) {}
}
</script>
不同的钩子函数会在(指令所绑定的)元素的不同生命周期阶段执行
created
在绑定元素的 attribute 之前或事件监听器被应用之前调用。该指令也可以为元素附加事件监听器,而会在内置指令v-on
所设置的事件监听器前调用时,这很有用 ❓beforeMount
在元素被插入到 DOM 前调用mounted
在绑定元素的父组件及他自己的所有子节点都挂载完成后调用beforeUpdate
绑定元素的父组件更新前调用updated
在绑定元素的父组件及他自己的所有子节点都更新后调用beforeUnmount
绑定元素的父组件卸载前调用unmounted
绑定元素的父组件卸载后调用,只调用一次
指令所使用的钩子函数的入参
指令的钩子函数会传入以下参数,以便操作所绑定的 DOM 和传递数据:
el
是指令所绑定的元素,可以用来直接操作 DOMbinding
是一个对象,包含以下属性instance
使用该指令的组件实例value
传递给指令的值
例如对于v-my-directive="1 + 1"
则该值为2
oldValue
先前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用arg
传递给指令的参数 (如果有的话)
例如对于v-my-directive:foo
则 arg 为foo
modifiers
一个对象,包含了传递给指令的修饰符 (如果有的话)
需要使用对象来「装载」修饰符,是因为一个指令可以有多个修饰符,以链式方式.a.b.c
串联使用
例如对于v-my-directive.foo.bar
则修饰符对象为{foo: true,bar: true}
dir
指令的定义对象(有点像是指令的「源码」,用于调试 ❓),即在注册指令时作为参数传递的对象
例如在以下自定义的指令中jsapp.directive('focus', { mounted(el) { el.focus() } })
则该指令的钩子函数的dir
参数将会是以下对象js{ mounted(el) { el.focus() } }
vnode
代表绑定元素的底层 VNodeprevNode
之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用
值得留意的一点是,除了 el
参数外,其他参数都是只读的,即不要更改它们。
若需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现,这也是 HTML 原生元素共享数据的常见方式。
提示
很多时候可能只想使用 mounted
和 updated
钩子函数,而且想触发相同的行为,可以使用简写形式,直接用一个函数来定义指令
<template>
<div v-color="color"></div>
</template>
// 用一个函数(而不是对象)来定义指令
app.directive('color', (el, binding) => {
// 该函数会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
全局注册
通过 Vue 实例 app
的方法 app.directive(directiveName, hooks)
注册一个全局可用的自定义指令
- 第一个参数
directiveName
是指令的后缀名 - 第二个参数
hooks
是一个对象,包含一系列的钩子函数,用于定义指令的行为
const app = Vue.createApp({})
app.directive('directiveName', {
// 指令是具有一组生命周期的钩子:
// 在绑定元素的 attribute 或事件监听器被应用之前调用
created() {},
// 在绑定元素的父组件挂载之前调用
beforeMount() {},
// 绑定元素的父组件被挂载时调用
mounted() {},
// 在包含组件的 VNode 更新之前调用
beforeUpdate() {},
// 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
updated() {},
// 在绑定元素的父组件卸载之前调用
beforeUnmount() {},
// 卸载绑定元素的父组件时调用
unmounted() {}
})
然后可以在 Vue 应用的任意组件的模板中任何元素上使用自定义的指令 <div v-directiveName="value">
局部注册
在组件的选项 directives
中注册局部指令,也是可以使用 7 种钩子函数。
<script>
export default {
// ...
directives: {
directiveName: {
// 设定钩子函数
mounted: function (el) {}
}
}
}
</script>
然后可以在该组件的模板中任何元素上使用自定义的指令 <div v-directiveName="value">
使用自定义指令
自定义了一个名为 directiveName
的指令(作为后缀),然后可以在 Vue 的模板中使用该自定义的指令 <div v-directiveName="value">
和内置指令类似,自定义指令的参数也可以是动态的(即传递给指令的参数是根据 JavaScript 表达式计算而得)
<template>
<div v-example:[arg]="value"></div>
</template>
提示
如果指令需要多个值/参数,可以传入一个 JavaScript 对象字面量
<template>
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
</template>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
应用到组件
和非 prop 的 attribute 类似,当自定义指令应用到子组件中时,实际上是作用于子组件的根节点上。
<template>
<MyComponent v-demo="test" />
</template>
<template>
<div> <!-- v-demo 指令会被应用在此处 -->
<span>My component content</span>
</div>
</template>
注意
但是和 attribute 不同,指令不能通过 v-bind="$attrs"
应用到其他的子节点上
所以当自定义指令被应用在一个具有多根节点的组件上时,指令会被忽略,并且会抛出一个警告
总的来说,不推荐在组件上使用自定义指令
属性绑定
在模板中使用指令 v-bind
将标签的属性与动态数据绑定,这样就实现了数据驱动画面(样式)。
指令 v-bind:attrName="value"
可以简写为 :attrName="value"
,绑定的值可以是字符串、对象、数组等。
提示
对于属性 class
和 style
这两个特殊的属性,Vue 进行特殊优化增强,绑定的值除了字符串之外,还可以是对象或数组,而且将动态绑定的数据与该属性的静态数据进行合并(而其他 attribute 就是「后盖前」)。
如果想将一个 JavaScript 对象上的所有属性分别绑定到元素上,可以采用不带参数的 v-bind
const objectOfAttrs = {
id: 'container',
class: 'wrapper'
}
<div v-bind="objectOfAttrs"></div>
注意
如果绑定的值是 null
或 undefined
,那么该 attribute 将不会被包含在渲染的元素上。
说明
对于 DOM 元素的一些 attribute,其属性值是布尔值的(它们只要存在就意味着值为 true
),即只在 truthy 时才渲染在元素上,而在 falsy 时不会添加到 DOM 元素上。
但是对于值为 空字符串 ""
的情况,该 attribute 仍会添加到 DOM 元素上,这是为了与 <button disabled="">
这种形式保持一致。
子组件 Attribute 继承
当组件返回单个根节点时,非 prop attribute 将自动添加到根节点上,作为其 attribute
提示
如果不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false
禁用 attribute 自动继承的一个常见需求是要将 attribute 应用于根节点以外的其他(子)节点上
然后通过访问组件的 $attrs
property,该 property 包括组件 props
和 emits
property 中未包含的所有属性,将该 property 手动绑定 v-bind="$attrs"
到所需的节点上,就可以将相应的 attribute 应用到该节点。
在 Vue 3 中 $attrs
包含传递给组件的所有 attribute,包括 class
和 style
<script>
app.component('date-picker', {
inheritAttrs: false,
template: `
<div class="date-picker">
<input type="datetime-local" v-bind="$attrs" />
</div>
`
})
</script>
由于 Vue 3 支持组件有多个根节点,如果组件的模板具有多个根节点,必须使用 $attrs
显式指定非 Prop 的 attribute 绑定到哪个节点上,否则会弹出警告
<script>
// 这将发出警告
app.component('custom-layout', {
template: `
<header>...</header>
<main>...</main>
<footer>...</footer>
`
})
// 没有警告,$attrs 被传递到 <main> 元素
app.component('custom-layout', {
template: `
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
`
})
</script>
提示
当然也可以指定的某个 attribute(例如只将 class
属性)绑定到某个节点;在组件配置参数的业务逻辑中可以通过 this.$attrs
访问所有传递进来的非 props attribute
<div id="app">
<my-component class="baz"></my-component>
</div>
<script>
// 将 class 属性绑定到第一个根节点
app.component('custom-layout', {
template:`
<div :class="$attrs.class">Hello</div>
<div>World</div>
`
})
</script>
双向绑定
在模板的标签中使用指令 v-model
实现表单的值与动态数据双向绑定,即可以实现在表单 <input>
、<textarea>
、<select>
等标签,通过输入数据可以修改动态数据变量 data property;也可以通过修改相应的动态数据 data property 控制表单的值。
说明
v-model
本质上不过是语法糖,它在内部为不同的表单元素监听不同的 property 以响应用户的输入,并抛出不同的事件,然后在事件处理函数中修改绑定的响应式数据,实现双向绑定,如果需要客制化可以修改相应的事件处理函数。
v-model
「接管」后会忽略任何表单元素上初始的 value
、checked
或 selected attribute,它将始终将当前绑定的 JavaScript 状态视为数据的正确来源,所以应该在 JavaScript 中使用响应式系统的 API 来声明该初始值。
v-model
实际上是为不同的表单元素绑定不同的 property 并监听不同的事件:
- 为
<input type="text">
和<textarea>
元素绑定value
property 并监听input
事件注意
对于文本域
<textarea>
以模板语法{{ value }}
绑定动态数据是不会生效的html<!-- ⚠️ 这样并不会实现双向绑定 --> <textarea>{{text}}</textarea>
应用使用
v-model
来实现双向绑定html<textarea v-model="message" placeholder="add multiple lines"></textarea>
- 为
<input type="checkbox">
和<input type="radio">
元素绑定checked
property 并监听change
事件提示
对于多个复选框,可以绑定到同一个数组或集合的变量值,以便收集当前被选中的所有值
vue<template> <input type="checkbox" id="jack" value="Jack" v-model="checkedNames"> <label for="jack">Jack</label> <input type="checkbox" id="john" value="John" v-model="checkedNames"> <label for="john">John</label> <input type="checkbox" id="mike" value="Mike" v-model="checkedNames"> <label for="mike">Mike</label> </template> <script setup> const checkedNames = ref([]) </script>
- 为
<select>
元素绑定value
property 并监听change
事件注意
如果
v-model
表达式的初始值不匹配任何一个选择项,<select>
元素会渲染成一个**「未选择」的状态**。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个
change
事件。因此建议提供一个空值的禁用选项作为第一个选项来「占位」(默认选项/提示词选项)vue<template> <select v-model="selected"> <option disabled value="">Please select one</option> <option>A</option> <option>B</option> <option>C</option> </select> </template>
提示
对于文本输入的表单,如果用户使用 IME 的语言 (中文,日文和韩文等), v-model
不会在 IME 输入还在拼字阶段时触发更新。
如果想在拼字阶段也触发更新,则需要手动编写 input
事件监听器和 value
属性,而不要使用 v-model
修饰符
Vue 提供了一些修饰符,方便地对表单输入值进行设置:
number
修饰符:如果希望绑定的动态数据变量获得的是数值类型,可以使用该表单修饰符(表单的值默认是字符串类型)tipbox如果该值无法被 `parseFloat()` 处理,那么将返回原始值 `number` 修饰符会在输入框有 `type="number"` 时自动启用
trim
修饰符:表单修饰符v-model.trim
自动过滤用户输入的首尾空白字符lazy
修饰符:表单修饰符v-model.lazy
在change
事件之后(即表单的输入框失去焦点后)进行数据更新同步(而默认是在input
事件触发后,如果用户使用的是 IME,则拼字阶段的状态是例外的)
提示
支持以类似于链式调用的方式设置多个修饰符
<input type="text" v-model.trim.number="content" />
另设值
对于单选按钮 <input type="radio" />
、复选框 <input type="checkbox">
以及选择框 <select>
,有时候希望这些表单的最终值是另设的,这样可以根据双向绑定的值变动,再设置相应的真实的表单值(甚至可以将对象这一类复杂的数据结构作为表单值)
- 复选框:可以使用
true-value
和false-value
属性html<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
js// when checked: vm.toggle === 'yes' // when unchecked: vm.toggle === 'no'
- 单选框:可以绑定
value
属性html<input type="radio" v-model="pick" v-bind:value="a" />
js// 当选中时 vm.pick === vm.a
- 选择框选项:绑定
<option>
元素的value
属性(本来是以选中的<option>
元素的内容innerText
作为该表单的值)html<select v-model="selected"> <!-- 内联对象字面量 --> <option :value="{ number: 123 }">123</option> </select>
js// 当被选中时 typeof vm.selected // => 'object' vm.selected.number // => 123
在组件上使用 v-model
在使用自定义的输入型组件时,Vue 为它们进行优化,也支持使用 v-model
实现双向绑定。
<custom-input v-model="searchText"></custom-input>
<!-- 等价于以下代码 -->
<custom-input
:model-value="searchText"
@update:model-value="searchText = $event"
></custom-input>
在使用该组件时(在外部,在组件的标签上):通过指令 v-model
双向绑定需要同步的变量,它的数据默认作为一个名为 modelValue
的 prop 传递到组件内(因此需要在组件内部的 props
里声明 modelValue
,并将其绑定到内部的表单元素的 value
属性上)
在这些组件的内部需要进行相应的处理:
- 在组件内使用指令
v-bind
将表单元素的value
属性绑定到以上声明的 propmodelValue
上,即v-bind:value="modelValue"
(这样就可以将外部传进来的值作为表单的值,实现与外部数据的「同步」) - 在组件内使用指令
v-on
监听表单元素input
输入事件,并通过抛出update:modelValue
事件$emit('update:modelValue', $event.target.value)
向外传递相应的数据(如果表单是checkbox
应该传递的数据是$event.target.checked
)
然后外部就会接收抛出的事件,并对指令 v-model
绑定的变量(基于抛出的数据)进行改变(即数据的修改操作还是在父层完成)
// component
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
})
<custom-input v-model="searchText"></custom-input>
注意
Vue 2 提供了 .sync
修饰符,通过 v-bind:propName.sync="variable"
和 $emit('update:propName', newValue)
实现类似双向绑定的功能;但在 Vue 3 中 .sync
修饰符已被移除,而现在 Vue 3 使用类似的语法(组件也是抛出以 update:
为前缀的事件)实现的。
在组件内,默认传递的 prop 变量是 modelValue
,默认监听抛出的事件是 update:modelValue
,这种方法更通用,对于 type="text"
或 type="checkbox"
其语义都不会混淆。
如果需要修改 modelValue
这个默认值,在 Vue 2 中可以通过选项 model
进行配置,但在 Vue 3 中 model
选项已移除,作为替代可以在父级中为 v-model
添加参数,这个参数 argument 就是组件内的 prop 变量名和需要监听的事件名(加上 update:
前缀)
<!-- 使用组件 -->
<my-component v-model:title="bookTitle"></my-component>
<!-- 是以下的简写 -->
<my-component :title="bookTitle" @update:title="bookTitle = $event" />
// 组件
app.component('my-component', {
props: {
title: String
},
emits: ['update:title'],
template: `
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)">
`
})
在 Vue 3 中利用特定的 prop(通过指令 v-model
的参数来设置)和抛出相应事件,可以在单个组件上创建多个 v-model
,从而实现多个 prop 的双向绑定
<!-- 使用组件,实现 first-name 和 last-name 的双向绑定 -->
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
emits: ['update:firstName', 'update:lastName'],
template: `
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)">
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)">
`
})
自定义修饰符
另外,在输入型组件上使用指令 v-model
时,不仅支持 Vue 内置的修饰符 .trim
、.number
、.lazy
,Vue 3 还支持添加自定义的修饰符,添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件。
<!-- 使用自定义修饰符,实现字符串首字母大写 -->
<my-component v-model.capitalize="myText"></my-component>
// 组件
app.component('my-component', {
props: {
modelValue: String,
// modelModifiers prop 这里我们提供了初始值
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
methods: {
// 表单 input 事件的处理函数
emitValue(e) {
let value = e.target.value
// 如果 v-model 有修饰符 capitalize 就对表单输入值实现首字母大写
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
// 抛出事件
this.$emit('update:modelValue', value)
}
},
template: `
<input type="text"
:value="modelValue"
@input="emitValue">`,
created() {
// 当组件的 created 生命周期钩子触发时,会根据调用组件的情况对 modelModifiers prop 进行设置
// 由于这个例子中 v-model 的修饰符为 capitalize,因此 modelModifiers 对象会包含 capitalize 属性
console.log(this.modelModifiers) // { capitalize: true }
}
})
说明
如果指令 v-model
需要同时设置参数 argument 以及修饰符 modifier,则修饰符应该在参数后
<my-component v-model:description.capitalize="myText"></my-component>
如果 v-model
设置了参数,则组件内接受修饰符的 prop 的名称(默认为 modelModifiers
)也会作出相应的改变,prop 名称采用 arg
+ Modifiers
的形式
app.component('my-component', {
props: ['description', 'descriptionModifiers'],
emits: ['update:description'],
template: `
<input type="text"
:value="description"
@input="$emit('update:description', $event.target.value)">
`,
created() {
console.log(this.descriptionModifiers) // { capitalize: true }
}
})
条件渲染
指令 v-if
和 v-show
都可以实现条件渲染,元素只会在指令的表达式返回 truthy 值的时候被渲染或显示出来。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
v-if
在模板标签中使用指令 v-if
控制 DOM 的渲染,只有条件指令的值为 true
时才渲染元素,否则 DOM 被移除(或不进行初始化渲染生成)。
提示
如果想同时控制多个元素的条件渲染,可以使用 <template>
元素作为容器,它被当做不可见的包裹元素,即最终的渲染结果将不包含 <template>
元素。
还可以使用 v-if
、v-else-if
、v-else
设置同级并列元素的条件渲染,这三个指令的元素需要连续且同级。
提示
对于 v-if
/v-else
/v-else-if
的各分支项 key
将不再是必须的,因为现在 Vue 会自动生成唯一的 key
。如果你手动提供 key
,那么需要保证每个分支必须使用唯一的 key
。
v-show
在模板标签中使用指令 v-show
也是控制 DOM 的渲染,只有条件指令的值为 true
时才显示元素,否则 DOM 被隐藏。
注意
v-show
不支持 <template>
元素,因为 v-show
是要元素先进行渲染再通过 CSS 控制显示/隐藏的,因此只能作用于可以在页面渲染出 DOM 的元素。
列表渲染
在模板标签中使用指令 v-for
可以将响应式数据中的数组渲染为一系列的 DOM 元素,称为列表渲染,常用形式是 v-for item in arr
或 v-for item of arr
除了可以遍历 arr 数组以外,还支持遍历 obj 对象、number(从 1 开始)数字等
注意
在遍历 objList (类数组的对象)的时候,无法保证每次遍历 item 的先后次序
在使用 v-for
时其中很重要的一点是需要为生成的各个元素设置 key
属性,一般绑定的是与该元素相关的唯一标识(尽量不要直接用 index
,因为一个元素删除后可能会导致其他元素的排序改变,从而影响 key
唯一性),以便提高 DOM 更新的效率。
提示
因为 key
作为一个元素的标识,那么「反过来」利用它也是可行的,即手动更改一个元素的 key
属性值,以强制替换元素/组件而不是重复使用它。
例如针对以下的场景
- 完整地触发组件的生命周期钩子
- 触发过渡
说明
Vue 3 可以将指令 v-for
应用到元素 <template>
,同时可以在 <template>
元素上指定 key
属性,而不必像 Vue 2 那样将 key
属性设置在子元素上
注意
v-if
会拥有比 v-for
更高的优先级,这与 Vue 2 中的工作方式正好相反。
如果在同一个元素上使用 v-for
和 v-if
,则 v-if
将没有权限访问 v-for
里的变量
<!-- This will throw an error because property "todo" is not defined on instance. -->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
官方文档推荐使用 <template>
标签把 v-for
移动到「外层」来修正这个问题。
<template v-for="todo in todos" :key="todo.name">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
不推荐将 v-if
和 v-for
写到同一个元素/层级里,也不是采用前面的方案先进行迭代再进行判断(消耗性能)
更优的方案是将 v-if
的筛选逻辑通过 computed
来实现,再用 v-for
来迭代这个计算属性
注意
由于对象和数组都是引用数据类型,所以修改它们后常常会出现无法正确地触发响应式变化的问题,例如无法触发列表重新渲染。
如果是采用直接通过下标来修改数组的值,这样通常是无法触发视图进行响应式地更新
而如果使用数组变更方法(这些方法对调用它们的原数组进行变更),即 push()
、pop()
、shift()
、unshift()
、splice()
、sort()
、reverse()
,Vue 特意做出优化,通过这些方法修改数组 Vue 可以侦听到,相关依赖会做出响应式变化
其中应该特别留意 reverse()
和 sort()
方法,这两个方法将变更原始数组,所以不能用于 computed
中,因为该选项是不应该有副作用的(即不能对依赖的响应式变量进行修改),应该先创建一个原数组的副本再调用这些方法
- return numbers.reverse()
+ return [...numbers].reverse()
相对地,数组有一些不可变 immutable 方法(这些方法并没有修改原数组,而是返回一个新的数组),即 filter()
、concat()
和 slice()
,如果使用这些方法就需要用返回的结果/数组替换响应性变量的旧数组
// `items` 是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/))
面对整个数组替换的操作,Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作
Teleport
有时组件模板的一部分代码从逻辑上属于该组件,而从 UI 角度来看,最好将模板的这一部分移动到 Vue app 之外的其他位置,例如弹出式的模态框 modal/提示框这一类全屏模式的组件,应该作为 <body>
元素的直接子元素,而不是深度嵌套在组件的一堆 <div>
中。
Vue 3 新增的内置组件 <teleport>
提供了一种干净的方法,允许我们自由地控制在容器中的模板渲染在页面的哪个 DOM 节点下。
内置组件 <teleport>
作为容器,其属性 to
指定内容需要插入到哪一个 DOM 节点,属性值可以是标签名,如 body
,也可以是 CSS 选择器。
注意
<Teleport>
挂载时,传送的 to
目标必须已经存在于 DOM 中。理想情况下,这应该是整个 Vue 应用 DOM 树外部的一个元素。
如果目标元素也是由 Vue 渲染的,你需要确保在挂载 <Teleport>
之前先挂载该元素。
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<!-- 模态框内容渲染为 body 标签的子级 -->
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
提示
当 <teleport>
容器内包含 Vue 组件时,虽然最终渲染在指定的 DOM 节点下,但是逻辑上的父子组件关系并不会变,即来自父组件的注入依然按预期工作,并且在 Vue Devtools 中,子组件将嵌套的父组件之下,而不是放在实际内容移动到的位置。
提示
多个 <teleport>
组件可以挂载到同一个目标元素,以追加的模式叠加,即稍后挂载的元素位于较早的挂载之后,一个常见的用例场景是一个可重用的 <Modal>
组件用于弹出消息,挂载在 <body>
元素下,可能同时有多个实例处于活动状态,从 UI 角度看到的效果是新的模态框就会覆盖旧的模态框。
可以通过 disabled
prop 来禁用 <Teleport>
的「转移」功能,让它充当普通的容器组件。
例如要在桌面端将一个组件当做浮层来渲染,但在移动端则当作行内组件,可以通过对 <Teleport>
动态地传入一个 disabled
prop 来处理这两种不同情况
<template>
<!-- isMobile 可以根据 CSS media query 的不同结果动态地更新 -->
<Teleport :disabled="isMobile">
<!-- ... -->
</Teleport>
</template>
响应式变量
在配置(根)组件时,可以在选项式 API data()
函数中声明响应式变量,然后将它们作为返回值包在一个对象中,Vue 会通过响应性系统将其包裹起来,在组件实例创建后,以 $data
的形式存储在组件实例 vm
中。
这样在就可以在模板中直接使用它们,而在其他选项式 API 函数中则使用 this.$data.varName
(其中 this
是组件实例 vm
,在 Vue 外部也可以通过 vm.$data
访问该响应式对象)来访问。
为方便起见,该对象的任何顶级 property 可以直接通过组件实例暴露出来,即可以通过 this.varName
替代。
const app = Vue.createApp({
data() {
return { count: 4 }
}
})
vm = app.mount('#app');
console.log(vm.$data.count) // 4
console.log(vm.count) // 4
注意
这些 property 仅在实例首次创建时被添加,所以你需要确保它们都在 data
函数返回的对象中。必要时,要对尚未提供所需值的 property 使用 null
、undefined
或其他值作为占位的值/初始值。
计算属性和侦听器
在选项 computed
中创建计算属性,在选项 watch
创建监听器,两者都是基于响应式数据的变化再执行操作的,但用处有不同。
computed
计算属性常常是为了将复杂和需要重复使用的资料处理逻辑,从模板中的函数表达式提出来。它类似 data
的属性一样,作为响应式数据在模板上直接使用。
如果希望计算属性实时更新,那么它需要有响应式依赖(基于 data 或其他来源的响应式数据),并且返回一个值。
计算属性的最大优势是对于响应式依赖有缓存的,即计算属性只在相关响应式依赖发生改变时它们才会重新求值。
提示
如果不希望有缓存,请用 method
来替代,同样可以实现相同的功能,相比之下,每当触发组件重新渲染时,方法 method
函数将总会再次执行。
watch
可以将 watch
看作是更通用的响应数据侦听方式。
watch 也是依赖响应式数据的,可以是 data property 或 computed property(也可以是这些 property 的嵌套 property),然后在响应式数据发生变化时触发回调函数,在回调中可以执行异步操作,可以设置中间状态,而且不必返回值。
使用选项式 API 创建侦听器除了以函数形式(函数名就是需要侦听的响应式变量)以外,还可以是对象的形式,对侦听器进行更详细的设置
watch: {
// 函数形式,侦听响应式变量 a
a(val, oldVal) {
console.log(`new: ${val}, old: ${oldVal}`)
},
// 回调函数以函数名来表示(字符串)
b: 'someMethod',
// 对象形式
// 配置了 deep 为 true
// 该回调会在被侦听的对象 c 的任何 property 改变时被调用,不论其被嵌套多深
c: {
handler(val, oldVal) {
console.log('c changed')
},
deep: true
},
// 侦听对象 c 的属性 d(嵌套属性)
'c.d': function (val, oldVal) {
// do something
},
// 该回调函数将会在侦听开始之后被立即调用一次
e: {
handler(val, oldVal) {
console.log('e changed')
},
immediate: true
},
// 多个回调函数构成的数组,当响应式变量 f 发生变动时,这些回调函数会被逐一调用
f: [
'handle1',
function handle2(val, oldVal) {
console.log('handle2 triggered')
},
{
handler: function handle3(val, oldVal) {
console.log('handle3 triggered')
}
}
]
}
注意
不能直接侦听响应式对象的属性值,因为这相当于解构对象而造成响应性丢失
const obj = reactive({ count: 0 })
// ⚠️ 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
应该监听返回该属性的 getter 函数
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
深层侦听器的区别
如果传入一个响应式对象(由 reactive()
包裹的对象),会隐式地创建一个深层侦听器,即对象的任何属性(包括嵌套的属性)变更时都会触发回调函数
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// ⚠️ 注意传入的参数 `newValue` 和 `oldValue` 是相等的
// 因为它们引用的是同一个对象
})
如果传入一个返回响应式对象的 getter 函数,则只有在返回不同的对象时(即对象进行整体替换时),才会触发回调
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
可以给上面的例子显式地加上 deep
选项,强制转成深层侦听器
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当监听的对象比较大时需要留意性能的消耗,因此只在必要时才使用它
提示
在 Vue 2 中 watch 一般只能监听一个响应式数据(如果要监听多个变量,需要使用箭头函数将他们组合返回再给 watch 监听,相当于侦听一个 computed)
而在 Vue 3 中是支持以数组的形式监听多个数据源和响应式对象
// 侦听多个源 [fooRef, barRef]
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
方法
用 methods
选项向组件实例添加方法,该选项是一个对象,其中各属性就是该组件可以调用的方法。
可以在组件模板中直接调用方法,也可以在其他选项中通过 this.methodName([params])
来调用。
注意
Vue 自动为 methods
绑定 this
,以便于它始终指向组件实例,因此在定义这些方法时应该避免使用箭头函数(因为这会阻止 Vue 绑定恰当的 this
指向)
注意
另外官方文档还提到一个关于防抖的例子值得注意。
组件实例化的时候,虽然将组件的 methods
选项里的函数的 this
进行绑定(指向了组件实例),但是函数还是共享。因此在同一个页面复用组件时,进行防抖实际调用的是同一个函数,可能导致与预期不一样的效果,具体分析可以参考这篇文章。
因此正确的做法是在生命周期钩子的 created
里,将防抖函数添加到组件实例上,这样就可以让每个组件都有自己独立的防抖函数
app.component('save-button', {
created() {
// 用 Lodash 的防抖函数对事件处理函数进行封装
// this 指的是组件实例
// 因此 debouncedClick 函数是每个组件实例彼此独立的
this.debouncedClick = _.debounce(this.click, 500)
},
unmounted() {
// 移除组件时,取消定时器
this.debouncedClick.cancel()
},
methods: {
click() {
// ... 响应点击的具体处理函数 ...
}
},
template: `
<button @click="debouncedClick">
Save
</button>
`
})
事件处理
在模板的标签中使用指令 v-on:event-name
监听事件(也可以使用简写形式 @event-name
),当事件被触发时会调用相应的回调函数。
提示
在模板中设置的事件监听器时,所指定的事件类型的名称需要 kebab-case 形式
其回调函数默认传递 event
事件作为参数。
<template>
<div id="event-with-method">
<button @click="greet">Greet</button>
</div>
</template>
<script>
// ...
methods: {
greet(event) {
// `event` 是原生 DOM event
console.log(event)
}
}
</script>
如果希望同时传递参数和事件,可以用特殊变量 $event
把事件传入回调函数中(记得在事件处理函数中设置相应的形参),或使用箭头函数
<template>
<!-- 在事件处理器中使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联事件处理器,箭头函数的形式 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
</template>
<script>
// ...
methods: {
warn(message, event) {
// 现在既可以接收到自定义的参数,也可以访问到原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
}
</script>
提示
支持为同一个事件设置多个处理函数,使用逗号 ,
分隔,而且回调函数都要加上括号 ()
(即使没有传递参数)
<template>
<!-- 按钮被点击时,将执行 handlerOne() 和 handlerTwo() 这两个回调函数 -->
<button @click="handlerOne(), handlerTwo($event)">
Submit
</button>
</template>
提示
支持 v-on
不带参数绑定一个事件/监听器键值对的对象。
<!-- 对象语法 (只适用版本 2.4.0+ 以上的 Vue 中) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
当使用对象语法时,是不支持任何修饰器的
修饰符
Vue 提供了多种事件修饰符,用于更方便地对事件的行为进行设置,这样事件处理函数就可以只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。事件修饰符可以链式调用多个。
.stop
阻止事件继续传播.prevent
阻止事件的默认行为,例如表单提交后跳转到指定网址.self
当事件在该元素自身触发时才执行事件处理函数.once
只对事件进行一次响应后失效.capture
使用事件捕获模式.passive
一把针对移动端scroll
事件进行优化,提高性能(滚动事件将立即发生而非等待onScroll
完成)注意
但不要把
.passive
和.prevent
一起使用.prevent
将会被忽略,因为.passive
已经向浏览器表明了你不想阻止事件的默认行为。同时使用浏览器可能会向你展示一个警告
- 一系列的鼠标和按键修饰符:UI 用户界面的交互事件十分常见,如鼠标事件、按键事件等,Vue 提供了相应的鼠标和按键修饰符,例如可以直接将键盘事件对象
KeyboardEvent.key
所暴露的任意有效按键名转换为 kebab-case 来作为修饰符,这样就可以针对特定的按键设置响应函数html<!-- 处理函数只会在 $event.key 等于 'PageDown' 时被调用 --> <input @keyup.page-down="onPageDown" />
注意
在 Vue 3 中不再支持使用数字 (即键码) 作为
v-on
修饰符
注意
使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。
因此使用 @click.prevent.self
会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent
则只会阻止对元素本身的点击事件的默认行为。
使用自定义事件抛出一个值
在父子组件中间监听抛出的事件时,子组件在模板中使用 $emit()
方法抛出事件,第一个参数是事件名称,第二参数(可选)是需要传递的数据
<button @click="$emit('enlargeText', 0.1)">Enlarge text</button>
提示
如果希望抛出子组件的事件,可以使用特殊的变量 $event
作为参数
<!-- 使用 $event 访问子组件的点击事件,然后作为自定义事件 enlargeText 的参数抛出,让父组件可以接收 -->
<button @click="$emit('enlargeText', $event)">
Enlarge text
</button>
然后在父组件中监听这个事件,其回调函数会将子组件所抛出的数据作为第一个参数
<template>
<blog-post @enlarge-text="onEnlargeText"></blog-post>
</template>
<script>
// ...
methods: {
onEnlargeText(enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
</script>
提示
如果父组件中不使用回调函数,而直接使用内联处理器的写法,则可以使用特殊的变量 $event
来访问子组件所被抛出的这个值
<blog-post @enlarge-text="postFontSize += $event"></blog-post>
emits 选项
Vue 3 新增了一个 emits
选项(其作用和选项 props
作用类似),用来定义子组件可以向其父组件触发的事件类型
提示
强烈建议使用 emits
记录/声明每个组件可以触发的所有类型的(自定义)事件,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。
因为 Vue 3 移除了 .native
修饰符,对于未在子组件中的选项 emits
定义的事件,Vue 现在将把它们作为原生事件监听器(这一点和 Vue 2 不同,Vue 2 对于在使用组件时才添加的事件监听,都认为是监听自定义事件),添加到子组件的根元素中(除非在子组件的选项中设置了 inheritAttrs: false
)
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
该选项可以接受字符串作为元素的数组,也可以接收一个对象(可以为事件设置验证器,和 props
定义里的验证器类似)。
在对象语法中,每个 property 的值可以为 null
或验证函数,该函数将接收调用 $emit
时传递的数据。验证函数应返回布尔值,以表示事件参数是否有效。
<script>
const app = createApp({})
// 数组语法
app.component('todo-item', {
emits: ['check'],
created() {
this.$emit('check')
}
})
// 对象语法
app.component('reply-form', {
emits: {
// 没有验证函数
click: null,
// 带有验证函数
submit: payload => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
})
</script>