相比 Vue2
1. 优点
- vue3支持vue2的大多数特性,实现对vue2的兼容
- vue3对比vue2具有明显的性能提升
- 打包大小减少41%
- 初次渲染快55%,更新快133%
- 内存使用减少54%
- 更好的支持TypeScript
- 使用Proxy代替defineProperty实现响应式数据
2. 性能提升的原因
静态标记
- vue2从根节点开始对虚拟dom进行全量对比(每个节点不论写死的还是动态的都会一层一层比较)
- vue3新增了静态标记 与上次虚拟dom对比的时候,只对比带有 patchFlags 的节点。跳过一些静态节点对比
hoistStatic 静态提升
- vue2里每当触发更新的时候,不管元素是否参与更新,每次都会重新创建
- vue3为了避免每次渲染的时候都要重新创建这些对象,会把不参与更新的元素保存起来,只创建一次,每次复用
cacheHandlers 事件缓存
- vue2里绑定事件都要重新生成新的function去更新
- vue3会自动生成一个内联函数,同时生成一个静态节点。onclick时会读取缓存,如果缓存没有的话,就把传入的事件存到缓存里
3. 响应式数据的变化
vue2 响应式
核心
- Object.defineProperty 来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调
- 对象:递归调用defineProperty对对象已有属性值的读取和修改进行拦截
- 数组:重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
- 问题
- 不能监听到对象属性的添加和删除,需要Vue.set()来添加和删除。
- 不能通过下标替换元素或更新length
vue3 响应式
核心
- 通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 包括属性值的读写, 属性的添加, 属性的删除等
- 通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作
Vue3 应用实例 API
createApp()
创建一个应用实例。
function createApp(rootComponent: Component, rootProps?: object): App
- 第一个参数是
根组件
。第二个参数可选,它是要传递给根组件的props
实际使用
// 可以直接内联根组件
import { createApp } from 'vue'
const app = createApp({
/* root component options */
})
// 也可以使用从别处导入的组件
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
createSSRApp()
以 SSR 激活模式创建一个应用实例。用法与
createApp()
完全相同。
app.mount()
将应用实例挂载在一个容器元素中。
interface App {
mount(rootContainer: Element | string): ComponentPublicInstance
}
- 参数可以是一个实际的 DOM 元素或一个 CSS 选择器 (使用第一个匹配到的元素)。返回根组件的实例。
- 如果该组件有模板或定义了渲染函数,它将替换容器内所有现存的 DOM 节点。否则在运行时编译器可用的情况下,容器元素的
innerHTML
将被用作模板。 - 在 SSR 激活模式下,它将激活容器内现有的 DOM 节点。如果出现了激活不匹配,那么现有的 DOM 节点将会被修改以匹配客户端的实际渲染结果。
- 对于每个应用实例,
mount()
仅能调用一次。
实际使用
import { createApp } from 'vue'
const app = createApp(/* ... */)
app.mount('#app')
// 也可以挂载到一个实际的 DOM 元素
app.mount(document.body.firstChild)
app.unmount()
卸载一个已挂载的应用实例。卸载一个应用会触发该应用组件树内所有组件的卸载生命周期钩子。
interface App {
unmount(): void
}
app.provide()
提供一个值,可以在应用中的所有后代组件中注入使用。
interface App {
provide<T>(key: InjectionKey<T> | symbol | string, value: T): this
}
- 第一个参数应当是注入的 key,第二个参数则是提供的值。返回应用实例本身。
Provide (提供)
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一颗巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦:
要为组件后代提供数据,可以使用到provide()
函数:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
如果不使用 <script setup>
,请确保 provide()
是在 setup()
同步调用的:
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
provide()
函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol
。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide()
,使用不同的注入名,注入不同的依赖值。
第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。
应用层 Provide
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。
Inject (注入)
要注入上层组件提供的数据,需使用
inject()
函数:
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
同样的,如果没有使用 <script setup>
,inject()
需要在 setup()
内同步调用:
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
注入默认值
默认情况下,
inject
假设传入的注入名会被某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。
如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
const value = inject('key', () => new ExpensiveClass())
和响应式数据配合使用
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly()
来包装提供的值。
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
使用 Symbol 作注入名
至此,我们已经了解了如何使用字符串作为注入名。但如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
我们通常推荐在一个单独的文件中导出这些注入名 Symbol:
// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, { /*
要提供的数据
*/ });
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
app.component()
如果同时传递一个组件名字符串及其定义,则注册一个全局组件;如果只传递一个名字,则会返回用该名字注册组件 (如果存在的话)。
interface App {
component(name: string): Component | undefined
component(name: string, component: Component): this
}
全局注册
我们可以使用 Vue 应用实例的
app.component()
方法,让组件在当前 Vue 应用中全局可用。
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
如果使用单文件组件,你可以注册被导入的 .vue
文件:
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
app.component()
方法可以被链式调用:
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用:
<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在_彼此内部_使用。
局部注册
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
在使用
<script setup>
的单文件组件中,导入的组件可以直接在模板中使用,无需注册:
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
如果没有使用 <script setup>
,则需要使用 components
选项来显式注册:
import ComponentA from './ComponentA.js'
export default {
components: {
ComponentA
},
setup() {
// ...
}
}
对于每个 components
对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。上面的例子中使用的是 ES2015 的缩写语法,等价于:
export default {
components: {
ComponentA: ComponentA
}
// ...
}
请注意:
局部注册的组件在后代组件中并不可用。在这个例子中,ComponentA
注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。
组件名格式
在整个指引中,我们都使用 PascalCase 作为组件名的注册格式,这是因为:
- PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
<PascalCase />
在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来。
在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 模板中是不可用的,详情参见 DOM 模板解析注意事项。
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以
MyComponent
为名注册的组件,在模板中可以通过<MyComponent>
或<my-component>
引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。
DOM 模板解析注意事项
大小写区分
- HTML 标签和属性名称是不分大小写的,所以浏览器会把任何大写的字符解释为小写。这意味着当你使用 DOM 内的模板时,无论是 PascalCase 形式的组件名称、camelCase 形式的 prop 名称还是 v-on 的事件名称,都需要转换为相应等价的 kebab-case (短横线连字符) 形式
闭合标签
- 闭合标签 (self-closing tag):Vue 的模板解析器支持任意标签使用
/>
作为标签关闭的标志。 - 然而在 DOM 模板中,我们必须显式地写出关闭标签:这是由于 HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是
<input>
和<img>
。对于其他的元素来说,如果你省略了关闭标签,原生的 HTML 解析器会认为开启的标签永远没有结束
元素位置限制
- 某些 HTML 元素对于放在其中的元素类型有限制,例如
<ul>
,<ol>
,<table>
和<select>
,相应的,某些元素仅在放置于特定元素中时才会显示,例如<li>
,<tr>
和<option>
。 - 这将导致在使用带有此类限制元素的组件时出现问题。例如:
<table>
<blog-post-row></blog-post-row>
</table>
- 自定义的组件
<blog-post-row>
将作为无效的内容被忽略,因而在最终呈现的输出中造成错误。我们可以使用特殊的is
attribute 作为一种解决方案:
<table>
<tr is="vue:blog-post-row"></tr>
</table>
app.directive()
如果同时传递一个名字和一个指令定义,则注册一个全局指令;如果只传递一个名字,则会返回用该名字注册的指令 (如果存在的话)。
interface App {
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
}
自定义指令
除了 Vue 内置的一系列指令 (比如
v-model
或v-show
) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。
我们已经介绍了两种在 Vue 中重用代码的方式:组件和组合式函数。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。另一方面,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
假设你还未点击页面中的其他地方,那么上面这个 input 元素应该会被自动聚焦。该指令比
autofocus
attribute 更有用,因为它不仅仅可以在页面加载完成后生效,还可以在 Vue 动态插入元素后生效。
在<script setup>
中,任何以v
开头的驼峰式命名的变量都可以被用作一个自定义指令。在上面的例子中,vFocus
即可以在模板中以v-focus
的形式使用。
在没有使用<script setup>
的情况下,自定义指令需要通过directives
选项注册:
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
/* ... */
}
}
}
将一个自定义指令全局注册到应用层级也是一种常见的做法:
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})
TIP
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
指令钩子
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
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) {}
}
钩子参数
指令的钩子会传递以下几种参数:
el
:指令绑定到的元素。这可以用于直接操作 DOM。binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。
vnode
:代表绑定元素的底层 VNode。prevNode
:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
举例来说,像下面这样使用指令:
<div v-example:foo.bar="baz">
binding
参数会是一个这样的对象:
{
arg: 'foo',
modifiers: { bar: true },
value: /* `baz` 的值 */,
oldValue: /* 上一次更新时 `baz` 的值 */
}
和内置指令类似,自定义指令的参数也可以是动态的。举例来说:
<div v-example:[arg]="value"></div>
这里指令的参数会基于组件的 arg
数据属性响应式地更新。
Note 除了
el
外,其他参数都是只读的,不要更改它们。若你需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现。
简化形式
对于自定义指令来说,一个很常见的情况是仅仅需要在
mounted
和updated
上实现相同的行为,除此之外并不需要其他钩子。这种情况下我们可以直接用一个函数来定义指令,如下所示:
<div v-color="color"></div>
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
对象字面量
如果你的指令需要多个值,你可以向它传递一个 JavaScript 对象字面量。别忘了,指令也可以接收任何合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
在组件上使用
当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传 attributes 类似。
<MyComponent v-demo="test" />
<!-- MyComponent 的模板 -->
<div> <!-- v-demo 指令会被应用在此处 -->
<span>My component content</span>
</div>
需要注意的是组件可能含有多个根节点。当应用到一个多根组件时,指令将会被忽略且抛出一个警告。和 attribute
不同,指令不能通过 v-bind="$attrs"
来传递给一个不同的元素。总的来说,不推荐在组件上使用自定义指令。
app.use()
安装一个插件。
interface App {
use(plugin: Plugin, ...options: any[]): this
}
- 第一个参数应是插件本身,可选的第二个参数是要传递给插件的选项。
- 插件可以是一个带
install()
方法的对象,亦或直接是一个将被用作install()
方法的函数。插件选项 (app.use()
的第二个参数) 将会传递给插件的install()
方法。 - 若
app.use()
对同一个插件多次调用,该插件只会被安装一次。
插件概述
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:
import { createApp } from 'vue'
const app = createApp({})
app.use(myPlugin, {
/* 可选的选项 */
})
一个插件可以是一个拥有 install()
方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use()
的额外选项作为参数:
const myPlugin = {
install(app, options) {
// 配置此应用
}
}
插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:
- 通过
app.component()
和app.directive()
注册一到多个全局组件或自定义指令。 - 通过
app.provide()
使一个资源可被注入进整个应用。 - 向
app.config.globalProperties
中添加一些全局实例属性或方法 - 一个可能上述三种都包含了的功能库 (例如 vue-router)。
编写一个插件
为了更好地理解如何构建 Vue.js 插件,我们可以试着写一个简单的
i18n
(国际化 (Internationalization) 的缩写) 插件。
让我们从设置插件对象开始。建议在一个单独的文件中创建并导出它,以保证更好地管理逻辑,如下所示:
// plugins/i18n.js
export default {
install: (app, options) => {
// 在这里编写插件代码
}
}
我们希望有一个翻译函数,这个函数接收一个以 .
作为分隔符的 key
字符串,用来在用户提供的翻译字典中查找对应语言的文本。期望的使用方式如下:
<h1>{{ $translate('greetings.hello') }}</h1>
这个函数应当能够在任意模板中被全局调用。这一点可以通过在插件中将它添加到 app.config.globalProperties
上来实现:
// plugins/i18n.js
export default {
install: (app, options) => {
// 注入一个全局可用的 $translate() 方法
app.config.globalProperties.$translate = (key) => {
// 获取 `options` 对象的深层属性
// 使用 `key` 作为索引
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
}
}
我们的 $translate
函数会接收一个例如 greetings.hello
的字符串,在用户提供的翻译字典中查找,并返回翻译得到的值。
用于查找的翻译字典对象则应当在插件被安装时作为 app.use()
的额外参数被传入:
import i18nPlugin from './plugins/i18n'
app.use(i18nPlugin, {
greetings: {
hello: 'Bonjour!'
}
})
这样,我们一开始的表达式 $translate('greetings.hello')
就会在运行时被替换为 Bonjour!
了。
TIP
请谨慎使用全局属性,如果在整个应用中使用不同插件注入的太多全局属性,很容易让应用变得难以理解和维护。
插件中的 Provide / Inject
在插件中,我们可以通过 provide
来为插件用户供给一些内容。举例来说,我们可以将插件接收到的 options
参数提供给整个应用,让任何组件都能使用这个翻译字典对象。
// plugins/i18n.js
export default {
install: (app, options) => {
app.config.globalProperties.$translate = (key) => {
return key.split('.').reduce((o, i) => {
if (o) return o[i]
}, options)
}
app.provide('i18n', options)
}
}
现在,插件用户就可以在他们的组件中以 i18n
为 key 注入并访问插件的选项对象了。
<script setup>
import { inject } from 'vue'
const i18n = inject('i18n')
console.log(i18n.greetings.hello)
</script>
app.mixin()
应用一个全局 mixin (适用于该应用程序的范围)。一个全局的 mixin 会作用于应用中的每个组件实例。
不推荐
Mixins 在 Vue 3 支持主要是为了向后兼容,因为生态中有许多库使用到。在新的应用中应尽量避免使用 mixin,特别是全局 mixin。
若要进行逻辑复用,推荐用组合式函数来替代。
interface App {
mixin(mixin: ComponentOptions): this
}
app.version
提供当前应用所使用的 Vue 版本号。这在插件中很有用,因为可能需要根据不同的 Vue 版本执行不同的逻辑。
interface App {
version: string
}
实际使用
// 在一个插件中对版本作判断
export default {
install(app) {
const version = Number(app.version.split('.')[0])
if (version < 3) {
console.warn('This plugin requires Vue 3')
}
}
}
app.config
每个应用实例都会暴露一个
config
对象,其中包含了对这个应用的配置设定。你可以在挂载应用前更改这些属性 (下面列举了每个属性的对应文档)。
import { createApp } from 'vue'
const app = createApp(/* ... */)
console.log(app.config)
app.config.errorHandler
用于为应用内抛出的未捕获错误指定一个全局处理函数。
- 类型
interface AppConfig {
errorHandler?: (
err: unknown,
instance: ComponentPublicInstance | null,
// `info` 是一个 Vue 特定的错误信息
// 例如:错误是在哪个生命周期的钩子上抛出的
info: string
) => void
}
- 详细信息错误处理器接收三个参数:错误对象、触发该错误的组件实例和一个指出错误来源类型信息的字符串。它可以从下面这些来源中捕获错误:
- 组件渲染器
- 事件处理器
- 生命周期钩子
setup()
函数- 侦听器
- 自定义指令钩子
- 过渡 (Transition) 钩子
- 示例
app.config.errorHandler = (err, instance, info) => {
// 处理错误,例如:报告给一个服务
}
app.config.warnHandler
用于为 Vue 的运行时警告指定一个自定义处理函数。
- 类型
interface AppConfig {
warnHandler?: (
msg: string,
instance: ComponentPublicInstance | null,
trace: string
) => void
}
- 详细信息警告处理器将接受警告信息作为其第一个参数,来源组件实例为第二个参数,以及组件追踪字符串作为第三个参数。这可以用户过滤筛选特定的警告信息,降低控制台输出的冗余。所有的 Vue 警告都需要在开发阶段得到解决,因此仅建议在调试期间选取部分特定警告,并且应该在调试完成之后立刻移除。
TIP
警告仅会在开发阶段显示,因此在生产环境中,这条配置将被忽略。
- 示例
app.config.warnHandler = (msg, instance, trace) => {
// `trace` is the component hierarchy trace
}
app.config.performance
设置此项为
true
可以在浏览器开发工具的“性能/时间线”页中启用对组件初始化、编译、渲染和修补的性能表现追踪。仅在开发模式和支持 performance.mark API 的浏览器中工作。
- 类型
boolean
- 参考:指南 - 性能
app.config.compilerOptions
配置运行时编译器的选项。设置在此对象上的值将会在浏览器内进行模板编译时使用,并会影响到所配置应用的所有组件。另外你也可以通过
compilerOptions
选项在每个组件的基础上覆盖这些选项。
重要
此配置项仅在完整构建版本,即可以在浏览器中编译模板的vue.js
文件中可用。如果你用的是带构建的项目配置,且使用的是仅含运行时的 Vue 文件版本,那么编译器选项必须通过构建工具的相关配置传递给@vue/compiler-dom
。
app.config.compilerOptions.isCustomElement
用于指定一个检查方法来识别原生自定义元素。
- 类型
(tag: string) => boolean
- 详细信息如果该标签需要当作原生自定义元素则应返回
true
。对匹配到的标签,Vue 会将其渲染为原生元素而非将其视为一个 Vue 组件来解析。原生 HTML 和 SVG 标签不需要在此函数中进行匹配,Vue 的解析器会自动识别它们。 - 示例
// 将所有标签前缀为 `ion-` 的标签视为自定义元素
app.config.compilerOptions.isCustomElement = (tag) => {
return tag.startsWith('ion-')
}
app.config.compilerOptions.whitespace
用于调整模板中空格的处理行为。
- 类型
'condense' | 'preserve'
- 默认
'condense'
- 详细信息Vue 移除/缩短了模板中的空格以求更高效的模板输出。默认的策略是“缩短”,表现行为如下:
设置该选项为 'preserve'
则会禁用 (2) 和 (3) 两项。
- 元素中开头和结尾的空格字符将被缩短为一个空格。
- 包含换行的元素之间的空白字符会被删除。
- 文本节点中连续的空白字符被缩短成一个空格。
- 示例
app.config.compilerOptions.whitespace = 'preserve'
app.config.compilerOptions.delimiters
用于调整模板内文本插值的分隔符。
- 类型
[string, string]
- 默认
['{{', '}}']
- 详细信息此项通常是为了避免与同样使用 mustache 语法的服务器端框架发生冲突。
- 示例
// 分隔符改为ES6模板字符串样式
app.config.compilerOptions.delimiters = ['${', '}']
app.config.compilerOptions.comments
用于调整是否移除模板中的 HTML 注释。
- 类型
boolean
- 默认
false
- 详细信息默认情况下,Vue 会在生产环境移除所有注释,设置该项为
true
会强制 Vue 在生产环境也保留注释。在开发过程中,注释是始终被保留的。这个选项通常在 Vue 与其他依赖 HTML 注释的库一起使用时使用。 - 示例
app.config.compilerOptions.comments = true
app.config.globalProperties
一个用于注册能够被应用内所有组件实例访问到的全局属性的对象。
- 类型
interface AppConfig {
globalProperties: Record<string, any>
}
- 详细信息这是对 Vue 2 中
Vue.prototype
使用方式的一种替代,此写法在 Vue 3 已经不存在了。与任何全局的东西一样,应该谨慎使用。如果全局属性与组件自己的属性冲突,组件自己的属性将具有更高的优先级。 - 用法
app.config.globalProperties.msg = 'hello'
这使得 msg
在应用的任意组件模板上都可用,并且也可以通过任意组件实例的 this
访问到:
export default {
mounted() {
console.log(this.msg) // 'hello'
}
}
app.config.optionMergeStrategies
一个用于定义自定义组件选项的合并策略的对象。
- 类型
interface AppConfig {
optionMergeStrategies: Record<string, OptionMergeFunction>
}
type OptionMergeFunction = (to: unknown, from: unknown) => any
- 详细信息一些插件或库对自定义组件选项添加了支持 (通过注入全局 mixin)。这些选项在有多个不同来源时可能需要特殊的合并策略 (例如 mixin 或组件继承)。可以在
app.config.optionMergeStrategies
对象上以选项的名称作为 key,可以为一个自定义选项注册分配一个合并策略函数。合并策略函数分别接受在父实例和子实例上定义的该选项的值作为第一和第二个参数。 - 示例
const app = createApp({
// option from self
msg: 'Vue',
// option from a mixin
mixins: [
{
msg: 'Hello '
}
],
mounted() {
// 在 this.$options 上暴露被合并的选项
console.log(this.$options.msg)
}
})
// 为 `msg` 定义一个合并策略函数
app.config.optionMergeStrategies.msg = (parent, child) => {
return (parent || '') + (child || '')
}
app.mount('#app')
// 打印 'Hello Vue'
全局通用 API
version
暴露当前所使用的 Vue 版本。
- 类型
string
- 示例
import { version } from 'vue'
console.log(version)
nextTick()
等待下一次 DOM 更新刷新的工具方法。
- 类型
function nextTick(callback?: () => void): Promise<void>
- 详细信息当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
nextTick()
可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。 - 示例
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
count: 0
}
},
methods: {
async increment() {
this.count++
// DOM 还未更新
console.log(document.getElementById('counter').textContent) // 0
await nextTick()
// DOM 此时已经更新
console.log(document.getElementById('counter').textContent) // 1
}
}
}
</script>
<template>
<button id="counter" @click="increment">{{ count }}</button>
</template>
defineComponent()
在定义 Vue 组件时提供类型推导的辅助函数。
- 类型
function defineComponent(
component: ComponentOptions | ComponentOptions['setup']
): ComponentConstructor
为了便于阅读,对类型进行了简化。
- 详细信息第一个参数是一个组件选项对象。返回值将是该选项对象本身,因为该函数实际上在运行时没有任何操作,仅用于提供类型推导。注意返回值的类型有一点特别:它会是一个构造函数类型,它的实例类型是根据选项推断出的组件实例类型。这是为了能让该返回值在 TSX 中用作标签时提供类型推导支持。你可以像这样从
defineComponent()
的返回类型中提取出一个组件的实例类型 (与其选项中的this
的类型等价):
const Foo = defineComponent(/* ... */)
type FooInstance = InstanceType<typeof Foo>
- webpack Treeshaking 的注意事项因为
defineComponent()
是一个函数调用,所以它可能被某些构建工具认为会产生副作用,如 webpack。即使一个组件从未被使用,也有可能不被 tree-shake。为了告诉 webpack 这个函数调用可以被安全地 tree-shake,我们可以在函数调用之前添加一个/*#__PURE__*/
形式的注释:
export default /*#__PURE__*/ defineComponent(/* ... */)
请注意,如果你的项目中使用的是 Vite,就不需要这么做,因为 Rollup (Vite 背后在生产环境使用的打包器) 可以智能地确定 defineComponent()
实际上并没有副作用,所以无需手动注释。
defineAsyncComponent()
定义一个异步组件,它在运行时是懒加载的。参数可以是一个异步加载函数,或是对加载行为进行更具体定制的一个选项对象。
- 类型
function defineAsyncComponent(
source: AsyncComponentLoader | AsyncComponentOptions
): Component
type AsyncComponentLoader = () => Promise
interface AsyncComponentOptions {
loader: AsyncComponentLoader
loadingComponent?: Component
errorComponent?: Component
delay?: number
timeout?: number
suspensible?: boolean
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number
) => any
}
## defineCustomElement()
> 这个方法和 [`defineComponent`](https://cn.vuejs.org/api/general.html#definecomponent) 接受的参数相同,不同的是会返回一个原生[自定义元素](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)类的构造器。
- **类型**
```ts
function defineCustomElement(
component:
| (ComponentOptions & { styles?: string[] })
| ComponentOptions['setup']
): {
new (props?: object): HTMLElement
}
为了便于阅读,对类型进行了简化。
- 详细信息除了常规的组件选项,
defineCustomElement()
还支持一个特别的选项styles
,它应该是一个内联 CSS 字符串的数组,所提供的 CSS 会被注入到该元素的 shadow root 上。返回值是一个可以通过customElements.define()
注册的自定义元素构造器。 - 示例
import { defineCustomElement } from ‘vue’
const MyVueElement = defineCustomElement({
/* 组件选项 */
})
// 注册自定义元素
customElements.define(‘my-vue-element’, MyVueElement)
# Vue3 组合式API
****
## setup()
> `setup()` 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
>
> 1. 需要在非单文件组件中使用组合式 API 时。
> 2. 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
>
> **其他情况下,都应优先使用 <script serup> 语法。**
### 基本使用
> 我们可以使用[响应式 API](https://cn.vuejs.org/api/reactivity-core.html) 来声明响应式的状态,在 `setup()` 函数中返回的对象会暴露给模板和组件实例。其它的选项也可以通过组件实例来获取 `setup()` 暴露的属性:
```vue
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
// 返回值会暴露给模板和其他的选项式 API 钩子
return {
count
}
},
mounted() {
console.log(this.count) // 0
}
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
请注意在模板中访问从 setup
返回的 ref 时,它会自动浅层解包,因此你无须再在模板中为它写 .value
。当通过 this
访问时也会同样如此解包。
TIP
setup()
自身并不含对组件实例的访问权,即在setup()
中访问this
会是undefined
。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。
访问 Props
setup
函数的第一个参数是组件的props
。和标准的组件一致,一个setup
函数的props
是响应式的,并且会在传入新的 props 时同步更新。
export default {
props: {
title: String
},
setup(props) {
console.log(props.title)
}
}
请注意如果你解构了 props
对象,解构出的变量将会丢失响应性。因此我们推荐通过 props.xxx
的形式来使用其中的 props。
如果你确实需要解构 props
对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么你可以使用 toRefs() 和 toRef() 这两个工具函数:
import { toRefs, toRef } from 'vue'
export default {
setup(props) {
// 将 `props` 转为一个其中全是 ref 的对象,然后解构
const { title } = toRefs(props)
// `title` 是一个追踪着 `props.title` 的 ref
console.log(title.value)
// 或者,将 `props` 的单个属性转为一个 ref
const title = toRef(props, 'title')
}
}
Setup 上下文
传入 setup
函数的第二个参数是一个 Setup 上下文对象。上下文对象暴露了其他一些在 setup
中可能会用到的值:
export default {
setup(props, context) {
// 透传 Attributes(非响应式的对象,等价于 $attrs)
console.log(context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots)
// 触发事件(函数,等价于 $emit)
console.log(context.emit)
// 暴露公共属性(函数)
console.log(context.expose)
}
}
该上下文对象是非响应式的,可以安全地解构:
export default {
setup(props, { attrs, slots, emit, expose }) {
...
}
}
attrs
和 slots
都是有状态的对象,它们总是会随着组件自身的更新而更新。这意味着你应当避免解构它们,并始终通过 attrs.x
或 slots.x
的形式使用其中的属性。此外还需注意,和 props
不同,attrs
和 slots
的属性都不是响应式的。如果你想要基于 attrs
或 slots
的改变来执行副作用,那么你应该在 onBeforeUpdate
生命周期钩子中编写相关逻辑。
暴露公共属性
expose
函数用于显式地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将仅能访问 expose
函数暴露出的内容:
export default {
setup(props, { expose }) {
// 让组件实例处于 “关闭状态”
// 即不向父组件暴露任何东西
expose()
const publicCount = ref(0)
const privateCount = ref(0)
// 有选择地暴露局部状态
expose({ count: publicCount })
}
}
与渲染函数一起使用
setup
也可以返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:
import { h, ref } from 'vue'
export default {
setup() {
const count = ref(0)
return () => h('div', count.value)
}
}
返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说,这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题了。
我们可以通过调用 expose()
解决这个问题:
import { h, ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
const increment = () => ++count.value
expose({
increment
})
return () => h('div', count.value)
}
}
此时父组件可以通过模板引用来访问这个 increment
方法。
响应式 API:核心
ref()
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性
.value
。
- 类型
function ref<T>(value: T): Ref<UnwrapRef<T>>
interface Ref<T> {
value: T
}
- 详细信息ref 对象是可更改的,也就是说你可以为
.value
赋予新的值。它也是响应式的,即所有对.value
的操作都将被追踪,并且写操作会触发与之相关的副作用。如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。若要避免这种深层次的转换,请使用shallowRef()
来替代。 - 示例
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
computed ()
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过
.value
暴露 getter 函数的返回值。它也可以接受一个带有get
和set
函数的对象来创建一个可写的 ref 对象。
- 类型
// 只读
function computed<T>(
getter: () => T,
// 查看下方的 "计算属性调试" 链接
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写的
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
- 示例创建一个只读的计算属性 ref:
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
创建一个可写的计算属性 ref:
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
调试:
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
reactive()
返回一个对象的响应式代理。
- 类型
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
- 详细信息响应式转换是“深层”的:它会影响到所有嵌套的属性。一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。值得注意的是,当访问到某个响应式数组或
Map
这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,请使用 shallowReactive() 作替代。返回的对象以及其中嵌套的对象都会通过 ES Proxy 包裹,因此不等于源对象,建议只使用响应式代理,避免使用原始对象。 - 示例创建一个响应式对象:
const obj = reactive({ count: 0 })
obj.count++
ref 的解包:
const count = ref(1)
const obj = reactive({ count })
// ref 会被解包
console.log(obj.count === count.value) // true
// 会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
// 也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3
注意当访问到某个响应式数组或 Map
这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包:
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
将一个 ref 赋值给为一个 reactive
属性时,该 ref 会被自动解包:
const count = ref(1)
const obj = reactive({})
obj.count = count
console.log(obj.count) // 1
console.log(obj.count === count.value) // true
readonly()
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
- 类型
function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>>
- 详细信息只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与
reactive()
相同,但解包得到的值是只读的。要避免深层级的转换行为,请使用 shallowReadonly() 作替代。 - 示例
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 用来做响应性追踪
console.log(copy.count)
})
// 更改源属性会触发其依赖的侦听器
original.count++
// 更改该只读副本将会失败,并会得到一个警告
copy.count++ // warning!
watchEffect()
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
- 类型
function watchEffect(
effect: (onCleanup: OnCleanup) => void,
options?: WatchEffectOptions
): StopHandle
type OnCleanup = (cleanupFn: () => void) => void
interface WatchEffectOptions {
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
type StopHandle = () => void
- 详细信息第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。默认情况下,侦听器将在组件渲染之前执行。设置
flush: 'post'
将会使侦听器延迟到组件渲染之后再执行。详见回调的触发时机。在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置flush: 'sync'
来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。返回值是一个用来停止该副作用的函数。 - 示例
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 输出 0
count.value++
// -> 输出 1
副作用清除:
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel)
data.value = await response
})
停止侦听器:
const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()
选项:
watchEffect(() => {}, {
flush: 'post',
onTrack(e) {
debugger
},
onTrigger(e) {
debugger
}
})
watchPostEffect()
watchEffect()
使用 flush: 'post'
选项时的别名。
watchSyncEffect()
watchEffect()
使用 flush: 'sync'
选项时的别名。
watch()
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
- 类型
// 侦听单个来源
function watch<T>(
source: WatchSource<T>,
callback: WatchCallback<T>,
options?: WatchOptions
): StopHandle
// 侦听多个来源
function watch<T>(
sources: WatchSource<T>[],
callback: WatchCallback<T[]>,
options?: WatchOptions
): StopHandle
type WatchCallback<T> = (
value: T,
oldValue: T,
onCleanup: (cleanupFn: () => void) => void
) => void
type WatchSource<T> =
| Ref<T> // ref
| (() => T) // getter
| T extends object
? T
: never // 响应式对象
interface WatchOptions extends WatchEffectOptions {
immediate?: boolean // 默认:false
deep?: boolean // 默认:false
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
为了便于阅读,对类型进行了简化。
- 详细信息
watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。第一个参数是侦听器的源。这个来源可以是以下几种:
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。
第三个可选的参数是一个对象,支持以下这些选项:
与 watchEffect()
相比,watch()
使我们可以:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- …或是由以上类型的值组成的数组
immediate
:在侦听器创建时立即触发回调。第一次调用时旧值是undefined
。deep
:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush
:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()
。onTrack / onTrigger
:调试侦听器的依赖。参考调试侦听器。- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的前一个值和当前值。
- 示例侦听一个 getter 函数:
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
侦听一个 ref:
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
当使用 getter 函数作为源时,回调只在此函数的返回值变化时才会触发。如果你想让回调在深层级变更时也能触发,你需要使用 { deep: true }
强制侦听器进入深层级模式。在深层级模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。
const state = reactive({ count: 0 })
watch(
() => state,
(newValue, oldValue) => {
// newValue === oldValue
},
{ deep: true }
)
当直接侦听一个响应式对象时,侦听器会自动启用深层模式:
const state = reactive({ count: 0 })
watch(state, () => {
/* 深层级变更状态所触发的回调 */
})
watch()
和 watchEffect()
享有相同的刷新时机和调试选项:
watch(source, callback, {
flush: 'post',
onTrack(e) {
debugger
}
})
响应式 API:工具函数
isRef()
检查某个值是否为 ref。
- 类型
function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
请注意,返回值是一个类型判定 (type predicate),这意味着 isRef
可以被用作类型守卫:
let foo: unknown
if (isRef(foo)) {
// foo 的类型被收窄为了 Ref<unknown>
foo.value
}
unref()
如果参数是 ref,则返回内部值,否则返回参数本身。这是
val = isRef(val) ? val.value : val
计算的一个语法糖。
- 类型
function unref<T>(ref: T | Ref<T>): T
- 示例
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x)
// unwrapped 现在保证为 number 类型
}
toRef()
基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
- 类型
function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]>
type ToRef<T> = T extends Ref ? T : Ref<T>
- 示例
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2
// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3
请注意,这不同于:
const fooRef = ref(state.foo)
上面这个 ref 不会和 state.foo
保持同步,因为这个 ref()
接收到的是一个纯数值。toRef()
这个函数在你想把一个 prop 的 ref 传递给一个组合式函数时会很有用:
<script setup>
import { toRef } from 'vue'
const props = defineProps(/* ... */)
// 将 `props.foo` 转换为 ref,然后传入
// 一个组合式函数
useSomeFeature(toRef(props, 'foo'))
</script>
当 toRef
与组件 props 结合使用时,关于禁止对 props 做出更改的限制依然有效。尝试将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。在这种场景下,你可能可以考虑使用带有 get
和 set
的 computed
替代。详情请见在组件上使用 v-model
指南。即使源属性当前不存在,toRef()
也会返回一个可用的 ref。这让它在处理可选 props 的时候格外实用,相比之下 toRefs
就不会为可选 props 创建对应的 refs。
toRefs()
将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用
toRef()
创建的。
- 类型
function toRefs<T extends object>(
object: T
): {
[K in keyof T]: ToRef<T[K]>
}
type ToRef = T extends Ref ? T : Ref<T>
- 示例
const state = reactive({
foo: 1,
bar: 2
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:{
foo: Ref<number>,
bar: Ref<number>
}
*/
// 这个 ref 和源属性已经“链接上了”
state.foo++
console.log(stateAsRefs.foo.value) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
当从组合式函数中返回响应式对象时,toRefs
相当有用。使用它,消费者组件可以解构/展开返回的对象而不会失去响应性:
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})
// ...基于状态的操作逻辑
// 在返回时都转为 ref
return toRefs(state)
}
// 可以解构而不会失去响应性
const { foo, bar } = useFeatureX()
toRefs
在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef
。
isProxy()
检查一个对象是否是由
reactive()
、readonly()
、shallowReactive()
或shallowReadonly()
创建的代理。
- 类型
function isProxy(value: unknown): boolean
isReactive()
检查一个对象是否是由
reactive()
或shallowReactive()
创建的代理。
- 类型
function isReactive(value: unknown): boolean
isReadonly()
检查一个对象是否是由
readonly()
或shallowReadonly()
创建的代理。
- 类型
function isReadonly(value: unknown): boolean
响应式 API:进阶
shallowRef()
ref()
的浅层作用形式。
- 类型
function shallowRef<T>(value: T): ShallowRef<T>
interface ShallowRef<T> {
value: T
}
- 详细信息和
ref()
不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对.value
的访问是响应式的。shallowRef()
常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。 - 示例
const state = shallowRef({ count: 1 })
// 不会触发更改
state.value.count = 2
// 会触发更改
state.value = { count: 2 }
triggerRef()
强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。
- 类型
function triggerRef(ref: ShallowRef): void
- 示例
const shallow = shallowRef({
greet: 'Hello, world'
})
// 触发该副作用第一次应该会打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 这次变更不应触发副作用,因为这个 ref 是浅层的
shallow.value.greet = 'Hello, universe'
// 打印 "Hello, universe"
triggerRef(shallow)
customRef()
创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。
- 类型
function customRef<T>(factory: CustomRefFactory<T>): Ref<T>
type CustomRefFactory<T> = (
track: () => void,
trigger: () => void
) => {
get: () => T
set: (value: T) => void
}
- 详细信息
customRef()
预期接收一个工厂函数作为参数,这个工厂函数接受track
和trigger
两个函数作为参数,并返回一个带有get
和set
方法的对象。一般来说,track()
应该在get()
方法中调用,而trigger()
应该在set()
中调用。然而事实上,你对何时调用、是否应该调用他们有完全的控制权。 - 示例创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:
import { customRef } from 'vue'
export function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()
}, delay)
}
}
})
}
在组件中使用:
<script setup>
import { useDebouncedRef } from './debouncedRef'
const text = useDebouncedRef('hello')
</script>
<template>
<input v-model="text" />
</template>
shallowReactive()
reactive()
的浅层作用形式。
- 类型
function shallowReactive<T extends object>(target: T): T
- 详细信息和
reactive()
不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。谨慎使用浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。 - 示例
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性是响应式的
state.foo++
// ...但下层嵌套对象不会被转为响应式
isReactive(state.nested) // false
// 不是响应式的
state.nested.bar++
shallowReadonly()
readonly()
的浅层作用形式
- 类型
function shallowReadonly<T extends object>(target: T): Readonly<T>
- 详细信息和
readonly()
不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。谨慎使用浅层数据结构应该只用于组件中的根级状态。请避免将其嵌套在深层次的响应式对象中,因为它创建的树具有不一致的响应行为,这可能很难理解和调试。 - 示例
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 更改状态自身的属性会失败
state.foo++
// ...但可以更改下层嵌套对象
isReadonly(state.nested) // false
// 这是可以通过的
state.nested.bar++
toRaw()
根据一个 Vue 创建的代理返回其原始对象。
- 类型
function toRaw<T>(proxy: T): T
- 详细信息
toRaw()
可以返回由reactive()
、readonly()
、shallowReactive()
或者shallowReadonly()
创建的代理对应的原始对象。这是一个可以用于临时读取而不引起代理访问/跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。 - 示例
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
markRaw()
将一个对象标记为不可被转为代理。返回该对象本身。
- 类型
function markRaw<T extends object>(value: T): T
- 示例
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 也适用于嵌套在其他响应性对象
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
谨慎使用markRaw()
和类似 shallowReactive()
这样的浅层式 API 使你可以有选择地避开默认的深度响应/只读转换,并在状态关系谱中嵌入原始的、非代理的对象。它们可能出于各种各样的原因被使用:
这应该是一种进阶需求,因为只在根层访问能到原始值,所以如果把一个嵌套的、没有标记的原始对象设置成一个响应式对象,然后再次访问它,你获取到的是代理的版本。这可能会导致对象身份风险,即执行一个依赖于对象身份的操作,但却同时使用了同一对象的原始版本和代理版本:
js
const foo = markRaw({
nested: {}
})
const bar = reactive({
// 尽管 `foo` 被标记为了原始对象,但 foo.nested 却没有
nested: foo.nested
})
console.log(foo.nested === bar.nested) // false
识别风险一般是很罕见的。然而,要正确使用这些 API,同时安全地避免这样的风险,需要你对响应性系统的工作方式有充分的了解。
- 有些值不应该是响应式的,例如复杂的第三方类实例或 Vue 组件对象。
- 当呈现带有不可变数据源的大型列表时,跳过代理转换可以提高性能。
effectScope()
创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。对于该 API 的使用细节,请查阅对应的 RFC。
- 类型
function effectScope(detached?: boolean): EffectScope
interface EffectScope {
run<T>(fn: () => T): T | undefined // 如果作用域不活跃就为 undefined
stop(): void
}
- 示例
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()
getCurrentScope()
如果有的话,返回当前活跃的 effect 作用域。
- 类型
function getCurrentScope(): EffectScope | undefined
onScopeDispose()
在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
这个方法可以作为可复用的组合式函数中 onUnmounted
的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup()
函数也是在一个 effect 作用域中调用的。
- 类型
function onScopeDispose(fn: () => void): void
组合式 API:生命周期钩子
onBeforeMount()
注册一个钩子,在组件被挂载之前被调用。
- 类型
function onBeforeMount(callback: () => void): void
- 详细信息当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。这个钩子在服务器端渲染期间不会被调用。
onMounted()
注册一个回调函数,在组件挂载完成后执行。
- 类型
function onMounted(callback: () => void): void
- 详细信息组件在以下情况下被视为已挂载:
这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端执行。
这个钩子在服务器端渲染期间不会被调用。
- 其所有同步子组件都已经被挂载 (不包含异步组件或
<Suspense>
树内的组件)。 - 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
- 示例通过模板引用访问一个元素:
<script setup>
import { ref, onMounted } from 'vue'
const el = ref()
onMounted(() => {
el.value // <div>
})
</script>
<template>
<div ref="el"></div>
</template>
onBeforeUpdate()
注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
- 类型
function onBeforeUpdate(callback: () => void): void
- 详细信息这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。这个钩子在服务器端渲染期间不会被调用。
onUpdated()
注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
- 类型
function onUpdated(callback: () => void): void
- 详细信息父组件的更新钩子将在其子组件的更新钩子之后调用。这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。这个钩子在服务器端渲染期间不会被调用。
WARNING
不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!
- 示例访问更新后的 DOM
<script setup>
import { ref, onUpdated } from 'vue'
const count = ref(0)
onUpdated(() => {
// 文本内容应该与当前的 `count.value` 一致
console.log(document.getElementById('count').textContent)
})
</script>
<template>
<button id="count" @click="count++">{{ count }}</button>
</template>
onBeforeUnmount()
注册一个钩子,在组件实例被卸载之前调用。
- 类型
function onBeforeUnmount(callback: () => void): void
- 详细信息当这个钩子被调用时,组件实例依然还保有全部的功能。这个钩子在服务器端渲染期间不会被调用。
onUnmounted()
注册一个回调函数,在组件实例被卸载之后调用。
- 类型
function onUnmounted(callback: () => void): void
- 详细信息一个组件在以下情况下被视为已卸载:
可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
这个钩子在服务器端渲染期间不会被调用。
- 其所有子组件都已经被卸载。
- 所有相关的响应式作用 (渲染作用以及
setup()
时创建的计算属性和侦听器) 都已经停止。 - 示例
<script setup>
import { onMounted, onUnmounted } from 'vue'
let intervalId
onMounted(() => {
intervalId = setInterval(() => {
// ...
})
})
onUnmounted(() => clearInterval(intervalId))
</script>
onErrorCaptured()
注册一个钩子,在捕获了后代组件传递的错误时调用。
- 类型
function onErrorCaptured(callback: ErrorCapturedHook): void
type ErrorCapturedHook = (
err: unknown,
instance: ComponentPublicInstance | null,
info: string
) => boolean | void
- 详细信息错误可以从以下几个来源中捕获:
这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。
你可以在 errorCaptured()
中更改组件状态来为用户显示一个错误状态。注意不要让错误状态再次渲染导致本次错误的内容,否则组件会陷入无限循环。
这个钩子可以通过返回 false
来阻止错误继续向上传递。请看下方的传递细节介绍。
错误传递规则
- 组件渲染
- 事件处理器
- 生命周期钩子
setup()
函数- 侦听器
- 自定义指令钩子
- 过渡钩子
- 默认情况下,所有的错误都会被发送到应用级的
app.config.errorHandler
(前提是这个函数已经定义),这样这些错误都能在一个统一的地方报告给分析服务。 - 如果组件的继承链或组件链上存在多个
errorCaptured
钩子,对于同一个错误,这些钩子会被按从底至上的顺序一一调用。这个过程被称为“向上传递”,类似于原生 DOM 事件的冒泡机制。 - 如果
errorCaptured
钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送到app.config.errorHandler
。 errorCaptured
钩子可以通过返回false
来阻止错误继续向上传递。即表示“这个错误已经被处理了,应当被忽略”,它将阻止其他的errorCaptured
钩子或app.config.errorHandler
因这个错误而被调用。
onRenderTracked()Dev only
注册一个调试钩子,当组件渲染过程中追踪到响应式依赖时调用。
这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。
- 类型
function onRenderTracked(callback: DebuggerHook): void
type DebuggerHook = (e: DebuggerEvent) => void
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TrackOpTypes /* 'get' | 'has' | 'iterate' */
key: any
}
onRenderTriggered()Dev only
注册一个调试钩子,当响应式依赖的变更触发了组件渲染时调用。
这个钩子仅在开发模式下可用,且在服务器端渲染期间不会被调用。
- 类型
function onRenderTriggered(callback: DebuggerHook): void
type DebuggerHook = (e: DebuggerEvent) => void
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
onServerPrefetch()SSR only
注册一个异步函数,在组件实例在服务器上被渲染之前调用。
- 类型
function onServerPrefetch(callback: () => Promise<any>): void
- 详细信息如果这个钩子返回了一个 Promise,服务端渲染会在渲染该组件前等待该 Promise 完成。这个钩子仅会在服务端渲染中执行,可以用于执行一些仅存在于服务端的数据抓取过程。
- 示例
<script setup>
import { ref, onServerPrefetch, onMounted } from 'vue'
const data = ref(null)
onServerPrefetch(async () => {
// 组件作为初始请求的一部分被渲染
// 在服务器上预抓取数据,因为它比在客户端上更快。
data.value = await fetchOnServer(/* ... */)
})
onMounted(async () => {
if (!data.value) {
// 如果数据在挂载时为空值,这意味着该组件
// 是在客户端动态渲染的。将转而执行
// 另一个客户端侧的抓取请求
data.value = await fetchOnClient(/* ... */)
}
})
</script>
onActivated()
注册一个回调函数,若组件实例是
<keepAlive>
缓存树的一部分,当组件被插入到 DOM 中时调用。
这个钩子在服务器端渲染期间不会被调用。
- 类型
function onActivated(callback: () => void): void
onDeactivated()
注册一个回调函数,若组件实例是
<keepAlive>
缓存树的一部分,当组件从 DOM 中被移除时调用。
这个钩子在服务器端渲染期间不会被调用。
- 类型
function onDeactivated(callback: () => void): void
组合式 API:依赖注入
provide()
提供一个值,可以被后代组件注入。
- 类型
function provide<T>(key: InjectionKey<T> | string, value: T): void
- 详细信息
provide()
接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。当使用 TypeScript 时,key 可以是一个被类型断言为InjectionKey
的 symbol。InjectionKey
是一个 Vue 提供的工具类型,继承自Symbol
,可以用来同步provide()
和inject()
之间值的类型。与注册生命周期钩子的 API 类似,provide()
必须在组件的setup()
阶段同步调用。 - 示例
<script setup>
import { ref, provide } from 'vue'
import { fooSymbol } from './injectionSymbols'
// 提供静态值
provide('foo', 'bar')
// 提供响应式的值
const count = ref(0)
provide('count', count)
// 提供时将 Symbol 作为 key
provide(fooSymbol, count)
</script>
inject()
注入一个由祖先组件或整个应用 (通过
app.provide()
) 提供的值。
- 类型
// 没有默认值
function inject<T>(key: InjectionKey<T> | string): T | undefined
// 带有默认值
function inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
// 使用工厂函数
function inject<T>(
key: InjectionKey<T> | string,
defaultValue: () => T,
treatDefaultAsFactory: true
): T
- 详细信息第一个参数是注入的 key。Vue 会遍历父组件链,通过匹配 key 来确定所提供的值。如果父组件链上多个组件对同一个 key 提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过 key 匹配到值,
inject()
将返回undefined
,除非提供了一个默认值。第二个参数是可选的,即在没有匹配到 key 时使用的默认值。它也可以是一个工厂函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么你必须将false
作为第三个参数传入,表明这个函数就是默认值,而不是一个工厂函数。与注册生命周期钩子的 API 类似,inject()
必须在组件的setup()
阶段同步调用。当使用 TypeScript 时,key 可以是一个类型为InjectionKey
的 symbol。InjectionKey
是一个 Vue 提供的工具类型,继承自Symbol
,可以用来同步provide()
和inject()
之间值的类型。 - 示例假设有一个父组件已经提供了一些值,如前面
provide()
的例子中所示:
<script setup>
import { inject } from 'vue'
import { fooSymbol } from './injectionSymbols'
// 注入值的默认方式
const foo = inject('foo')
// 注入响应式的值
const count = inject('count')
// 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol)
// 注入一个值,若为空则使用提供的默认值
const bar = inject('foo', 'default value')
// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('foo', () => new Map())
// 注入时为了表明提供的默认值是个函数,需要传入第三个参数
const fn = inject('function', () => {}, false)
</script>
Vue3 选项式API
状态选项
data
用于声明组件初始响应式状态的函数。
- 类型
interface ComponentOptions {
data?(
this: ComponentPublicInstance,
vm: ComponentPublicInstance
): object
}
- 详细信息该函数应当返回一个普通 JavaScript 对象,Vue 会将它转换为响应式对象。实例创建后,可以通过
this.$data
访问该响应式对象。组件实例也代理了该数据对象上所有的属性,因此this.a
等价于this.$data.a
。所有会用到的顶层数据属性都应该提前在这个对象中声明。虽然理论上可以向this.$data
添加新属性,但并不推荐这么做。如果一个属性的值在一开始还获取不到,应当先用undefined
或是null
值来占位,让 Vue 知道这个属性是存在的。以_
或$
开头的属性将不会被组件实例代理,因为它们可能和 Vue 的内置属性、API 方法冲突。你必须以this.$data._property
的方式访问它们。不推荐返回一个可能改变自身状态的对象,如浏览器 API 原生对象或是带原型的类实例等。理想情况下,返回的对象应是一个纯粹代表组件状态的普通对象。 - 示例
export default {
data() {
return { a: 1 }
},
created() {
console.log(this.a) // 1
console.log(this.$data) // { a: 1 }
}
}
注意,如果你为 data
属性使用了一个箭头函数,则 this
将不会指向该组件实例,不过你仍然可以通过该函数的第一个参数来访问实例:
data: (vm) => ({ a: vm.myProp })
props
用于声明一个组件的 props。
- 类型
interface ComponentOptions {
props?: ArrayPropsOptions | ObjectPropsOptions
}
type ArrayPropsOptions = string[]
type ObjectPropsOptions = { [key: string]: Prop }
type Prop<T = any> = PropOptions<T> | PropType<T> | null
interface PropOptions<T> {
type?: PropType<T>
required?: boolean
default?: T | ((rawProps: object) => T)
validator?: (value: unknown) => boolean
}
type PropType<T> = { new (): T } | { new (): T }[]
为了便于阅读,对类型进行了简化。
- 详细信息在 Vue 中,所有的组件 props 都需要被显式声明。组件 props 可以通过两种方式声明:
在基于对象的语法中,每个 prop 可以进一步定义如下选项:
- 使用字符串数组的简易形式。
- 使用对象的完整形式。该对象的每个属性键是对应 prop 的名称,值则是该 prop 应具有的类型的构造函数,或是更高级的选项。
type
:可以是下列原生构造函数之一:String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
、任何自定义构造函数,或由上述内容组成的数组。在开发模式中,Vue 会检查一个 prop 的值是否匹配其声明的类型,如果不匹配则会抛出警告。详见 Prop 校验。还要注意,一个Boolean
类型的 prop 会影响它在开发或生产模式下的值转换行为。详见 Boolean 类型转换。default
:为该 prop 指定一个当其没有被传入或值为undefined
时的默认值。对象或数组的默认值必须从一个工厂函数返回。工厂函数也接收原始 prop 对象作为参数。required
:定义该 prop 是否必需传入。在非生产环境中,如果 required 值为真值且 prop 未被传入,一个控制台警告将会被抛出。validator
:将 prop 值作为唯一参数传入的自定义验证函数。在开发模式下,如果该函数返回一个假值 (即验证失败),一个控制台警告将会被抛出。- 示例简易声明:
export default {
props: ['size', 'myMessage']
}
对象声明,带有验证:
export default {
props: {
// 类型检查
height: Number,
// 类型检查 + 其他验证
age: {
type: Number,
default: 0,
required: true,
validator: (value) => {
return value >= 0
}
}
}
}
computed
用于声明要在组件实例上暴露的计算属性。
- 类型
interface ComponentOptions {
computed?: {
[key: string]: ComputedGetter<any> | WritableComputedOptions<any>
}
}
type ComputedGetter<T> = (
this: ComponentPublicInstance,
vm: ComponentPublicInstance
) => T
type ComputedSetter<T> = (
this: ComponentPublicInstance,
value: T
) => void
type WritableComputedOptions<T> = {
get: ComputedGetter<T>
set: ComputedSetter<T>
}
- 详细信息该选项接收一个对象,其中键是计算属性的名称,值是一个计算属性 getter,或一个具有
get
和set
方法的对象 (用于声明可写的计算属性)。所有的 getters 和 setters 会将它们的this
上下文自动绑定为组件实例。注意,如果你为一个计算属性使用了箭头函数,则this
不会指向该组件实例,不过你仍然可以通过该函数的第一个参数来访问实例:
export default {
computed: {
aDouble: (vm) => vm.a * 2
}
}
- 示例
export default {
data() {
return { a: 1 }
},
computed: {
// 只读
aDouble() {
return this.a * 2
},
// 可写
aPlus: {
get() {
return this.a + 1
},
set(v) {
this.a = v - 1
}
}
},
created() {
console.log(this.aDouble) // => 2
console.log(this.aPlus) // => 2
this.aPlus = 3
console.log(this.a) // => 2
console.log(this.aDouble) // => 4
}
}
methods
用于声明要混入到组件实例中的方法。
- 类型
interface ComponentOptions {
methods?: {
[key: string]: (this: ComponentPublicInstance, ...args: any[]) => any
}
}
- 详细信息声明的方法可以直接通过组件实例访问,或者在模板语法表达式中使用。所有的方法都会将它们的
this
上下文自动绑定为组件实例,即使在传递时也如此。在声明方法时避免使用箭头函数,因为它们不能通过this
访问组件实例。 - 示例
export default {
data() {
return { a: 1 }
},
methods: {
plus() {
this.a++
}
},
created() {
this.plus()
console.log(this.a) // => 2
}
}
watch
用于声明在数据更改时调用的侦听回调。
- 类型
interface ComponentOptions {
watch?: {
[key: string]: WatchOptionItem | WatchOptionItem[]
}
}
type WatchOptionItem = string | WatchCallback | ObjectWatchOptionItem
type WatchCallback<T> = (
value: T,
oldValue: T,
onCleanup: (cleanupFn: () => void) => void
) => void
type ObjectWatchOptionItem = {
handler: WatchCallback | string
immediate?: boolean // default: false
deep?: boolean // default: false
flush?: 'pre' | 'post' | 'sync' // default: 'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
为了便于阅读,对类型进行了简化。
- 详细信息
watch
选项期望接受一个对象,其中键是需要侦听的响应式组件实例属性 (例如,通过data
或computed
声明的属性)——值是相应的回调函数。该回调函数接受被侦听源的新值和旧值。除了一个根级属性,键名也可以是一个简单的由点分隔的路径,例如a.b.c
。注意,这种用法不支持复杂表达式——仅支持由点分隔的路径。如果你需要侦听复杂的数据源,可以使用命令式的$watch()
API。值也可以是一个方法名称的字符串 (通过methods
声明),或包含额外选项的对象。当使用对象语法时,回调函数应被声明在handler
中。额外的选项包含:
声明侦听器回调时避免使用箭头函数,因为它们将无法通过 this
访问组件实例。
immediate
:在侦听器创建时立即触发回调。第一次调用时,旧值将为undefined
。deep
:如果源是对象或数组,则强制深度遍历源,以便在深度变更时触发回调。详见深层侦听器。flush
:调整回调的刷新时机。详见回调的触发时机及watchEffect()
。onTrack / onTrigger
:调试侦听器的依赖关系。详见侦听器调试。- 示例
export default {
data() {
return {
a: 1,
b: 2,
c: {
d: 4
},
e: 5,
f: 6
}
},
watch: {
// 侦听根级属性
a(val, oldVal) {
console.log(`new: ${val}, old: ${oldVal}`)
},
// 字符串方法名称
b: 'someMethod',
// 该回调将会在被侦听的对象的属性改变时调动,无论其被嵌套多深
c: {
handler(val, oldVal) {
console.log('c changed')
},
deep: true
},
// 侦听单个嵌套属性:
'c.d': function (val, oldVal) {
// do something
},
// 该回调将会在侦听开始之后立即调用
e: {
handler(val, oldVal) {
console.log('e changed')
},
immediate: true
},
// 你可以传入回调数组,它们将会被逐一调用
f: [
'handle1',
function handle2(val, oldVal) {
console.log('handle2 triggered')
},
{
handler: function handle3(val, oldVal) {
console.log('handle3 triggered')
}
/* ... */
}
]
},
methods: {
someMethod() {
console.log('b changed')
},
handle1() {
console.log('handle 1 triggered')
}
},
created() {
this.a = 3 // => new: 3, old: 1
}
}
emits
用于声明由组件触发的自定义事件。
- 类型
interface ComponentOptions {
emits?: ArrayEmitsOptions | ObjectEmitsOptions
}
type ArrayEmitsOptions = string[]
type ObjectEmitsOptions = { [key: string]: EmitValidator | null }
type EmitValidator = (...args: unknown[]) => boolean
- 详细信息可以以两种形式声明触发的事件:
验证函数会接收到传递给组件的 $emit
调用的额外参数。例如,如果 this.$emit('foo', 1)
被调用,foo
相应的验证函数将接受参数 1
。验证函数应返回布尔值,以表明事件参数是否通过了验证。
注意,emits
选项会影响一个监听器被解析为组件事件监听器,还是原生 DOM 事件监听器。被声明为组件事件的监听器不会被透传到组件的根元素上,且将从组件的 $attrs
对象中移除。详见透传 Attributes。
- 使用字符串数组的简易形式。
- 使用对象的完整形式。该对象的每个属性键是事件的名称,值是
null
或一个验证函数。 - 示例数组语法:
export default {
emits: ['check'],
created() {
this.$emit('check')
}
}
对象语法:
export default {
emits: {
// 没有验证函数
click: null,
// 具有验证函数
submit: (payload) => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
}
expose
用于声明当组件实例被父组件通过模板引用访问时暴露的公共属性。
- 类型
interface ComponentOptions {
expose?: string[]
}
- 详细信息默认情况下,当通过
$parent
、$root
或模板引用访问时,组件实例将向父组件暴露所有的实例属性。这可能不是我们希望看到的,因为组件很可能拥有一些应保持私有的内部状态或方法,以避免紧耦合。expose
选项值应当是一个包含要暴露的属性名称字符串的数组。当使用expose
时,只有显式列出的属性将在组件实例上暴露。expose
仅影响用户定义的属性——它不会过滤掉内置的组件实例属性。 - 示例
export default {
// 只有 `publicMethod` 在公共实例上可用
expose: ['publicMethod'],
methods: {
publicMethod() {
// ...
},
privateMethod() {
// ...
}
}
}
渲染选项
template
用于声明组件的字符串模板。
- 类型
interface ComponentOptions {
template?: string
}
- 详细信息通过
template
选项提供的模板将会在运行时即时编译。这仅在使用了包含模板编译器的 Vue 构建版本的情况下支持。文件名中带有runtime
的 Vue 构建版本未包含模板编译器,例如vue.runtime.esm-bundler.js
。请查阅构建文件指南了解不同构建版本之间的详细区别。如果该字符串以#
开头,它将被用作querySelector
的选择器,并使用所选中元素的innerHTML
作为模板字符串。这让我们能够使用原生<template>
元素来书写源模板。如果render
选项也同时存在于该组件中,template
将被忽略。如果应用的根组件不含任何template
或render
选项,Vue 将会尝试使用所挂载元素的innerHTML
来作为模板。安全性注意务必只使用可以信任的模板来源。不要直接将用户提供的内容用作模板。查看安全指南了解更多细节。
render
用于编程式地创建组件虚拟 DOM 树的函数。
- 类型
interface ComponentOptions {
render?(this: ComponentPublicInstance) => VNodeChild
}
type VNodeChild = VNodeChildAtom | VNodeArrayChildren
type VNodeChildAtom =
| VNode
| string
| number
| boolean
| null
| undefined
| void
type VNodeArrayChildren = (VNodeArrayChildren | VNodeChildAtom)[]
- 详细信息
render
是字符串模板的一种替代,可以使你利用 JavaScript 的丰富表达力来完全编程式地声明组件最终的渲染输出。预编译的模板,例如单文件组件中的模板,会在构建时被编译为render
选项。如果一个组件中同时存在render
和template
,则render
将具有更高的优先级。
compilerOptions
用于配置组件模板的运行时编译器选项。
- 类型
interface ComponentOptions {
compilerOptions?: {
isCustomElement?: (tag: string) => boolean
whitespace?: 'condense' | 'preserve' // 默认:'condense'
delimiters?: [string, string] // 默认:['{{', '}}']
comments?: boolean // 默认:false
}
}
- 详细信息这个配置选项仅在使用完整构建版本 (即可以在浏览器中编译模板的
vue.js
文件) 时才有效。它支持与应用级的 app.config.compilerOptions 相同的选项,并针对当前组件有更高的优先级。
生命周期选项
beforeCreate
在组件实例初始化完成之后立即调用。
- 类型
interface ComponentOptions {
beforeCreate?(this: ComponentPublicInstance): void
}
- 详细信息会在实例初始化完成、props 解析之后、
data()
和computed
等选项处理之前立即调用。注意,组合式 API 中的setup()
钩子会在所有选项式 API 钩子之前调用,beforeCreate()
也不例外。
created
在组件实例处理完所有与状态相关的选项后调用。
- 类型
interface ComponentOptions {
created?(this: ComponentPublicInstance): void
}
- 详细信息当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此
$el
属性仍不可用。
beforeMount
在组件被挂载之前调用。
- 类型
interface ComponentOptions {
beforeMount?(this: ComponentPublicInstance): void
}
- 详细信息当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。这个钩子在服务端渲染时不会被调用。
mounted
在组件被挂载之后调用。
- 类型
interface ComponentOptions {
mounted?(this: ComponentPublicInstance): void
}
- 详细信息组件在以下情况下被视为已挂载:
这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端被调用。
这个钩子在服务端渲染时不会被调用。
- 所有同步子组件都已经被挂载。(不包含异步组件或
<Suspense>
树内的组件) - 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
beforeUpdate
在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。
- 类型
interface ComponentOptions {
beforeUpdate?(this: ComponentPublicInstance): void
}
- 详细信息这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。这个钩子在服务端渲染时不会被调用。
updated
在组件因为一个响应式状态变更而更新其 DOM 树之后调用。
- 类型
interface ComponentOptions {
updated?(this: ComponentPublicInstance): void
}
- 详细信息父组件的更新钩子将在其子组件的更新钩子之后调用。这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。这个钩子在服务端渲染时不会被调用。
WARNING
不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!
beforeUnmount
在一个组件实例被卸载之前调用。
- 类型
interface ComponentOptions {
beforeUnmount?(this: ComponentPublicInstance): void
}
- 详细信息当这个钩子被调用时,组件实例依然还保有全部的功能。这个钩子在服务端渲染时不会被调用。
unmounted
在一个组件实例被卸载之后调用。
- 类型
interface ComponentOptions {
unmounted?(this: ComponentPublicInstance): void
}
- 详细信息一个组件在以下情况下被视为已卸载:
可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
这个钩子在服务端渲染时不会被调用。
- 其所有子组件都已经被卸载。
- 所有相关的响应式作用 (渲染作用以及
setup()
时创建的计算属性和侦听器) 都已经停止。
errorCaptured
在捕获了后代组件传递的错误时调用。
- 类型
interface ComponentOptions {
errorCaptured?(
this: ComponentPublicInstance,
err: unknown,
instance: ComponentPublicInstance | null,
info: string
): boolean | void
}
- 详细信息错误可以从以下几个来源中捕获:
这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。
你可以在 errorCaptured()
中更改组件状态来为用户显示一个错误状态。然而重要的是,不要让错误状态渲染为导致本次错误的内容,否则组件就会进入无限的渲染循环中。
这个钩子可以通过返回 false
来阻止错误继续向上传递。请看下方的传递细节介绍。
错误传递规则
- 组件渲染
- 事件处理器
- 生命周期钩子
setup()
函数- 侦听器
- 自定义指令钩子
- 过渡钩子
- 默认情况下,所有的错误都会被发送到应用级的
app.config.errorHandler
(前提是这个函数已经定义),这样这些错误都能在一个统一的地方报告给分析服务。 - 如果组件的继承链或组件链上存在多个
errorCaptured
钩子,对于同一个错误,这些钩子会被按从底至上的顺序一一调用。这个过程被称为“向上传递”,类似于原生 DOM 事件的冒泡机制。 - 如果
errorCaptured
钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送到app.config.errorHandler
。 errorCaptured
钩子可以通过返回false
来阻止错误继续向上传递。即表示“这个错误已经被处理了,应当被忽略”,它将阻止其他的errorCaptured
钩子或app.config.errorHandler
因这个错误而被调用。
renderTracked Dev only
在一个响应式依赖被组件的渲染作用追踪后调用。
- 类型
interface ComponentOptions {
renderTracked?(this: ComponentPublicInstance, e: DebuggerEvent): void
}
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TrackOpTypes /* 'get' | 'has' | 'iterate' */
key: any
}
renderTriggeredDev only
在一个响应式依赖被组件触发了重新渲染之后调用。
- 类型
interface ComponentOptions {
renderTriggered?(this: ComponentPublicInstance, e: DebuggerEvent): void
}
type DebuggerEvent = {
effect: ReactiveEffect
target: object
type: TriggerOpTypes /* 'set' | 'add' | 'delete' | 'clear' */
key: any
newValue?: any
oldValue?: any
oldTarget?: Map<any, any> | Set<any>
}
activated
若组件实例是
<keepAlive>
缓存树的一部分,当组件被插入到 DOM 中时调用。
这个钩子在服务端渲染时不会被调用。
- 类型
interface ComponentOptions {
activated?(this: ComponentPublicInstance): void
}
deactivated
若组件实例是
<keepAlive>
缓存树的一部分,当组件从 DOM 中被移除时调用。
这个钩子在服务端渲染时不会被调用。
- 类型
interface ComponentOptions {
deactivated?(this: ComponentPublicInstance): void
}
serverPrefetchSSR only
当组件实例在服务器上被渲染之前要完成的异步函数。
- 类型
interface ComponentOptions {
serverPrefetch?(this: ComponentPublicInstance): Promise<any>
}
- 详细信息如果这个钩子返回了一个 Promise,服务端渲染会在渲染该组件前等待该 Promise 完成。这个钩子仅会在服务端渲染中执行,可以用于执行一些仅在服务端才有的数据抓取过程。
- 示例
export default {
data() {
return {
data: null
}
},
async serverPrefetch() {
// 组件会作为初次请求的一部分被渲染
// 会在服务端预抓取数据,因为这比客户端更快
this.data = await fetchOnServer(/* ... */)
},
async mounted() {
if (!this.data) {
// 如果数据在挂载时是 null,这意味着这个组件
// 是在客户端动态渲染的,请另外执行一个
// 客户端请求作为替代
this.data = await fetchOnClient(/* ... */)
}
}
}
组合选项
provide
用于提供可以被后代组件注入的值。
- 类型
interface ComponentOptions {
provide?: object | ((this: ComponentPublicInstance) => object)
}
- 详细信息
provide
和inject
通常成对一起使用,使一个祖先组件作为其后代组件的依赖注入方,无论这个组件的层级有多深都可以注入成功,只要他们处于同一条组件链上。这个provide
选项应当是一个对象或是返回一个对象的函数。这个对象包含了可注入其后代组件的属性。你可以在这个对象中使用 Symbol 类型的值作为 key。 - 示例基本使用方式:
const s = Symbol()
export default {
provide: {
foo: 'foo',
[s]: 'bar'
}
}
使用函数可以提供其组件中的状态:
export default {
data() {
return {
msg: 'foo'
}
}
provide() {
return {
msg: this.msg
}
}
}
请注意,针对上面这个例子,所供给的 msg
将不会是响应式的。请查看和响应式数据配合使用一节获取更多细节。
inject
用于声明要通过从上层提供方匹配并注入进当前组件的属性。
- 类型
interface ComponentOptions {
inject?: ArrayInjectOptions | ObjectInjectOptions
}
type ArrayInjectOptions = string[]
type ObjectInjectOptions = {
[key: string | symbol]:
| string
| symbol
| { from?: string | symbol; default?: any }
}
- 详细信息该
inject
选项应该是以下两种之一:
如果没有供给相匹配的属性、也没有提供默认值,那么注入的属性将为 undefined
。
请注意,注入绑定并非响应式的。这是有意为之的一个设计。如果要注入的值是一个响应式对象,那么这个对象上的属性将会保留响应性。请看配合响应性一节获取更多细节。
- 一个字符串数组
- 一个对象,其 key 名就是在当前组件中的本地绑定名称,而它的值应该是以下两种之一:
- 匹配可用注入的 key (string 或者 Symbol)
- 一个对象
- 它的
from
属性是一个 key (string 或者 Symbol),用于匹配可用的注入 - 它的
default
属性用作候补值。和 props 的默认值类似,如果它是一个对象,那么应该使用一个工厂函数来创建,以避免多个组件共享同一个对象。
- 它的
- 示例基本使用方式:
export default {
inject: ['foo'],
created() {
console.log(this.foo)
}
}
使用注入的值作为 props 的默认值:
const Child = {
inject: ['foo'],
props: {
bar: {
default() {
return this.foo
}
}
}
}
使用注入的值作为 data:
const Child = {
inject: ['foo'],
data() {
return {
bar: this.foo
}
}
}
注入项可以选择是否带有默认值:
const Child = {
inject: {
foo: { default: 'foo' }
}
}
如果需要从不同名字的属性中注入,请使用 from
指明来源属性。
const Child = {
inject: {
foo: {
from: 'bar',
default: 'foo'
}
}
}
和 props 默认值类似,对于非原始数据类型的值,你需要使用工厂函数:
const Child = {
inject: {
foo: {
from: 'bar',
default: () => [1, 2, 3]
}
}
}
mixins
一个包含组件选项对象的数组,这些选项都将被混入到当前组件的实例中。
- 类型
interface ComponentOptions {
mixins?: ComponentOptions[]
}
- 详细信息
mixins
选项接受一个 mixin 对象数组。这些 mixin 对象可以像普通的实例对象一样包含实例选项,它们将使用一定的选项合并逻辑与最终的选项进行合并。举例来说,如果你的 mixin 包含了一个created
钩子,而组件自身也有一个,那么这两个函数都会被调用。Mixin 钩子的调用顺序与提供它们的选项顺序相同,且会在组件自身的钩子前被调用。
不再推荐
在 Vue 2 中,mixins 是创建可重用组件逻辑的主要方式。尽管在 Vue 3 中保留了 mixins 支持,但对于组件间的逻辑复用,Composition API 是现在更推荐的方式。
- 示例:
const mixin = {
created() {
console.log(1)
}
}
createApp({
created() {
console.log(2)
},
mixins: [mixin]
})
// => 1
// => 2
extends
要继承的“基类”组件。
- 类型
interface ComponentOptions {
extends?: ComponentOptions
}
- 详细信息使一个组件可以继承另一个组件的组件选项。从实现角度来看,
extends
几乎和mixins
相同。通过extends
指定的组件将会当作第一个 mixin 来处理。然而,extends
和mixins
表达的是不同的目标。mixins
选项基本用于组合功能,而extends
则一般更关注继承关系。同mixins
一样,所有选项都将使用相关的策略进行合并。 - 示例:
const CompA = { ... }
const CompB = {
extends: CompA,
...
}
其他杂项选项
name
用于显式声明组件展示时的名称。
- 类型
interface ComponentOptions {
name?: string
}
- 详细信息组件的名字有以下用途:
当你在使用单文件组件时,组件已经会根据其文件名推导出其名称。举例来说,一个名为 MyComponent.vue
的文件会推导出显示名称为“MyComponent”。
另一种场景是当一个组件通过 app.component
被全局注册时,这个全局 ID 就自动被设为了其名称。
使用 name
选项使你可以覆盖推导出的名称,或是在没有推导出名字时显式提供一个。(例如没有使用构建工具时,或是一个内联的非 SFC 式的组件)
有一种场景下 name
必须是已显式声明的:即 `` 通过其 include / exclude
prop 来匹配其需要缓存的组件时。
TIP
在 3.2.34 或以上的版本中,使用<script setup>
的单文件组件会自动根据文件名生成对应的name
选项,即使是在配合<KeepAlive>
使用时也无需再手动声明。
- 在组件自己的模板中递归引用自己时
- 在 Vue 开发者工具中的组件树显示时
- 在组件抛出的警告追踪栈信息中显示时
inheritAttrs
用于控制是否启用默认的组件 attribute 透传行为。
- 类型
interface ComponentOptions {
inheritAttrs?: boolean // 默认值:true
}
- 详细信息默认情况下,父组件传递的,但没有被子组件解析为 props 的 attributes 绑定会被“透传”。这意味着当我们有一个单根节点的子组件时,这些绑定会被作为一个常规的 HTML attribute 应用在子组件的根节点元素上。当你编写的组件想要在一个目标元素或其他组件外面包一层时,可能并不期望这样的行为。我们可以通过设置
inheritAttrs
为false
来禁用这个默认行为。这些 attributes 可以通过$attrs
这个实例属性来访问,并且可以通过v-bind
来显式绑定在一个非根节点的元素上。 - 示例在一个组件的
<script setup>
中声明这个选项时,需要一个额外的<script>
块:
<script>
export default {
inheritAttrs: false
}
</script>
<script setup>
defineProps(['label', 'value'])
defineEmits(['input'])
</script>
<template>
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</label>
</template>
components
一个对象,用于注册对当前组件实例可用的组件。
- 类型
interface ComponentOptions {
components?: { [key: string]: Component }
}
- 示例
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: {
// 简写
Foo,
// 注册为一个不同的名称
RenamedBar: Bar
}
}
directives
一个对象,用于注册对当前组件实例可用的指令。
- 类型
interface ComponentOptions {
directives?: { [key: string]: Directive }
}
- 示例
export default {
directives: {
// 在模板中启用 v-focus
focus: {
mounted(el) {
el.focus()
}
}
}
}
<input v-focus>
这个列表中的指令都在当前组件实例中可用。
组件实例
$data
从
data
选项函数中返回的对象,会被组件赋为响应式。组件实例将会代理对其数据对象的属性访问。
- 类型
interface ComponentPublicInstance {
$data: object
}
$props
表示组件当前已解析的 props 对象。
- 类型
interface ComponentPublicInstance {
$props: object
}
- 详细信息这里只包含通过
props
选项声明的 props。组件实例将会代理对其 props 对象上属性的访问。
$el
该组件实例管理的 DOM 根节点。
- 类型
interface ComponentPublicInstance {
$el: Node | undefined
}
- 详细信息
$el
直到组件挂载完成 (mounted) 之前都会是undefined
。
TIP
为保持一致性,我们推荐使用模板引用来直接访问元素而不是依赖$el
。
- 对于单一根元素的组件,
$el
将会指向该根元素。 - 对于以文本节点为根的组件,
$el
将会指向该文本节点。 - 对于以多个元素为根的组件,
$el
将是一个仅作占位符的 DOM 节点,Vue 使用它来跟踪组件在 DOM 中的位置 (文本节点或 SSR 激活模式下的注释节点)。
$options
已解析的用于实例化当前组件的组件选项。
- 类型
interface ComponentPublicInstance {
$options: ComponentOptions
}
- 详细信息这个
$options
对象暴露了当前组件的已解析选项,并且会是以下几种可能来源的合并结果:
它通常用于支持自定义组件选项:
const app = createApp({
customOption: 'foo',
created() {
console.log(this.$options.customOption) // => 'foo'
}
})
- 全局 mixin
- 组件
extends
的基组件 - 组件级 mixin
$parent
当前组件可能存在的父组件实例,如果当前组件是顶层组件,则为
null
。
- 类型
interface ComponentPublicInstance {
$parent: ComponentPublicInstance | null
}
$root
当前组件树的根组件实例。如果当前实例没有父组件,那么这个值就是它自己。
- 类型
interface ComponentPublicInstance {
$root: ComponentPublicInstance
}
$slots
一个表示父组件所传入插槽的对象。
- 类型
interface ComponentPublicInstance {
$slots: { [name: string]: Slot }
}
type Slot = (...args: any[]) => VNode[]
- 详细信息通常用于手写渲染函数,但也可用于检测是否存在插槽。每一个插槽都在
this.$slots
上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为this.$slots.default
。如果插槽是一个作用域插槽,传递给该插槽函数的参数可以作为插槽的 prop 提供给插槽。 - 参考:渲染函数 - 渲染插槽
$refs
一个包含 DOM 元素和组件实例的对象,通过模板引用注册。
- 类型
interface ComponentPublicInstance {
$refs: { [name: string]: Element | ComponentPublicInstance | null }
}
$attrs
一个包含了组件所有透传 attributes 的对象。
- 类型
interface ComponentPublicInstance {
$attrs: object
}
- 详细信息透传 Attributes 是指由父组件传入,且没有被子组件声明为 props 或是组件自定义事件的 attributes 和事件处理函数。默认情况下,若是单一根节点组件,
$attrs
中的所有属性都是直接自动继承自组件的根元素。而多根节点组件则不会如此,同时你也可以通过配置inheritAttrs
选项来显式地关闭该行为。
$watch()
用于命令式地创建侦听器的 API。
- 类型
interface ComponentPublicInstance {
$watch(
source: string | (() => any),
callback: WatchCallback,
options?: WatchOptions
): StopHandle
}
type WatchCallback<T> = (
value: T,
oldValue: T,
onCleanup: (cleanupFn: () => void) => void
) => void
interface WatchOptions {
immediate?: boolean // default: false
deep?: boolean // default: false
flush?: 'pre' | 'post' | 'sync' // default: 'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
type StopHandle = () => void
- 详细信息第一个参数是侦听来源。可以是一个组件的属性名的字符串,一个简单的由点分隔的路径字符串,或是一个 getter 函数。第二个参数是回调函数。它接收的参数分别是侦听来源的新值、旧值。
- 示例侦听一个属性名:
this.$watch('a', (newVal, oldVal) => {})
侦听一个由 .
分隔的路径:
this.$watch('a.b', (newVal, oldVal) => {})
对更复杂表达式使用 getter 函数:
this.$watch(
// 每一次这个 `this.a + this.b` 表达式生成一个
// 不同的结果,处理函数都会被调用
// 这就好像我们在侦听一个计算属性
// 而不定义计算属性本身。
() => this.a + this.b,
(newVal, oldVal) => {}
)
停止该侦听器:
const unwatch = this.$watch('a', cb)
// 之后……
unwatch()
$emit()
在当前组件触发一个自定义事件。任何额外的参数都会传递给事件监听器的回调函数。
- 类型
interface ComponentPublicInstance {
$emit(event: string, ...args: any[]): void
}
- 示例
export default {
created() {
// 仅触发事件
this.$emit('foo')
// 带有额外的参数
this.$emit('bar', 1, 2, 3)
}
}
$forceUpdate()
强制该组件重新渲染。
- 类型
interface ComponentPublicInstance {
$forceUpdate(): void
}
- 详细信息鉴于 Vue 的全自动响应性系统,这个功能应该很少会被用到。唯一可能需要它的情况是,你使用高阶响应式 API 显式创建了一个非响应式的组件状态。
$nextTick()
绑定在实例上的
nextTick()
函数。
- 类型
interface ComponentPublicInstance {
$nextTick(callback?: (this: ComponentPublicInstance) => void): Promise<void>
}
- 详细信息和全局版本的
nextTick()
的唯一区别就是组件传递给this.$nextTick()
的回调函数会带上this
上下文,其绑定了当前组件实例。
Vue3 内置指令
v-text
更新元素的文本内容。
- 期望的绑定值类型:
string
- 详细信息
v-text
通过设置元素的 textContent 属性来工作,因此它将覆盖元素中所有现有的内容。如果你需要更新textContent
的部分,应该使用 mustache interpolations 代替。 - 示例
<span v-text="msg"></span>
<!-- 等同于 -->
<span>{{msg}}</span>
v-html
更新元素的 innerHTML。
- 期望的绑定值类型:
string
- 详细信息
v-html
的内容直接作为普通 HTML 插入—— Vue 模板语法是不会被解析的。如果你发现自己正打算用 v-html
来编写模板,不如重新想想怎么使用组件来代替。
安全说明
在你的站点上动态渲染任意的 HTML 是非常危险的,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要将用户提供的内容作为插值
在单文件组件,scoped
样式将不会作用于 v-html
里的内容,因为 HTML 内容不会被 Vue 的模板编译器解析。如果你想让 v-html
的内容也支持 scoped CSS,你可以使用 CSS modules 或使用一个额外的全局 <style>
元素,手动设置类似 BEM 的作用域策略。
- 示例:
<div v-html="html"></div>
v-show
基于表达式值的真假性,来改变元素的可见性。
- 期望的绑定值类型:
any
- 详细信息
v-show
通过设置内联样式的display
CSS 属性来工作,当元素可见时将使用初始display
值。当条件改变时,也会触发过渡效果。
v-if
基于表达式值的真假性,来条件性地渲染元素或者模板片段。
- 期望的绑定值类型:
any
- 详细信息当
v-if
元素被触发,元素及其所包含的指令/组件都会销毁和重构。如果初始条件是假,那么其内部的内容根本都不会被渲染。可用于<template>
表示仅包含文本或多个元素的条件块。当条件改变时会触发过渡效果。当同时使用时,v-if
比v-for
优先级更高。我们并不推荐在一元素上同时使用这两个指令 — 查看列表渲染指南详情。
v-else
表示
v-if
或v-if
/v-else-if
链式调用的“else 块”。
- 无需传入表达式
- 详细信息
- 限定:上一个兄弟元素必须有
v-if
或v-else-if
。 - 可用于
<template>
表示仅包含文本或多个元素的条件块。
- 限定:上一个兄弟元素必须有
- 示例
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
v-else-if
表示
v-if
的“else if 块”。可以进行链式调用。
- 期望的绑定值类型:
any
- 详细信息
- 限定:上一个兄弟元素必须有
v-if
或v-else-if
。 - 可用于
<template>
表示仅包含文本或多个元素的条件块。
- 限定:上一个兄弟元素必须有
- 示例
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
v-for
基于原始数据多次渲染元素或模板块。
- 期望的绑定值类型:
Array | Object | number | string | Iterable
- 详细信息指令值必须使用特殊语法
alias in expression
为正在迭代的元素提供一个别名:
<div v-for="item in items">
{{ item.text }}
</div>
或者,你也可以为索引指定别名 (如果用在对象,则是键值):
<div v-for="(item, index) in items"></div>
<div v-for="(value, key) in object"></div>
<div v-for="(value, name, index) in object"></div>
v-for
的默认方式是尝试就地更新元素而不移动它们。要强制其重新排序元素,你需要用特殊 attribute key
来提供一个排序提示:
<div v-for="item in items" :key="item.id">
{{ item.text }}
</div>
v-for
也可以用于 Iterable Protocol 的实现,包括原生 Map
和 Set
。
v-on
给元素绑定事件监听器。
- 缩写:
@
- 期望的绑定值类型:
Function | Inline Statement | Object (不带参数)
- 参数:
event
(使用对象语法则为可选项) - 修饰符:
.stop
——调用event.stopPropagation()
。.prevent
——调用event.preventDefault()
。.capture
——在捕获模式添加事件监听器。.self
——只有事件从元素本身发出才触发处理函数。.{keyAlias}
——只在某些按键下触发处理函数。.once
——最多触发一次处理函数。.left
——只在鼠标左键事件触发处理函数。.right
——只在鼠标右键事件触发处理函数。.middle
——只在鼠标中键事件触发处理函数。.passive
——通过{ passive: true }
附加一个 DOM 事件。
- 详细信息事件类型由参数来指定。表达式可以是一个方法名,一个内联声明,如果有修饰符则可省略。当用于普通元素,只监听原生 DOM 事件。当用于自定义元素组件,则监听子组件触发的自定义事件。当监听原生 DOM 事件时,方法接收原生事件作为唯一参数。如果使用内联声明,声明可以访问一个特殊的
$event
变量:v-on:click="handle('ok', $event)"
。v-on
还支持绑定不带参数的事件/监听器对的对象。请注意,当使用对象语法时,不支持任何修饰符。 - 示例:
<!-- 方法处理函数 -->
<button v-on:click="doThis"></button>
<!-- 动态事件 -->
<button v-on:[event]="doThis"></button>
<!-- 内联声明 -->
<button v-on:click="doThat('hello', $event)"></button>
<!-- 缩写 -->
<button @click="doThis"></button>
<!-- 使用缩写的动态事件 -->
<button @[event]="doThis"></button>
<!-- 停止传播 -->
<button @click.stop="doThis"></button>
<!-- 阻止默认事件 -->
<button @click.prevent="doThis"></button>
<!-- 不带表达式地阻止默认事件 -->
<form @submit.prevent></form>
<!-- 链式调用修饰符 -->
<button @click.stop.prevent="doThis"></button>
<!-- 按键用于 keyAlias 修饰符-->
<input @keyup.enter="onEnter" />
<!-- 点击事件将最多触发一次 -->
<button v-on:click.once="doThis"></button>
<!-- 对象语法 -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
监听子组件的自定义事件 (当子组件的“my-event”事件被触发,处理函数将被调用):
<MyComponent @my-event="handleThis" />
<!-- 内联声明 -->
<MyComponent @my-event="handleThis(123, $event)" />
v-bind
动态的绑定一个或多个 attribute,也可以是组件的 prop。
- 缩写:
:
或者.
(当使用.prop
修饰符) - 期望:
any (带参数) | Object (不带参数)
- 参数:
attrOrProp (可选的)
- 修饰符:
.camel
——将短横线命名的 attribute 转变为驼峰式命名。.prop
——强制绑定为 DOM property。3.2+.attr
——强制绑定为 DOM attribute。3.2+
- **用途:**当用于绑定
class
或style
attribute,v-bind
支持额外的值类型如数组或对象。详见下方的指南链接。在处理绑定时,Vue 默认会利用in
操作符来检查该元素上是否定义了和绑定的 key 同名的 DOM property。如果存在同名的 property,则 Vue 会把作为 DOM property 赋值,而不是作为 attribute 设置。这个行为在大多数情况都符合期望的绑定值类型,但是你也可以显式用.prop
和.attr
修饰符来强制绑定方式。有时这是必要的,特别是在和自定义元素打交道时。当用于组件 props 绑定时,所绑定的 props 必须在子组件中已被正确声明。当不带参数使用时,可以用于绑定一个包含了多个 attribute 名称-绑定值对的对象。 - 示例:
<!-- 绑定 attribute -->
<img v-bind:src="imageSrc" />
<!-- 动态 attribute 名 -->
<button v-bind:[key]="value"></button>
<!-- 缩写 -->
<img :src="imageSrc" />
<!-- 缩写形式的动态 attribute 名 -->
<button :[key]="value"></button>
<!-- 内联字符串拼接 -->
<img :src="'/path/to/images/' + fileName" />
<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]"></div>
<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>
<!-- 绑定对象形式的 attribute -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>
<!-- prop 绑定。“prop” 必须在子组件中已声明。 -->
<MyComponent :prop="someThing" />
<!-- 传递子父组件共有的 prop -->
<MyComponent v-bind="$props" />
<!-- XLink -->
<svg><a :xlink:special="foo"></a></svg>
.prop
修饰符也有专门的缩写,.
:
<div :someProperty.prop="someObject"></div>
<!-- 等同于 -->
<div .someProperty="someObject"></div>
当在 DOM 内模板使用 .camel
修饰符,可以驼峰化 v-bind
attribute 的名称,例如 SVG viewBox
attribute:
<svg :view-box.camel="viewBox"></svg>
如果使用字符串模板或使用构建步骤预编译模板,则不需要 .camel
。
v-model
在表单输入元素或组件上创建双向绑定。
- 期望的绑定值类型:根据表单输入元素或组件输出的值而变化
- 仅限:
<input>
<select>
<textarea>
- components
- 修饰符:
.lazy
——监听change
事件而不是input
.number
——将输入的合法符串转为数字.trim
——移除输入内容两端空格
v-slot
用于声明具名插槽或是期望接收 props 的作用域插槽。
- 缩写:
#
- 期望的绑定值类型:能够合法在函数参数位置使用的 JavaScript 表达式。支持解构语法。绑定值是可选的——只有在给作用域插槽传递 props 才需要。
- 参数:插槽名 (可选,默认是
default
) - 仅限:
<template>
- components (用于带有 prop 的单个默认插槽)
- 示例:
<!-- 具名插槽 -->
<BaseLayout>
<template v-slot:header>
Header content
</template>
<template v-slot:default>
Default slot content
</template>
<template v-slot:footer>
Footer content
</template>
</BaseLayout>
<!-- 接收 prop 的具名插槽 -->
<InfiniteScroll>
<template v-slot:item="slotProps">
<div class="item">
{{ slotProps.item.text }}
</div>
</template>
</InfiniteScroll>
<!-- 接收 prop 的默认插槽,并解构 -->
<Mouse v-slot="{ x, y }">
Mouse position: {{ x }}, {{ y }}
</Mouse>
v-pre
跳过该元素及其所有子元素的编译。
- 无需传入
- 详细信息元素内具有
v-pre
,所有 Vue 模板语法都会被保留并按原样渲染。最常见的用例就是显示原始双大括号标签及内容。 - 示例:
<span v-pre>{{ this will not be compiled }}</span>
v-once
仅渲染元素和组件一次,并跳过之后的更新。
- 无需传入
- 详细信息在随后的重新渲染,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。template
<!-- 单个元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 带有子元素的元素 -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- 组件 -->
<MyComponent v-once :comment="msg" />
<!-- `v-for` 指令 -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
从 3.2 起,你也可以搭配 v-memo
的无效条件来缓存部分模板。
v-memo 3.2+
- 期望的绑定值类型:
any[]
- 详细信息缓存一个模板的子树。在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过。举例来说:
<div v-memo="[valueA, valueB]">
...
</div>
当组件重新渲染,如果 valueA
和 valueB
都保持不变,这个 <div>
及其子项的所有更新都将被跳过。实际上,甚至虚拟 DOM 的 vnode 创建也将被跳过,因为缓存的子树副本可以被重新使用。正确指定缓存数组很重要,否则应该生效的更新可能被跳过。v-memo
传入空依赖数组 (v-memo="[]"
) 将与 v-once
效果相同。与 v-for
一起使用v-memo
仅用于性能至上场景中的微小优化,应该很少需要。最常见的情况可能是有助于渲染海量 v-for
列表 (长度超过 1000 的情况):
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
<p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
<p>...more child nodes</p>
</div>
当组件的 selected
状态改变,默认会重新创建大量的 vnode,尽管绝大部分都跟之前是一模一样的。v-memo
用在这里本质上是在说“只有当该项的被选中状态改变时才需要更新”。这使得每个选中状态没有变的项能完全重用之前的 vnode 并跳过差异比较。注意这里 memo 依赖数组中并不需要包含 item.id
,因为 Vue 也会根据 item 的 :key
进行判断。
警告
当搭配v-for
使用v-memo
,确保两者都绑定在同一个元素上。v-memo
** 不能用在v-for
内部。**
v-memo
也能被用于在一些默认优化失败的边际情况下,手动避免子组件出现不需要的更新。但是一样的,开发者需要负责指定正确的依赖数组以免跳过必要的更新。
v-cloak
用于隐藏尚未完成编译的 DOM 模板。
- 无需传入
- **详细信息该指令只在没有构建步骤的环境下需要使用。**当使用直接在 DOM 中书写的模板时,可能会出现一种叫做“未编译模板闪现”的情况:用户可能先看到的是还没编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。
v-cloak
会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。配合像[v-cloak] { display: none }
这样的 CSS 规则,它可以在组件编译完毕前隐藏原始模板。 - 示例:
[v-cloak] {
display: none;
}
<div v-cloak>
{{ message }}
</div>
直到编译完成前,<div>
将不可见。
Vue3 内置组件
<Transition>
为单个元素或组件提供动画过渡效果。
- Props
interface TransitionProps {
/**
* 用于自动生成过渡 CSS class 名。
* 例如 `name: 'fade'` 将自动扩展为 `.fade-enter`、
* `.fade-enter-active` 等。
*/
name?: string
/**
* 是否应用 CSS 过渡 class。
* 默认:true
*/
css?: boolean
/**
* 指定要等待的过渡事件类型
* 来确定过渡结束的时间。
* 默认情况下会自动检测
* 持续时间较长的类型。
*/
type?: 'transition' | 'animation'
/**
* 显式指定过渡的持续时间。
* 默认情况下是等待过渡效果的根元素的第一个 `transitionend`
* 或`animationend`事件。
*/
duration?: number | { enter: number; leave: number }
/**
* 控制离开/进入过渡的时序。
* 默认情况下是同时的。
*/
mode?: 'in-out' | 'out-in' | 'default'
/**
* 是否对初始渲染使用过渡。
* 默认:false
*/
appear?: boolean
/**
* 用于自定义过渡 class 的 prop。
* 在模板中使用短横线命名,例如:enter-from-class="xxx"
*/
enterFromClass?: string
enterActiveClass?: string
enterToClass?: string
appearFromClass?: string
appearActiveClass?: string
appearToClass?: string
leaveFromClass?: string
leaveActiveClass?: string
leaveToClass?: string
}
- 事件
@before-enter
@before-leave
@enter
@leave
@appear
@after-enter
@after-leave
@after-appear
@enter-cancelled
@leave-cancelled
(v-show
only)@appear-cancelled
- 示例简单元素:
<Transition>
<div v-if="ok">toggled content</div>
</Transition>
动态组件,初始渲染时带有过渡模式 + 动画出现:
<Transition name="fade" mode="out-in" appear>
<component :is="view"></component>
</Transition>
监听过渡事件:
<Transition @after-enter="onTransitionComplete">
<div v-show="ok">toggled content</div>
</Transition>
<TransitionGroup>
为列表中的多个元素或组件提供过渡效果。
- Props
<TransitionGroup>
拥有与<Transition>
除了mode
以外所有的 props,并增加了两个额外的 props:
interface TransitionGroupProps extends Omit<TransitionProps, 'mode'> {
/**
* 如果未定义,则渲染为片段 (fragment)。
*/
tag?: string
/**
* 用于自定义过渡期间被应用的 CSS class。
* 在模板中使用 kebab-case,例如 move-class="xxx"
*/
moveClass?: string
}
- 事件
<TransitionGroup>
抛出与<Transition>
相同的事件。 - 详细信息默认情况下,
<TransitionGroup>
不会渲染一个容器 DOM 元素,但是可以通过tag
prop 启用。注意,每个<transition-group>
的子节点必须有独立的 key,动画才能正常工作。<TransitionGroup>
支持通过 CSS transform 控制移动效果。当一个子节点在屏幕上的位置在更新之后发生变化时,它会被添加一个使其位移的 CSS class (基于name
attribute 推导,或使用move-class
prop 显式配置)。如果使其位移的 class 被添加时 CSS 的transform
属性是“可过渡的”,那么该元素会基于 FLIP 技巧平滑地到达动画终点。 - 示例
<TransitionGroup tag="ul" name="slide">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
<KeepAlive>
缓存包裹在其中的动态切换组件。
- Props
interface KeepAliveProps {
/**
* 如果指定,则只有与 `include` 名称
* 匹配的组件才会被缓存。
*/
include?: MatchPattern
/**
* 任何名称与 `exclude`
* 匹配的组件都不会被缓存。
*/
exclude?: MatchPattern
/**
* 最多可以缓存多少组件实例。
*/
max?: number | string
}
type MatchPattern = string | RegExp | (string | RegExp)[]
- 详细信息
<KeepAlive>
包裹动态组件时,会缓存不活跃的组件实例,而不是销毁它们。任何时候都只能有一个活跃组件实例作为<KeepAlive>
的直接子节点。当一个组件在<KeepAlive>
中被切换时,它的activated
和deactivated
生命周期钩子将被调用,用来替代mounted
和unmounted
。这适用于<KeepAlive>
的直接子节点及其所有子孙节点。 - 示例基本用法:
<KeepAlive>
<component :is="view"></component>
</KeepAlive>
与 v-if
/ v-else
分支一起使用时,同一时间只能有一个组件被渲染:
<KeepAlive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</KeepAlive>
与 <Transition>
一起使用:
<Transition>
<KeepAlive>
<component :is="view"></component>
</KeepAlive>
</Transition>
使用 include
/ exclude
:
<!-- 用逗号分隔的字符串 -->
<KeepAlive include="a,b">
<component :is="view"></component>
</KeepAlive>
<!-- 正则表达式 (使用 `v-bind`) -->
<KeepAlive :include="/a|b/">
<component :is="view"></component>
</KeepAlive>
<!-- 数组 (使用 `v-bind`) -->
<KeepAlive :include="['a', 'b']">
<component :is="view"></component>
</KeepAlive>
使用 max
:
<KeepAlive :max="10">
<component :is="view"></component>
</KeepAlive>
<Teleport>
将其插槽内容渲染到 DOM 中的另一个位置。
- Props
interface TeleportProps {
/**
* 必填项。指定目标容器。
* 可以是选择器或实际元素。
*/
to: string | HTMLElement
/**
* 当值为 `true` 时,内容将保留在其原始位置
* 而不是移动到目标容器中。
* 可以动态更改。
*/
disabled?: boolean
}
- 示例指定目标容器:
<teleport to="#some-id" />
<teleport to=".some-class" />
<teleport to="[data-teleport]" />
有条件地禁用:
<teleport to="#popup" :disabled="displayVideoInline">
<video src="./my-movie.mp4">
</teleport>
<Suspense>
Experimental
用于协调对组件树中嵌套的异步依赖的处理。
- Props
interface SuspenseProps {
timeout?: string | number
}
- 事件
@resolve
@pending
@fallback
- 详细信息
<Suspense>
接受两个插槽:#default
和#fallback
。它将在内存中渲染默认插槽的同时展示后备插槽内容。如果在渲染时遇到异步依赖项 (异步组件和具有async setup()
的组件),它将等到所有异步依赖项解析完成时再显示默认插槽。
Vue3 内置特殊元素
<component>
一个用于渲染动态组件或元素的“元组件”。
- Props
interface DynamicComponentProps {
is: string | Component
}
- 详细信息要渲染的实际组件由
is
prop 决定。- 当
is
是字符串,它既可以是 HTML 标签名也可以是组件的注册名。 - 或者,
is
也可以直接绑定到组件的定义。
- 当
- 示例按注册名渲染组件 (选项式 API):
<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: { Foo, Bar },
data() {
return {
view: 'Foo'
}
}
}
</script>
<template>
<component :is="view" />
</template>
按定义渲染组件 (<script setup>
组合式 API):
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Math.random() > 0.5 ? Foo : Bar" />
</template>
渲染 HTML 元素:
<component :is="href ? 'a' : 'span'"></component>
内置组件都可以传递给 is
,但是如果想通过名称传递则必须先对其进行注册。举例来说:
<script>
import { Transition, TransitionGroup } from 'vue'
export default {
components: {
Transition,
TransitionGroup
}
}
</script>
<template>
<component :is="isGroup ? 'TransitionGroup' : 'Transition'">
...
</component>
</template>
如果将组件本身传递给 is
而不是其名称,则不需要注册,例如在 <script setup>
中。如果在 <component>
标签上使用 v-model
,模板编译器会将其扩展为 modelValue
prop 和 update:modelValue
事件监听器,就像对任何其他组件一样。但是,这与原生 HTML 元素不兼容,例如 <input>
或 <select>
。因此,在动态创建的原生元素上使用 v-model
将不起作用:
<script setup>
import { ref } from 'vue'
const tag = ref('input')
const username = ref('')
</script>
<template>
<!-- 由于 'input' 是原生 HTML 元素,因此这个 v-model 不起作用 -->
<component :is="tag" v-model="username" />
</template>
在实践中,这种极端情况并不常见,因为原生表单字段通常包裹在实际应用的组件中。如果确实需要直接使用原生元素,那么你可以手动将 v-model
拆分为 attribute 和事件。
<slot>
表示模板中的插槽内容出口。
- Props
interface SlotProps {
/**
* 任何传递给 <slot> 的 prop 都可以作为作用域插槽
* 的参数传递
*/
[key: string]: any
/**
* 保留,用于指定插槽名。
*/
name?: string
}
- 详细信息
<slot>
元素可以使用name
attribute 来指定插槽名。当没有指定name
时,将会渲染默认插槽。传递给插槽元素的附加 attributes 将作为插槽 props,传递给父级中定义的作用域插槽。元素本身将被其所匹配的插槽内容替换。Vue 模板里的<slot>
元素会被编译到 JavaScript,因此不要与原生<slot>
元素进行混淆。
Vue3 内置的特殊 Attributes
key
key
这个特殊的 attribute 主要作为 Vue 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。
- 预期:
number | string | symbol
- 详细信息在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,并尽可能地就地更新/复用相同类型的元素。如果传了 key,则将根据 key 的变化顺序来重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。同一个父元素下的子元素必须具有唯一的 key。重复的 key 将会导致渲染异常。最常见的用例是与
v-for
结合:
<ul>
<li v-for="item in items" :key="item.id">...</li>
</ul>
也可以用于强制替换一个元素/组件而不是复用它。当你想这么做时它可能会很有用:
举例来说:
<transition>
<span :key="text">{{ text }}</span>
</transition>
当 text
变化时,<span>
总是会被替换而不是更新,因此 transition 将会被触发。
- 在适当的时候触发组件的生命周期钩子
- 触发过渡
ref
用于注册模板引用。
- 预期:
string | Function
- 详细信息
ref
用于注册元素或子组件的引用。使用选项式 API,引用将被注册在组件的this.$refs
对象里:
<!-- 存储为 this.$refs.p -->
<p ref="p">hello</p>
使用组合式 API,引用将存储在与名字匹配的 ref 里:
<script setup>
import { ref } from 'vue'
const p = ref()
</script>
<template>
<p ref="p">hello</p>
</template>
如果用于普通 DOM 元素,引用将是元素本身;如果用于子组件,引用将是子组件的实例。或者 ref
可以接收一个函数值,用于对存储引用位置的完全控制:
<ChildComponent :ref="(el) => child = el" />
关于 ref 注册时机的重要说明:因为 ref 本身是作为渲染函数的结果来创建的,必须等待组件挂载后才能对它进行访问。this.$refs
也是非响应式的,因此你不应该尝试在模板中使用它来进行数据绑定。
is
用于绑定动态组件。
- 预期:
string | Component
- 用于原生元素
3.1+
当is
attribute 用于原生 HTML 元素时,它将被当作 Customized built-in element,其为原生 web 平台的特性。但是,在这种用例中,你可能需要 Vue 用其组件来替换原生元素,如 DOM 模板解析注意事项所述。你可以在is
attribute 的值中加上vue:
前缀,这样 Vue 就会把该元素渲染为 Vue 组件:
<table>
<tr is="vue:my-row-component"></tr>
</table>
Vue3 组合式函数
什么是“组合式函数”?
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。
相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。
鼠标跟踪器示例
如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
下面是它在组件中使用的方式:
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
如你所见,核心逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式 API。现在,useMouse()
的功能可以在任何组件中轻易复用了。
更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。
举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:
// event.js
import { onMounted, onUnmounted } from 'vue'
export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback))
}
有了它,之前的 useMouse()
组合式函数可以被简化为:
// mouse.js
import { ref } from 'vue'
import { useEventListener } from './event'
export function useMouse() {
const x = ref(0)
const y = ref(0)
useEventListener(window, 'mousemove', (event) => {
x.value = event.pageX
y.value = event.pageY
})
return { x, y }
}
TIP
每一个调用useMouse()
的组件实例会创建其独有的x
、y
状态拷贝,因此他们不会互相影响。如果你想要在组件之间共享状态,请阅读状态管理这一章。
异步状态示例
useMouse()
组合式函数没有接收任何参数,因此让我们再来看一个需要接收一个参数的组合式函数示例。在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。
<script setup>
import { ref } from 'vue'
const data = ref(null)
const error = ref(null)
fetch('...')
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
</script>
<template>
<div v-if="error">Oops! Error encountered: {{ error.message }}</div>
<div v-else-if="data">
Data loaded:
<pre>{{ data }}</pre>
</div>
<div v-else>Loading...</div>
</template>
如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:
// fetch.js
import { ref } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
fetch(url)
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
return { data, error }
}
现在我们在组件里只需要:
<script setup>
import { useFetch } from './fetch.js'
const { data, error } = useFetch('...')
</script>
useFetch()
接收一个静态的 URL 字符串作为输入,所以它只执行一次请求,然后就完成了。但如果我们想让它在每次 URL 变化时都重新请求呢?那我们可以让它同时允许接收 ref 作为参数:
// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
function doFetch() {
// 在请求之前重设状态...
data.value = null
error.value = null
// unref() 解包可能为 ref 的值
fetch(unref(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
if (isRef(url)) {
// 若输入的 URL 是一个 ref,那么启动一个响应式的请求
watchEffect(doFetch)
} else {
// 否则只请求一次
// 避免监听器的额外开销
doFetch()
}
return { data, error }
}
这个版本的 useFetch()
现在同时可以接收静态的 URL 字符串和 URL 字符串的 ref。当通过 isRef()
检测到 URL 是一个动态 ref 时,它会使用 watchEffect()
启动一个响应式的 effect。该 effect 会立刻执行一次,并在此过程中将 URL 的 ref 作为依赖进行跟踪。当 URL 的 ref 发生改变时,数据就会被重置,并重新请求。
这里是一个升级版的 useFetch()
,出于演示目的,我们人为地设置了延迟和随机报错。
约定和最佳实践
1. 命名
组合式函数约定用驼峰命名法命名,并以“use”作为开头。
2. 输入参数
尽管其响应性不依赖 ref,组合式函数仍可接收 ref 参数。如果编写的组合式函数会被其他开发者使用,你最好在处理输入参数时兼容 ref 而不只是原始的值。unref()
工具函数会对此非常有帮助:
import { unref } from 'vue'
function useFeature(maybeRef) {
// 若 maybeRef 确实是一个 ref,它的 .value 会被返回
// 否则,maybeRef 会被原样返回
const value = unref(maybeRef)
}
如果你的组合式函数在接收 ref 为参数时会产生响应式 effect,请确保使用 watch()
显式地监听此 ref,或者在 watchEffect()
中调用 unref()
来进行正确的追踪。
3. 返回值
你可能已经注意到了,我们一直在组合式函数中使用 ref()
而不是 reactive()
。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
// x 和 y 是两个 ref
const { x, y } = useMouse()
从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive()
包装一次,这样其中的 ref 会被自动解包,例如:
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
4. 副作用
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
- 如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:
onMounted()
。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。 - 确保在
onUnmounted()
时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在onUnmounted()
中被移除 (就像我们在useMouse()
示例中看到的一样)。当然也可以像之前的useEventListener()
示例那样,使用一个组合式函数来自动帮你做这些事。
5. 使用限制
组合式函数在
<script setup>
或setup()
钩子中,应始终被同步地调用。在某些场景下,你也可以在像onMounted()
这样的生命周期钩子中使用他们。
这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:
- 将生命周期钩子注册到该组件实例上
- 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
TIP
通过抽取组合式函数改善代码结构
抽取组合式函数不仅是为了复用,也是为了代码组织。随着组件复杂度的增高,你可能会最终发现组件多得难以查询和理解。组合式 API 会给予你足够的灵活性,让你可以基于逻辑问题将组件代码拆分成更小的函数:
<script setup>
import { useFeatureA } from './featureA.js'
import { useFeatureB } from './featureB.js'
import { useFeatureC } from './featureC.js'
const { foo, bar } = useFeatureA()
const { baz } = useFeatureB(foo)
const { qux } = useFeatureC(baz)
</script>
在某种程度上,你可以将这些提取出的组合式函数看作是可以相互通信的组件范围内的服务。
在选项式 API 中使用组合式函数
如果你正在使用选项式 API,组合式函数必须在
setup()
中调用。且其返回的绑定必须在setup()
中返回,以便暴露给this
及其模板:
import { useMouse } from './mouse.js'
import { useFetch } from './fetch.js'
export default {
setup() {
const { x, y } = useMouse()
const { data, error } = useFetch('...')
return { x, y, data, error }
},
mounted() {
// setup() 暴露的属性可以在通过 `this` 访问到
console.log(this.x)
}
// ...其他选项
}
与其他模式的比较
1. 和 Mixin 的对比
Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:
- 不清晰的数据来源:当使用了多个 mixin 时,实例上的数据属性来自哪个 mixin 变得不清晰,这使追溯实现和理解组件行为变得困难。这也是我们推荐在组合式函数中使用 ref + 解构模式的理由:让属性的来源在消费组件时一目了然。
- 命名空间冲突:多个来自不同作者的 mixin 可能会注册相同的属性名,造成命名冲突。若使用组合式函数,你可以通过在解构变量时对变量进行重命名来避免相同的键名。
- 隐式的跨 mixin 交流:多个 mixin 需要依赖共享的属性名来进行相互作用,这使得它们隐性地耦合在一起。而一个组合式函数的返回值可以作为另一个组合式函数的参数被传入,像普通函数那样。
基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。
2. 和无渲染组件的对比
在组件插槽一章中,我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。
组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。
我们推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。
3. 和 React Hooks 的对比
如果你有 React 的开发经验,你可能注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部分灵感正来自于 React hooks,Vue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性系统,这和 React hooks 的执行模型有本质上的不同。这一话题在组合式 API 的常见问题中有更细致的讨论。
Vue3 单文件组件
SFC 语法定义
总览
一个 Vue 单文件组件 (SFC),通常使用 *.vue
作为文件扩展名,它是一种使用了类似 HTML 语法的自定义文件格式,用于定义 Vue 组件。一个 Vue 单文件组件在语法上是兼容 HTML 的。
每一个 *.vue
文件都由三种顶层语言块构成:<template>
、<script>
和 <style>
,以及一些其他的自定义块:
<template>
<div class="example">{{ msg }}</div>
</template>
<script>
export default {
data() {
return {
msg: 'Hello world!'
}
}
}
</script>
<style>
.example {
color: red;
}
</style>
<custom1>
This could be e.g. documentation for the component.
</custom1>
相应语言块
<template>
- 每个
*.vue
文件最多可以包含一个顶层<template>
块。 - 语块包裹的内容将会被提取、传递给
@vue/compiler-dom
,预编译为 JavaScript 渲染函数,并附在导出的组件上作为其render
选项。
<script>
- 每个
*.vue
文件最多可以包含一个<script>
块。(使用 `` 的情况除外) - 这个脚本代码块将作为 ES 模块执行。
- 默认导出应该是 Vue 的组件选项对象,可以是一个对象字面量或是 defineComponent 函数的返回值。
<script setup>
- 每个
*.vue
文件最多可以包含一个<script setup>
。(不包括一般的<script>
) - 这个脚本块将被预处理为组件的
setup()
函数,这意味着它将为每一个组件实例都执行。<script setup>
中的顶层绑定都将自动暴露给模板。要了解更多细节,请看 `` 的专门文档。
<style>
- 每个
*.vue
文件可以包含多个<style>
标签。 - 一个
<style>
标签可以使用scoped
或module
attribute (查看 SFC 样式功能了解更多细节) 来帮助封装当前组件的样式。使用了不同封装模式的多个<style>
标签可以被混合入同一个组件。
自定义块
在一个 *.vue
文件中可以为任何项目特定需求使用额外的自定义块。举例来说,一个用作写文档的 <docs>
块。这里是一些自定义块的真实用例:
自定义块的处理需要依赖工具链。如果你想要在构建中集成你的自定义语块,请参见相关工具链指南获取更多细节。
自动名称推导
SFC 在以下场景中会根据文件名自动推导其组件名:
- 开发警告信息中需要格式化组件名时;
- DevTools 中观察组件时;
- 递归组件自引用时。例如一个名为
FooBar.vue
的组件可以在模板中通过<FooBar/>
引用自己。(同名情况下) 这比明确注册/导入的组件优先级低。
预处理器
代码块可以使用 lang
这个 attribute 来声明预处理器语言,最常见的用例就是在 <script>
中使用 TypeScript:
<script lang="ts">
// use TypeScript
</script>
lang
在任意块上都能使用,比如我们可以在 <style>
标签中使用 SASS 或是 <template>
中使用 Pug:
<template lang="pug">
p {{ msg }}
</template>
<style lang="scss">
$primary-color: #333;
body {
color: $primary-color;
}
</style>
注意对不同预处理器的集成会根据你所使用的工具链而有所不同,具体细节请查看相应的工具链文档来确认:
Src 导入
如果你更喜欢将 *.vue
组件分散到多个文件中,可以为一个语块使用 src
这个 attribute 来导入一个外部文件:
<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>
请注意 src
导入和 JS 模块导入遵循相同的路径解析规则,这意味着:
- 相对路径需要以
./
开头 - 你也可以从 npm 依赖中导入资源
<!-- 从所安装的 "todomvc-app-css" npm 包中导入一个文件 -->
<style src="todomvc-app-css/index.css" />
src
导入对自定义语块也同样适用:
<unit-test src="./unit-test.js">
</unit-test>
注释
在每一个语块中你都可以按照相应语言 (HTML、CSS、JavaScript 和 Pug 等等) 的语法书写注释。对于顶层注释,请使用 HTML 的注释语法 <!-- comment contents here -->
单文件组件 <script setup>
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的
基本语法
要启用该语法,需要在 <script>
代码块上添加 setup
attribute:
<script setup>
console.log('hello script setup')
</script>
里面的代码会被编译成组件 setup()
函数的内容。这意味着与普通的 <script>
只在组件被首次引入的时候执行一次不同,<script setup>
中的代码会在每次组件实例被创建的时候执行。
顶层的绑定会被暴露给模板
当使用 <script setup>
的时候,任何在 <script setup>
声明的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用:
<script setup>
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>
<template>
<button @click="log">{{ msg }}</button>
</template>
import 导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的 helper 函数,而不需要通过 methods
选项来暴露它:
<script setup>
import { capitalize } from './helpers'
</script>
<template>
<div>{{ capitalize('hello') }}</div>
</template>
响应式
响应式状态需要明确使用响应式 API 来创建。和 setup()
函数的返回值一样,ref 在模板中使用的时候会自动解包:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
使用组件
<script setup>
范围里的值也能被直接作为自定义组件的标签名使用:
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>
这里 MyComponent
应当被理解为像是在引用一个变量。如果你使用过 JSX,此处的心智模型是类似的。其 kebab-case 格式的 <my-component>
同样能在模板中使用——不过,我们强烈建议使用 PascalCase 格式以保持一致性。同时这也有助于区分原生的自定义元素。
动态组件
由于组件是通过变量引用而不是基于字符串组件名注册的,在 <script setup>
中要使用动态组件的时候,应该使用动态的 :is
来绑定:
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>
请注意组件是如何在三元表达式中被当做变量使用的。
递归组件
一个单文件组件可以通过它的文件名被其自己所引用。例如:名为 FooBar.vue
的组件可以在其模板中用 <FooBar/>
引用它自己。
请注意这种方式相比于导入的组件优先级更低。如果有具名的导入和组件自身推导的名字冲突了,可以为导入的组件添加别名:
import { FooBar as FooBarChild } from './components'
命名空间组件
可以使用带 .
的组件标签,例如 <Foo.Bar>
来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>
使用自定义指令
全局注册的自定义指令将正常工作。本地的自定义指令在 <script setup>
中不需要显式注册,但他们必须遵循 vNameOfDirective
这样的命名规范:
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// 在元素上做些操作
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>
如果指令是从别处导入的,可以通过重命名来使其符合命名规范:
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
defineProps()
和 defineEmits()
为了在声明 props
和 emits
选项时获得完整的类型推导支持,我们可以使用 defineProps
和 defineEmits
API,它们将自动地在 <script setup>
中可用:
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>
defineProps
和defineEmits
都是只能在<script setup>
中使用的编译器宏。他们不需要导入,且会随着<script setup>
的处理过程一同被编译掉。defineProps
接收与props
选项相同的值,defineEmits
接收与emits
选项相同的值。defineProps
和defineEmits
在选项传入后,会提供恰当的类型推导。- 传入到
defineProps
和defineEmits
的选项会从 setup 中提升到模块的作用域。因此,传入的选项不能引用在 setup 作用域中声明的局部变量。这样做会引起编译错误。但是,它_可以_引用导入的绑定,因为它们也在模块作用域内。
如果使用了 TypeScript,使用纯类型声明来声明 prop 和 emit 也是可以的。
defineExpose
使用 <script setup>
的组件是默认关闭的——即通过模板引用或者 $parent
链获取到的组件的公开实例,不会暴露任何在 <script setup>
中声明的绑定。
可以通过 defineExpose
编译器宏来显式指定在 <script setup>
组件中要暴露出去的属性:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板引用的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number }
(ref 会和在普通实例中一样被自动解包)
useSlots()
和 useAttrs()
在 <script setup>
使用 slots
和 attrs
的情况应该是相对来说较为罕见的,因为可以在模板中直接通过 $slots
和 $attrs
来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlots
和 useAttrs
两个辅助函数:
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
useSlots
和 useAttrs
是真实的运行时函数,它的返回与 setupContext.slots
和 setupContext.attrs
等价。它们同样也能在普通的组合式 API 中使用。
与普通的 <script>
一起使用
<script setup>
可以和普通的
- 声明无法在
<script setup>
中声明的选项,例如inheritAttrs
或插件的自定义选项。 - 声明模块的具名导出 (named exports)。
- 运行只需要在模块作用域执行一次的副作用,或是创建单例对象。
<script>
// 普通 <script>, 在模块作用域下执行 (仅一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
顶层 await
<script setup>
中可以使用顶层 await。结果代码会被编译成 async setup():
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>
另外,await 的表达式会自动编译成在 await
之后保留当前组件实例上下文的格式。
注意
async setup()
必须与Suspense
内置组件组合使用,Suspense
目前还是处于实验阶段的特性,会在将来的版本中稳定。
针对 TypeScript 的功能
针对类型的 props/emit 声明
props 和 emit 都可以通过给 defineProps
和 defineEmits
传递纯类型参数的方式来声明:
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
defineProps
或defineEmits
要么使用运行时声明,要么使用类型声明。同时使用两种声明方式会导致编译报错。- 使用类型声明的时候,静态分析会自动生成等效的运行时声明,从而在避免双重声明的前提下确保正确的运行时行为。
- 在开发模式下,编译器会试着从类型来推导对应的运行时验证。例如这里从
foo: string
类型中推断出foo: String
。如果类型是对导入类型的引用,这里的推导结果会是foo: null
(与any
类型相等),因为编译器没有外部文件的信息。 - 在生产模式下,编译器会生成数组格式的声明来减少打包体积 (这里的 props 会被编译成
['foo', 'bar']
)。 - 生成的代码仍然是有着合法类型的 TypeScript 代码,它可以在后续的流程中被其他工具处理。
- 在开发模式下,编译器会试着从类型来推导对应的运行时验证。例如这里从
- 截至目前,类型声明参数必须是以下内容之一,以确保正确的静态分析:
现在还不支持复杂的类型和从其他文件进行类型导入,但我们有计划在将来支持。
- 类型字面量
- 在同一文件中的接口或类型字面量的引用
使用类型声明时的默认 props 值
针对类型的 defineProps
声明的不足之处在于,它没有可以给 props 提供默认值的方式。为了解决这个问题,我们还提供了 withDefaults
编译器宏:
export interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
上面代码会被编译为等价的运行时 props 的 default
选项。此外,withDefaults
辅助函数提供了对默认值的类型检查,并确保返回的 props
的类型删除了已声明默认值的属性的可选标志。
限制
由于模块执行语义的差异,<script setup>
中的代码依赖单文件组件的上下文。当将其移动到外部的 .js
或者 .ts
文件中的时候,对于开发者和工具来说都会感到混乱。因此,<script setup>
不能和 src
attribute 一起使用。
单文件组件 CSS 功能
组件作用域 CSS
当 <style>
标签带有 scoped
attribute 的时候,它的 CSS 只会影响当前组件的元素,和 Shadow DOM 中的样式封装类似。使用时有一些注意事项,不过好处是不需要任何的 polyfill。它的实现方式是通过 PostCSS 将以下内容:
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
转换为:
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
<template>
<div class="example" data-v-f3f3eg9>hi</div>
</template>
子组件的根元素
使用 scoped
后,父组件的样式将不会渗透到子组件中。不过,子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。
深度选择器
处于 scoped
样式中的选择器如果想要做更“深度”的选择,也即:影响到子组件,可以使用 :deep()
这个伪类:
<style scoped>
.a :deep(.b) {
/* ... */
}
</style>
上面的代码会被编译成:
.a[data-v-f3f3eg9] .b {
/* ... */
}
TIP
通过v-html
创建的 DOM 内容不会被作用域样式影响,但你仍然可以使用深度选择器来设置其样式。
插槽选择器
默认情况下,作用域样式不会影响到 <slot/>
渲染出来的内容,因为它们被认为是父组件所持有并传递进来的。使用 :slotted
伪类以明确地将插槽内容作为选择器的目标:
<style scoped>
:slotted(div) {
color: red;
}
</style>
全局选择器
如果想让其中一个样式规则应用到全局,比起另外创建一个 <style>
,可以使用 :global
伪类来实现 (看下面的代码):
<style scoped>
:global(.red) {
color: red;
}
</style>
混合使用局部与全局样式
你也可以在同一个组件中同时包含作用域样式和非作用域样式:
<style>
/* 全局样式 */
</style>
<style scoped>
/* 局部样式 */
</style>
作用域样式须知
- 作用域样式并没有消除对 class 的需求。由于浏览器渲染各种各样 CSS 选择器的方式,
p { color: red }
结合作用域样式使用时 (即当与 attribute 选择器组合的时候) 会慢很多倍。如果你使用 class 或者 id 来替代,例如.example { color: red }
,那你几乎就可以避免性能的损失。 - 小心递归组件中的后代选择器!对于一个使用了
.a .b
选择器的样式规则来说,如果匹配到.a
的元素包含了一个递归的子组件,那么所有的在那个子组件中的.b
都会匹配到这条样式规则。
CSS Modules
一个 <style module>
标签会被编译为 CSS Modules 并且将生成的 CSS class 作为 $style
对象暴露给组件:
<template>
<p :class="$style.red">This should be red</p>
</template>
<style module>
.red {
color: red;
}
</style>
得出的 class 将被哈希化以避免冲突,实现了同样的将 CSS 仅作用于当前组件的效果。
自定义注入名称
你可以通过给 module
attribute 一个值来自定义注入 class 对象的属性名:
<template>
<p :class="classes.red">red</p>
</template>
<style module="classes">
.red {
color: red;
}
</style>
与组合式 API 一同使用
可以通过 useCssModule
API 在 setup()
和 <script setup>
中访问注入的 class。对于使用了自定义注入名称的 <style module>
块,useCssModule
接收一个匹配的 module
attribute 值作为第一个参数:
import { useCssModule } from 'vue'
// 在 setup() 作用域中...
// 默认情况下, 返回 <style module> 的 class
useCssModule()
// 具名情况下, 返回 <style module="classes"> 的 class
useCssModule('classes')
CSS 中的 v-bind()
单文件组件的 <style>
标签支持使用 v-bind
CSS 函数将 CSS 的值链接到动态的组件状态:
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: 'red'
}
}
}
</script>
<style>
.text {
color: v-bind(color);
}
</style>
这个语法同样也适用于 <script setup>
,且支持 JavaScript 表达式 (需要用引号包裹起来):
<script setup>
const theme = {
color: 'red'
}
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind('theme.color');
}
</style>
实际的值会被编译成哈希化的 CSS 自定义属性,因此 CSS 本身仍然是静态的。自定义属性会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式地更新。