Vue 3 基础

vue3

Vue 3 基础

介绍 Vue 3 的基础使用方法,主要针对与 Vue 2 的不同点

安装引入

可以使用 CDN 引入最新版的 Vue 3 框架

html
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

然后就可以访问暴露的 Vue 对象

也可以使用 npm 安装

bash
# 最新稳定版
$ npm install vue@latest

并在入口文件 main.jsmain.ts 中引入 Vue(或以解构的方式引入 createApp 函数)

ts
import { createApp } from 'vue'

初始化

使用 Vue 的方法 .createApp() 创建一个 Vue 实例应用,它接受一个对象,该对象的属性就是关于根组件的配置选项。

根组件的 props

方法 createApp() 还可以接受第二个(可选)参数,作为根组件的 props

ts
function createApp(rootComponent: Component, rootProps?: object): App
注意

Vue 应用实例 app 会暴露一些方法,例如

  • app.config 允许配置一些应用级的选项
    js
    // 定义一个应用级的错误处理器,用来捕获所有子组件上的错误
    app.config.errorHandler = (err) => {
      /* 处理错误 */
    }
    

    请确保在挂载应用实例之前完成所有应用配置
  • app.component() 注册全局组件

然后再使用实例的方法 .mount(el) 将 Vue 实例其挂载到指定的 DOM 上,这样就将数据渲染进页面,而且是响应式的。

参考 el

参考 el 是一个实际的 DOM 元素或是一个 CSS 选择器字符串,选中的元素会作为 Vue 应用的容器

html
<div>
    <div id="app"></div>
</div>

<script>
// 初始化 Vue 实例
const app = Vue.createApp({
  data() {
    return {
      content: 'Hello World!'
    }
  }
  template: '<div>{{ content }}</div>'
})

// 将实例挂载到页面
app.mount('#app')
</script>

被挂载的应用并不会替换元素,即当我们挂载一个应用时,其渲染内容会替换在 mount(el) 中指定的元素 elinnerHTML(作为子元素,而不是替换 el 元素)

提示

如果根组件不提供模板 template 选项时,Vue 会自动使用容器 elinnerHTML 作为模板,所以也可以直接通过在挂载容器内编写模板

html
<div id="app">
  <!-- 直接在应用的容器中编写模板 -->
  <button @click="count++">{{ count }}</button>
</div>
js
import { createApp } from 'vue'

const app = createApp({
  // 根组件的配置对象中没有 `template` 选项
  data() {
    return {
      count: 0
    }
  }
})

// 挂载应用
app.mount('#app')
说明

Vue 应用的实例所暴露的大多数方法都会返回自身(同一个实例),以便进行链式调用的方式对同一实例进行多种全局配置

js
Vue.createApp({})
  .component('SearchInput', SearchInputComponent) // 注册全局组件
  .directive('focus', FocusDirective) // 注册全局自定义指令
  .use(LocalePlugin) // 使用插件

当调用方法 .mount() 将 Vue 实例挂载到页面后,该方法返回的不是应用本身,而是根组件实例,它是一个 Proxy 形式的对象,就是 MVVM 设计模式中的 vm(即视图和数据的连接层

js
const app = Vue.createApp({
  data() {
    return {
        content: 'Hello World!'
    }
  },
  template: '<div>{{ content }}</div>'
});

const vm = app.mount('#app');

组件实例暴露了一些组件相关的 property,可以在组件的模板中访问这些方法,也可以在组件的选项中访问它们,甚至能够在开发模式下使用浏览器的开发者工具在终端中访问。

这些 property 都有一个 $ 前缀,以避免与用户定义的 property 名称冲突。

js
console.log(vm.$data.content)

// 如果该代码是在组件的选项式 API `data` 中,还可以直接访问
console.log(vm.content)
提示

一个页面可以多次调用 createApp() 创建多个 Vue 实例,并挂载到页面的相应的 DOM 元素上

模板语法

将 Vue 应用挂载到页面后,在该 DOM 节点内部的 HTML 就支持模板语法 Mustache,即使用双花括号包含的部分,在其中支持使用单个 JavaScript 表达式

html
<p>{{ content }}</p>
Tip

可以使用 v-once 指令执行一次性的插值,即只将数据的初始值渲染到页面上

这个值之后就不改变,一般针对的场景是将初始值显示到页面,但是该值不需要响应性变化/更新,使用该指令可以提升页面性能

html
<span v-once>这个将不会改变: {{ msg }}</span>
Tip

使用 v-html 指令可以将指令的值作为 HTML 进行渲染,作为该节点的 innerHTML 内容,但不能在其中再进行数据绑定(即 v-html 指令的值只能是静态的 HTML 内容)。

js
const RenderHtmlApp = {
  data() {
    return {
      rawHtml: '<span style="color: red">This should be red.</span>'
    }
  }
}

Vue.createApp(RenderHtmlApp).mount('#example1')
html
<div id="example1">
  <p>Using v-html directive: <span v-html="rawHtml"></span></p>
</div>

原来页面的 <span> 元素的内容将会被替换成为 rawHtml property 的值,直接作为 HTML,会忽略解析 property 值中的数据绑定,所以不能使用 v-html 来复合局部模板(因为 Vue 不是基于字符串的模板引擎)。

对于用户界面,组件会更适合作为可重用和可组合的基本单位。

DOM 模板解析注意事项

如果你想在 HTML 的 DOM 元素里直接书写 Vue 模板(Vue 从 DOM 中获取模板字符串),由于浏览器的原生 HTML 解析行为限制,有一些需要注意的事项

说明

这些约束规则只适用于直接在 DOM 中编写模板的情况,如果使用以下来源的字符串模板,就不需要顾虑这些限制

  • 单文件组件
  • 内联模板字符串,如 template: '...'
  • <script type="text/x-template">
  • 闭合标签:HTML 只允许一小部分特殊的元素省略其关闭标签,最常见的就是 <input><img>,所以必须显式地写出关闭标签,即 <my-component></my-component>(而不能是 <my-component/>
  • 区分大小写:HTML 标签和属性名称是不分大小写的(浏览器会把任何大写的字符解释为小写),所以一些名称要转换为相应等价的 kebab-case (短横线连字符) 形式
    • 如果组件名称采用 PascalCase 形式,如 <MyComponent />,则在使用组件时需要采用的名称是 <my-component></my-component>
    • 如果 prop 名称采用 camelCase 形式,如 postTitle,则在使用组件时需要设置的 prop 是 <my-component post-title="hello!"></my-component>
    • 如果自定义的事件名称采用 camelCase 形式,如 updatePost,则在使用组件时需要监听的事件名称是 <my-component @update-post="updatePostHandler"></my-component>
  • 元素位置的限制:因为对于 <ul><ol><table><select> 这些元素,其内部允许放置的直接子元素是有严格限制的,如果嵌入其他元素会被视为无效的内容,而提升到外部,造成最终渲染问题。
    如果我们需要在这些元素中使用组件作为直接子元素,则可以在「合法」的子元素上使用属性 is 来指定渲染的实际内容,这时属性 is 用在原生的 HTML 元素上(如 <tr>),该属性值需要使用 vue: 作为前缀,以表示解析的实际上是一个 Vue 组件
    html
    <table>
      <tr is="vue:blog-post-row"></tr>
    </table>
    

指令

指令是为元素所添加的带有 v- 前缀的特殊 attribute,其作用一般是为了更方便地对 DOM 进行操作,Vue 提供多种内置指令,如 v-ifv-bindv-onv-model

指令可以有简写形式,可能需要配合参数 argument 使用,还可以设置修饰符

指令 directive
指令 directive

说明

使用指令时,如果要同时使用参数 arg 和修饰符 modifier,应该按照 v-directiveName:argument.modifier 先后次序使用(因为 arg 只有一个;而 modifier 可以串联多个,应该放在最后)

动态参数

参数在指令后通过一个冒号 : 隔开,一般是一个字符串,也支持采用 JavaScript 表达式动态生成参数

动态参数需要用方括号 [] 包裹

vue
<template>
  <a v-bind:[attributeName]="url"> ... </a>

  <!-- 简写 -->
  <a :[attributeName]="url"> ... </a>
</template>

动态参数中表达式的值应当是一个字符串,或者是 null(显式移除该绑定),如果表达式的结果是其他非字符串的值会触发警告。

注意

在 HTML 文件中 (直接写在 HTML 文件里的模板) ,动态参数表达式因为某些字符的缘故有一些语法限制

  • 不能使用空格和引号
    html
    <!-- 这会触发一个编译器警告 -->
    <a :['foo' + bar]="value"> ... </a>
    
  • 避免在名称中使用大写字母,因为浏览器会强制将其转换为小写,例如 <a :[someAttr]="value"> ... </a> 其中指令会被浏览器转换为 :[someattr] 如果你的组件拥有 “someAttr” 属性而非 “someattr”,这段代码将不会工作。

所以在 HTML 文件中如果需要使用复杂的动态参数,推荐使用计算属性来作为动态参数,而不是直接在模板中编写复杂的表达式

如果采用单文件组件来书写模板,则不受以上限制

自定义指令

Vue 允许注册自定义指令,主要是为了复用关于 DOM 访问/操作的逻辑

<script setup> 中,任何v 开头的驼峰式命名的变量都可以被用作一个自定义指令,在模板中以 v-focus 的形式使用;而在没有使用 <script setup> 的情况下,自定义指令需要通过(组件的配置对象) directives 选项进行局部注册;或通过 app.directive(directiveName, hooks) 进行全局注册

例如自定义了一个名为 directiveName 的指令(作为后缀),然后可以在 Vue 的模板中使用该自定义的指令 <div v-directiveName="value">

推荐

只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。

其他情况下应该尽可能地使用 v-bind 这样的内置指令来声明式地使用模板(以数据驱动/操作视图),这样更高效,也对服务端渲染更友好。

例如创建一个自定义指令 vFocus 当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦

vue
<script setup>
// 在模板中启用 v-focus
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

针对以上场景,使用 Vue 自定指令比使用 HTML 的原生属性 autofocus 更有用,因为它不仅仅可以在页面加载完成后生效,还可以在 Vue 动态插入元素后生效

指令钩子

<script setup> 中,可以直接v 开头的驼峰式命名的变量来设置指令的定义对象,其中包含一系列(可选的)钩子函数

js
<script setup>
const vMyDirective = {
  created(el, binding, vnode, prevVnode) {},
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted(el, binding, vnode, prevVnode) {},
  beforeUpdate(el, binding, vnode, prevVnode) {},
  updated(el, binding, vnode, prevVnode) {},
  beforeUnmount(el, binding, vnode, prevVnode) {},
  unmounted(el, binding, vnode, prevVnode) {}
}
</script>

不同的钩子函数会在(指令所绑定的)元素的不同生命周期阶段执行

  • created 在绑定元素的 attribute 之前或事件监听器被应用之前调用。该指令也可以为元素附加事件监听器,而会在内置指令 v-on 所设置的事件监听器前调用时,这很有用 ❓
  • beforeMount 在元素被插入到 DOM 前调用
  • mounted 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
  • beforeUpdate 绑定元素的父组件更新前调用
  • updated 在绑定元素的父组件及他自己的所有子节点都更新后调用
  • beforeUnmount 绑定元素的父组件卸载前调用
  • unmounted 绑定元素的父组件卸载后调用,只调用一次
指令所使用的钩子函数的入参

指令的钩子函数会传入以下参数,以便操作所绑定的 DOM 和传递数据:

  • el 是指令所绑定的元素,可以用来直接操作 DOM
  • binding 是一个对象,包含以下属性
    • instance 使用该指令的组件实例
    • value 传递给指令的值
      例如对于 v-my-directive="1 + 1" 则该值为 2
    • oldValue 先前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用
    • arg 传递给指令的参数 (如果有的话)
      例如对于 v-my-directive:foo 则 arg 为 foo
    • modifiers 一个对象,包含了传递给指令的修饰符 (如果有的话)
      需要使用对象来「装载」修饰符,是因为一个指令可以有多个修饰符,以链式方式 .a.b.c 串联使用
      例如对于 v-my-directive.foo.bar 则修饰符对象为 {foo: true,bar: true}
    • dir 指令的定义对象(有点像是指令的「源码」,用于调试 ❓),即在注册指令时作为参数传递的对象
      例如在以下自定义的指令中
      js
      app.directive('focus', {
        mounted(el) {
          el.focus()
        }
      })
      

      则该指令的钩子函数的 dir 参数将会是以下对象
      js
      {
        mounted(el) {
          el.focus()
        }
      }
      
  • vnode 代表绑定元素的底层 VNode
  • prevNode 之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用

值得留意的一点是,除了 el 参数外,其他参数都是只读的,即不要更改它们。

若需要在不同的钩子间共享信息,推荐通过元素的 dataset attribute 实现,这也是 HTML 原生元素共享数据的常见方式。

提示

很多时候可能只想使用 mountedupdated 钩子函数,而且想触发相同的行为,可以使用简写形式,直接用一个函数来定义指令

vue
<template>
  <div v-color="color"></div>
</template>
js
// 用一个函数(而不是对象)来定义指令
app.directive('color', (el, binding) => {
  // 该函数会在 `mounted` 和 `updated` 时都调用
  el.style.color = binding.value
})

全局注册

通过 Vue 实例 app 的方法 app.directive(directiveName, hooks) 注册一个全局可用的自定义指令

  • 第一个参数 directiveName 是指令的后缀名
  • 第二个参数 hooks 是一个对象,包含一系列的钩子函数,用于定义指令的行为
js
const app = Vue.createApp({})

app.directive('directiveName', {
// 指令是具有一组生命周期的钩子:
  // 在绑定元素的 attribute 或事件监听器被应用之前调用
  created() {},
  // 在绑定元素的父组件挂载之前调用
  beforeMount() {},
  // 绑定元素的父组件被挂载时调用
  mounted() {},
  // 在包含组件的 VNode 更新之前调用
  beforeUpdate() {},
  // 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
  updated() {},
  // 在绑定元素的父组件卸载之前调用
  beforeUnmount() {},
  // 卸载绑定元素的父组件时调用
  unmounted() {}
})

然后可以在 Vue 应用的任意组件的模板中任何元素上使用自定义的指令 <div v-directiveName="value">

局部注册

在组件的选项 directives 中注册局部指令,也是可以使用 7 种钩子函数。

vue
<script>
export default {
  // ...
  directives: {
    directiveName: {
      // 设定钩子函数
      mounted: function (el) {}
    }
  }
}
</script>

然后可以在该组件的模板中任何元素上使用自定义的指令 <div v-directiveName="value">

使用自定义指令

自定义了一个名为 directiveName 的指令(作为后缀),然后可以在 Vue 的模板中使用该自定义的指令 <div v-directiveName="value">

和内置指令类似,自定义指令的参数也可以是动态的(即传递给指令的参数是根据 JavaScript 表达式计算而得)

vue
<template>
  <div v-example:[arg]="value"></div>
</template>
提示

如果指令需要多个值/参数,可以传入一个 JavaScript 对象字面量

vue
<template>
  <div v-demo="{ color: 'white', text: 'hello!' }"></div>
</template>
js
app.directive('demo', (el, binding) => {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text) // => "hello!"
})

应用到组件

和非 prop 的 attribute 类似,当自定义指令应用到子组件中时,实际上是作用于子组件的根节点上。

parent.vue
vue
<template>
  <MyComponent v-demo="test" />
</template>
MyComponent.vue
vue
<template>
  <div> <!-- v-demo 指令会被应用在此处 -->
    <span>My component content</span>
  </div>
</template>
注意

但是和 attribute 不同,指令能通过 v-bind="$attrs" 应用到其他的子节点上

所以当自定义指令被应用在一个具有多根节点的组件上时,指令会被忽略,并且会抛出一个警告

总的来说,不推荐在组件上使用自定义指令

属性绑定

在模板中使用指令 v-bind标签的属性与动态数据绑定,这样就实现了数据驱动画面(样式)

指令 v-bind:attrName="value" 可以简写为 :attrName="value",绑定的值可以是字符串、对象、数组等。

提示

对于属性 classstyle 这两个特殊的属性,Vue 进行特殊优化增强,绑定的值除了字符串之外,还可以是对象数组,而且将动态绑定的数据与该属性的静态数据进行合并(而其他 attribute 就是「后盖前」)。

如果想将一个 JavaScript 对象上的所有属性分别绑定到元素上,可以采用不带参数的 v-bind

js
const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
}
html
<div v-bind="objectOfAttrs"></div>
注意

如果绑定的值是 nullundefined,那么该 attribute 将不会被包含在渲染的元素上。

说明

对于 DOM 元素的一些 attribute,其属性值是布尔值的(它们只要存在就意味着值为 true),即只在 truthy 时才渲染在元素上,而在 falsy 时不会添加到 DOM 元素上。

但是对于值为 空字符串 "" 的情况,该 attribute 仍会添加到 DOM 元素上,这是为了与 <button disabled=""> 这种形式保持一致。

子组件 Attribute 继承

当组件返回单个根节点时,非 prop attribute 将自动添加到根节点上,作为其 attribute

说明

一个非 prop 的 attribute 是指传向一个组件,但是并未在组件内进行相应 propsemits 定义的 attribute

常见的示例包括 classstyleid 属性

提示

如果不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false

禁用 attribute 自动继承的一个常见需求是要将 attribute 应用于根节点以外的其他(子)节点上

然后通过访问组件的 $attrs property,该 property 包括组件 propsemits property 中未包含的所有属性,将该 property 手动绑定 v-bind="$attrs" 到所需的节点上,就可以将相应的 attribute 应用到该节点。

在 Vue 3 中 $attrs 包含传递给组件的所有 attribute,包括 classstyle

vue
<script>
app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime-local" v-bind="$attrs" />
    </div>
  `
})
</script>

由于 Vue 3 支持组件有多个根节点,如果组件的模板具有多个根节点必须使用 $attrs 显式指定非 Prop 的 attribute 绑定到哪个节点上,否则会弹出警告

vue
<script>
// 这将发出警告
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  `
})

// 没有警告,$attrs 被传递到 <main> 元素
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
  `
})
</script>
提示

当然也可以指定的某个 attribute(例如只将 class 属性)绑定到某个节点;在组件配置参数的业务逻辑中可以通过 this.$attrs 访问所有传递进来的非 props attribute

html
<div id="app">
  <my-component class="baz"></my-component>
</div>
vue
<script>
// 将 class 属性绑定到第一个根节点
app.component('custom-layout', {
  template:`
    <div :class="$attrs.class">Hello</div>
    <div>World</div>
  `
})
</script>

双向绑定

在模板的标签中使用指令 v-model 实现表单的值与动态数据双向绑定,即可以实现在表单 <input><textarea><select> 等标签,通过输入数据可以修改动态数据变量 data property;也可以通过修改相应的动态数据 data property 控制表单的值。

说明

v-model 本质上不过是语法糖,它在内部为不同的表单元素监听不同的 property 以响应用户的输入,并抛出不同的事件,然后在事件处理函数中修改绑定的响应式数据,实现双向绑定,如果需要客制化可以修改相应的事件处理函数。

v-model 「接管」后会忽略任何表单元素上初始的 valuechecked 或 selected attribute,它将始终将当前绑定的 JavaScript 状态视为数据的正确来源,所以应该在 JavaScript 中使用响应式系统的 API 来声明该初始值。

v-model 实际上是为不同的表单元素绑定不同的 property 并监听不同的事件:

  • <input type="text"><textarea> 元素绑定 value property 并监听 input 事件
    注意

    对于文本域 <textarea> 以模板语法 {{ value }} 绑定动态数据是不会生效

    html
    <!-- ⚠️ 这样并不会实现双向绑定 -->
    <textarea>{{text}}</textarea>
    

    应用使用 v-model 来实现双向绑定

    html
    <textarea v-model="message" placeholder="add multiple lines"></textarea>
    
  • <input type="checkbox"><input type="radio"> 元素绑定 checked property 并监听 change 事件
    提示

    对于多个复选框,可以绑定到同一个数组或集合的变量值,以便收集当前被选中的所有值

    vue
    <template>
      <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
      <label for="jack">Jack</label>
    
      <input type="checkbox" id="john" value="John" v-model="checkedNames">
      <label for="john">John</label>
    
      <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
      <label for="mike">Mike</label>
    </template>
    <script setup>
    const checkedNames = ref([])
    </script>
    
  • <select> 元素绑定 value property 并监听 change 事件
    注意

    如果 v-model 表达式的初始值不匹配任何一个选择项<select> 元素会渲染成一个**「未选择」的状态**。

    在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此建议提供一个空值的禁用选项作为第一个选项来「占位」(默认选项/提示词选项)

    vue
    <template>
      <select v-model="selected">
        <option disabled value="">Please select one</option>
        <option>A</option>
        <option>B</option>
        <option>C</option>
      </select>
    </template>
    
提示

对于文本输入的表单,如果用户使用 IME 的语言 (中文,日文和韩文等), v-model 不会在 IME 输入还在拼字阶段时触发更新。

如果想在拼字阶段也触发更新,则需要手动编写 input 事件监听器和 value 属性,而不要使用 v-model

修饰符

Vue 提供了一些修饰符,方便地对表单输入值进行设置:

  • number 修饰符:如果希望绑定的动态数据变量获得的是数值类型,可以使用该表单修饰符(表单的值默认是字符串类型)
    tipbox
    如果该值无法被 `parseFloat()` 处理,那么将返回原始值
    
    `number` 修饰符会在输入框有 `type="number"` 时自动启用
    
  • trim 修饰符:表单修饰符 v-model.trim 自动过滤用户输入的首尾空白字符
  • lazy 修饰符:表单修饰符 v-model.lazychange 事件之后(即表单的输入框失去焦点后)进行数据更新同步(而默认是在 input 事件触发后,如果用户使用的是 IME,则拼字阶段的状态是例外的)
提示

支持以类似于链式调用的方式设置多个修饰符

html
<input type="text" v-model.trim.number="content" />

另设值

对于单选按钮 <input type="radio" />、复选框 <input type="checkbox"> 以及选择框 <select>,有时候希望这些表单的最终值是另设的,这样可以根据双向绑定的值变动,再设置相应的真实的表单值(甚至可以将对象这一类复杂的数据结构作为表单值)

  • 复选框:可以使用 true-valuefalse-value 属性
    html
    <input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
    
    js
    // when checked:
    vm.toggle === 'yes'
    // when unchecked:
    vm.toggle === 'no'
    
  • 单选框:可以绑定 value 属性
    html
    <input type="radio" v-model="pick" v-bind:value="a" />
    
    js
    // 当选中时
    vm.pick === vm.a
    
  • 选择框选项:绑定 <option> 元素的 value 属性(本来是以选中的 <option> 元素的内容 innerText 作为该表单的值)
    html
    <select v-model="selected">
      <!-- 内联对象字面量 -->
      <option :value="{ number: 123 }">123</option>
    </select>
    
    js
    // 当被选中时
    typeof vm.selected // => 'object'
    vm.selected.number // => 123
    

在组件上使用 v-model

在使用自定义的输入型组件时,Vue 为它们进行优化,也支持使用 v-model 实现双向绑定。

html
<custom-input v-model="searchText"></custom-input>

<!-- 等价于以下代码 -->
<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>

在使用该组件时(在外部,在组件的标签上):通过指令 v-model 双向绑定需要同步的变量,它的数据默认作为一个名为 modelValue 的 prop 传递到组件内(因此需要在组件内部的 props 里声明 modelValue,并将其绑定到内部的表单元素的 value 属性上)

在这些组件的内部需要进行相应的处理:

  • 在组件内使用指令 v-bind表单元素的 value 属性绑定到以上声明的 prop modelValue 上,即 v-bind:value="modelValue"(这样就可以将外部传进来的值作为表单的值,实现与外部数据的「同步」)
  • 在组件内使用指令 v-on 监听表单元素 input 输入事件,并通过抛出 update:modelValue 事件 $emit('update:modelValue', $event.target.value) 向外传递相应的数据(如果表单是 checkbox 应该传递的数据是 $event.target.checked

然后外部就会接收抛出的事件,并对指令 v-model 绑定的变量(基于抛出的数据)进行改变(即数据的修改操作还是在父层完成

js
// component
app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})
html
<custom-input v-model="searchText"></custom-input>
注意

Vue 2 提供了 .sync 修饰符,通过 v-bind:propName.sync="variable"$emit('update:propName', newValue) 实现类似双向绑定的功能;但在 Vue 3 中 .sync 修饰符已被移除,而现在 Vue 3 使用类似的语法(组件也是抛出以 update: 为前缀的事件)实现的。

在组件内,默认传递的 prop 变量是 modelValue,默认监听抛出的事件是 update:modelValue,这种方法更通用,对于 type="text"type="checkbox" 其语义都不会混淆。

如果需要修改 modelValue 这个默认值,在 Vue 2 中可以通过选项 model 进行配置,但在 Vue 3 中 model 选项已移除,作为替代可以在父级中v-model 添加参数,这个参数 argument 就是组件内的 prop 变量名和需要监听的事件名(加上 update: 前缀)

html
<!-- 使用组件 -->
<my-component v-model:title="bookTitle"></my-component>

<!-- 是以下的简写 -->
<my-component :title="bookTitle" @update:title="bookTitle = $event" />
js
// 组件
app.component('my-component', {
  props: {
    title: String
  },
  emits: ['update:title'],
  template: `
    <input
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})

在 Vue 3 中利用特定的 prop(通过指令 v-model 的参数来设置)和抛出相应事件,可以在单个组件上创建多个 v-model,从而实现多个 prop 的双向绑定

html
<!-- 使用组件,实现 first-name 和 last-name 的双向绑定 -->
<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
js
app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName'],
  template: `
    <input
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
})

自定义修饰符

另外,在输入型组件上使用指令 v-model 时,不仅支持 Vue 内置的修饰符 .trim.number.lazy,Vue 3 还支持添加自定义的修饰符,添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。

html
<!-- 使用自定义修饰符,实现字符串首字母大写 -->
<my-component v-model.capitalize="myText"></my-component>
js
// 组件
app.component('my-component', {
  props: {
    modelValue: String,
    // modelModifiers prop 这里我们提供了初始值
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    // 表单 input 事件的处理函数
    emitValue(e) {
      let value = e.target.value
      // 如果 v-model 有修饰符 capitalize 就对表单输入值实现首字母大写
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      // 抛出事件
      this.$emit('update:modelValue', value)
    }
  },
  template: `
    <input type="text"
      :value="modelValue"
      @input="emitValue">`,
  created() {
    // 当组件的 created 生命周期钩子触发时,会根据调用组件的情况对 modelModifiers prop 进行设置
    // 由于这个例子中 v-model 的修饰符为 capitalize,因此 modelModifiers 对象会包含 capitalize 属性
    console.log(this.modelModifiers) // { capitalize: true }
  }
})
说明

如果指令 v-model 需要同时设置参数 argument 以及修饰符 modifier,则修饰符应该在参数后

html
<my-component v-model:description.capitalize="myText"></my-component>

如果 v-model 设置了参数,则组件内接受修饰符的 prop 的名称(默认为 modelModifiers)也会作出相应的改变,prop 名称采用 arg + Modifiers 的形式

js
app.component('my-component', {
  props: ['description', 'descriptionModifiers'],
  emits: ['update:description'],
  template: `
    <input type="text"
      :value="description"
      @input="$emit('update:description', $event.target.value)">
  `,
  created() {
    console.log(this.descriptionModifiers) // { capitalize: true }
  }
})

条件渲染

指令 v-ifv-show 都可以实现条件渲染,元素只会在指令的表达式返回 truthy 值的时候被渲染或显示出来。

一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

v-if

在模板标签中使用指令 v-if 控制 DOM 的渲染,只有条件指令的值为 true 时才渲染元素,否则 DOM 被移除(或不进行初始化渲染生成)。

提示

如果想同时控制多个元素的条件渲染,可以使用 <template> 元素作为容器,它被当做不可见的包裹元素,即最终的渲染结果将不包含 <template> 元素。

还可以使用 v-ifv-else-ifv-else 设置同级并列元素的条件渲染,这三个指令的元素需要连续且同级。

提示

对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key。如果你手动提供 key,那么需要保证每个分支必须使用唯一的 key

v-show

在模板标签中使用指令 v-show 也是控制 DOM 的渲染,只有条件指令的值为 true 时才显示元素,否则 DOM 被隐藏

注意

v-show 不支持 <template> 元素,因为 v-show 是要元素先进行渲染再通过 CSS 控制显示/隐藏的,因此只能作用于可以在页面渲染出 DOM 的元素。

列表渲染

在模板标签中使用指令 v-for 可以将响应式数据中的数组渲染为一系列的 DOM 元素,称为列表渲染,常用形式是 v-for item in arrv-for item of arr

除了可以遍历 arr 数组以外,还支持遍历 obj 对象、number(从 1 开始)数字等

注意

在遍历 objList (类数组的对象)的时候,无法保证每次遍历 item 的先后次序

在使用 v-for 时其中很重要的一点是需要为生成的各个元素设置 key 属性,一般绑定的是与该元素相关的唯一标识(尽量不要直接用 index,因为一个元素删除后可能会导致其他元素的排序改变,从而影响 key 唯一性),以便提高 DOM 更新的效率。

提示

因为 key 作为一个元素的标识,那么「反过来」利用它也是可行的,即手动更改一个元素的 key 属性值,以强制替换元素/组件而不是重复使用它。

例如针对以下的场景

  • 完整地触发组件的生命周期钩子
  • 触发过渡
说明

Vue 3 可以将指令 v-for 应用到元素 <template>,同时可以在 <template> 元素上指定 key 属性,而不必像 Vue 2 那样将 key 属性设置在子元素上

注意

v-if 会拥有比 v-for 更高的优先级,这与 Vue 2 中的工作方式正好相反。

如果在同一个元素上使用 v-forv-if,则 v-if没有权限访问 v-for 里的变量

vue
<!-- This will throw an error because property "todo" is not defined on instance. -->

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

官方文档推荐使用 <template> 标签把 v-for 移动到「外层」来修正这个问题。

vue
<template v-for="todo in todos" :key="todo.name">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</template>

不推荐将 v-ifv-for 写到同一个元素/层级里,也不是采用前面的方案先进行迭代再进行判断(消耗性能)

更优的方案是将 v-if 的筛选逻辑通过 computed 来实现,再用 v-for 来迭代这个计算属性

注意

由于对象和数组都是引用数据类型,所以修改它们后常常会出现无法正确地触发响应式变化的问题,例如无法触发列表重新渲染。

如果是采用直接通过下标来修改数组的值,这样通常是无法触发视图进行响应式地更新

而如果使用数组变更方法(这些方法对调用它们的原数组进行变更),即 push()pop()shift()unshift()splice()sort()reverse(),Vue 特意做出优化,通过这些方法修改数组 Vue 可以侦听到,相关依赖会做出响应式变化

其中应该特别留意 reverse()sort() 方法,这两个方法将变更原始数组,所以不能用于 computed 中,因为该选项是不应该有副作用的(即不能对依赖的响应式变量进行修改),应该先创建一个原数组的副本再调用这些方法

diff
- return numbers.reverse()
+ return [...numbers].reverse()

相对地,数组有一些不可变 immutable 方法(这些方法并没有修改原数组,而是返回一个新的数组),即 filter()concat()slice(),如果使用这些方法就需要用返回的结果/数组替换响应性变量的旧数组

js
// `items` 是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/))

面对整个数组替换的操作,Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作

Teleport

有时组件模板的一部分代码从逻辑上属于该组件,而从 UI 角度来看,最好将模板的这一部分移动到 Vue app 之外的其他位置,例如弹出式的模态框 modal/提示框这一类全屏模式的组件,应该作为 <body> 元素的直接子元素,而不是深度嵌套在组件的一堆 <div> 中。

Vue 3 新增的内置组件 <teleport> 提供了一种干净的方法,允许我们自由地控制在容器中的模板渲染在页面的哪个 DOM 节点下。

内置组件 <teleport> 作为容器,其属性 to 指定内容需要插入到哪一个 DOM 节点,属性值可以是标签名,如 body,也可以是 CSS 选择器。

注意

<Teleport> 挂载时,传送的 to 目标必须已经存在于 DOM 中。理想情况下,这应该是整个 Vue 应用 DOM 树外部的一个元素。

如果目标元素也是由 Vue 渲染的,你需要确保在挂载 <Teleport> 之前先挂载该元素

js
app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>
      <!-- 模态框内容渲染为 body 标签的子级 -->
    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal!
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return {
      modalOpen: false
    }
  }
})
提示

<teleport> 容器内包含 Vue 组件时,虽然最终渲染在指定的 DOM 节点下,但是逻辑上的父子组件关系并不会变,即来自父组件的注入依然按预期工作,并且在 Vue Devtools 中,子组件将嵌套的父组件之下,而不是放在实际内容移动到的位置。

提示

多个 <teleport> 组件可以挂载到同一个目标元素,以追加的模式叠加,即稍后挂载的元素位于较早的挂载之后,一个常见的用例场景是一个可重用的 <Modal> 组件用于弹出消息,挂载在 <body> 元素下,可能同时有多个实例处于活动状态,从 UI 角度看到的效果是新的模态框就会覆盖旧的模态框。

可以通过 disabled prop 来禁用 <Teleport> 的「转移」功能,让它充当普通的容器组件。

例如要在桌面端将一个组件当做浮层来渲染,但在移动端则当作行内组件,可以通过对 <Teleport> 动态地传入一个 disabled prop 来处理这两种不同情况

vue
<template>
  <!-- isMobile 可以根据 CSS media query 的不同结果动态地更新 -->
  <Teleport :disabled="isMobile">
    <!-- ... -->
  </Teleport>
</template>

响应式变量

在配置(根)组件时,可以在选项式 API data() 函数中声明响应式变量,然后将它们作为返回值包在一个对象中,Vue 会通过响应性系统将其包裹起来,在组件实例创建后,以 $data 的形式存储在组件实例 vm 中。

这样在就可以在模板中直接使用它们,而在其他选项式 API 函数中则使用 this.$data.varName(其中 this 是组件实例 vm,在 Vue 外部也可以通过 vm.$data 访问该响应式对象)来访问。

为方便起见,该对象的任何顶级 property 可以直接通过组件实例暴露出来,即可以通过 this.varName 替代。

js
const app = Vue.createApp({
  data() {
    return { count: 4 }
  }
})

vm = app.mount('#app');

console.log(vm.$data.count) // 4
console.log(vm.count)       // 4
注意

这些 property 仅在实例首次创建时被添加,所以你需要确保它们都在 data 函数返回的对象中。必要时,要对尚未提供所需值的 property 使用 nullundefined 或其他值作为占位的值/初始值。

计算属性和侦听器

在选项 computed 中创建计算属性,在选项 watch 创建监听器,两者都是基于响应式数据的变化再执行操作的,但用处有不同。

computed

计算属性常常是为了将复杂和需要重复使用的资料处理逻辑,从模板中的函数表达式提出来。它类似 data 的属性一样,作为响应式数据在模板上直接使用

如果希望计算属性实时更新,那么它需要有响应式依赖(基于 data 或其他来源的响应式数据),并且返回一个值

计算属性的最大优势是对于响应式依赖有缓存,即计算属性只在相关响应式依赖发生改变时它们才会重新求值。

提示

如果不希望有缓存,请用 method 来替代,同样可以实现相同的功能,相比之下,每当触发组件重新渲染时,方法 method 函数将总会再次执行

watch

可以将 watch 看作是更通用的响应数据侦听方式。

watch 也是依赖响应式数据的,可以是 data property 或 computed property(也可以是这些 property 的嵌套 property),然后在响应式数据发生变化时触发回调函数,在回调中可以执行异步操作,可以设置中间状态,而且不必返回值

使用选项式 API 创建侦听器除了以函数形式(函数名就是需要侦听的响应式变量)以外,还可以是对象的形式,对侦听器进行更详细的设置

js
watch: {
  // 函数形式,侦听响应式变量 a
  a(val, oldVal) {
    console.log(`new: ${val}, old: ${oldVal}`)
  },
  // 回调函数以函数名来表示(字符串)
  b: 'someMethod',
  // 对象形式
  // 配置了 deep 为 true
  // 该回调会在被侦听的对象 c 的任何 property 改变时被调用,不论其被嵌套多深
  c: {
    handler(val, oldVal) {
      console.log('c changed')
    },
    deep: true
  },
  // 侦听对象 c 的属性 d(嵌套属性)
  'c.d': function (val, oldVal) {
  // do something
  },
  // 该回调函数将会在侦听开始之后被立即调用一次
  e: {
    handler(val, oldVal) {
      console.log('e changed')
    },
    immediate: true
  },
  // 多个回调函数构成的数组,当响应式变量 f 发生变动时,这些回调函数会被逐一调用
  f: [
    'handle1',
    function handle2(val, oldVal) {
      console.log('handle2 triggered')
    },
    {
      handler: function handle3(val, oldVal) {
        console.log('handle3 triggered')
      }
    }
  ]
}
注意

不能直接侦听响应式对象的属性值,因为这相当于解构对象而造成响应性丢失

js
const obj = reactive({ count: 0 })

// ⚠️ 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
  console.log(`count is: ${count}`)
})

应该监听返回该属性的 getter 函数

js
// 提供一个 getter 函数
watch(
  () => obj.count,
  (count) => {
    console.log(`count is: ${count}`)
  }
)
深层侦听器的区别

如果传入一个响应式对象(由 reactive() 包裹的对象),会隐式地创建一个深层侦听器,即对象的任何属性(包括嵌套的属性)变更时都会触发回调函数

js
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
  // ⚠️ 注意传入的参数 `newValue` 和 `oldValue` 是相等的
  // 因为它们引用的是同一个对象
})

如果传入一个返回响应式对象的 getter 函数,则只有在返回不同的对象时(即对象进行整体替换时),才会触发回调

js
watch(
  () => state.someObject,
  () => {
    // 仅当 state.someObject 被替换时触发
  }
)

可以给上面的例子显式地加上 deep 选项,强制转成深层侦听器

深度侦听需要遍历被侦听对象中的所有嵌套的属性,当监听的对象比较大时需要留意性能的消耗,因此只在必要时才使用它

提示

在 Vue 2 中 watch 一般只能监听一个响应式数据(如果要监听多个变量,需要使用箭头函数将他们组合返回再给 watch 监听,相当于侦听一个 computed

而在 Vue 3 中是支持以数组的形式监听多个数据源和响应式对象

js
// 侦听多个源 [fooRef, barRef]
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

方法

methods 选项向组件实例添加方法,该选项是一个对象,其中各属性就是该组件可以调用的方法。

可以在组件模板中直接调用方法,也可以在其他选项中通过 this.methodName([params]) 来调用。

注意

Vue 自动为 methods 绑定 this,以便于它始终指向组件实例,因此在定义这些方法时应该避免使用箭头函数(因为这会阻止 Vue 绑定恰当的 this 指向)

注意

另外官方文档还提到一个关于防抖的例子值得注意。

组件实例化的时候,虽然将组件的 methods 选项里的函数的 this 进行绑定(指向了组件实例),但是函数还是共享。因此在同一个页面复用组件时,进行防抖实际调用的是同一个函数,可能导致与预期不一样的效果,具体分析可以参考这篇文章

因此正确的做法是在生命周期钩子的 created 里,将防抖函数添加到组件实例上,这样就可以让每个组件都有自己独立的防抖函数

js
app.component('save-button', {
  created() {
    // 用 Lodash 的防抖函数对事件处理函数进行封装
    // this 指的是组件实例
    // 因此 debouncedClick 函数是每个组件实例彼此独立的
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // 移除组件时,取消定时器
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... 响应点击的具体处理函数 ...
    }
  },
  template: `
    <button @click="debouncedClick">
      Save
    </button>
  `
})

事件处理

在模板的标签中使用指令 v-on:event-name 监听事件(也可以使用简写形式 @event-name),当事件被触发时会调用相应的回调函数。

提示

在模板中设置的事件监听器时,所指定的事件类型的名称需要 kebab-case 形式

其回调函数默认传递 event 事件作为参数

vue
<template>
  <div id="event-with-method">
    <button @click="greet">Greet</button>
  </div>
</template>

<script>
  // ...
  methods: {
    greet(event) {
      // `event` 是原生 DOM event
      console.log(event)
    }
  }
</script>

如果希望同时传递参数和事件,可以用特殊变量 $event 把事件传入回调函数中(记得在事件处理函数中设置相应的形参),或使用箭头函数

vue
<template>
  <!-- 在事件处理器中使用特殊的 $event 变量 -->
  <button @click="warn('Form cannot be submitted yet.', $event)">
    Submit
  </button>
  <!-- 使用内联事件处理器,箭头函数的形式 -->
  <button @click="(event) => warn('Form cannot be submitted yet.', event)">
    Submit
  </button>
</template>

<script>
  // ...
  methods: {
    warn(message, event) {
      // 现在既可以接收到自定义的参数,也可以访问到原生事件
      if (event) {
        event.preventDefault()
      }
      alert(message)
    }
  }
</script>
提示

支持为同一个事件设置多个处理函数,使用逗号 , 分隔,而且回调函数都要加上括号 ()即使没有传递参数

vue
<template>
  <!-- 按钮被点击时,将执行 handlerOne() 和 handlerTwo() 这两个回调函数 -->
  <button @click="handlerOne(), handlerTwo($event)">
    Submit
  </button>
</template>
提示

支持 v-on 不带参数绑定一个事件/监听器键值对的对象

html
<!-- 对象语法 (只适用版本 2.4.0+ 以上的 Vue 中) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

当使用对象语法时,是不支持任何修饰器的

修饰符

Vue 提供了多种事件修饰符,用于更方便地对事件的行为进行设置,这样事件处理函数就可以只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。事件修饰符可以链式调用多个。

  • .stop 阻止事件继续传播
  • .prevent 阻止事件的默认行为,例如表单提交后跳转到指定网址
  • .self事件在该元素自身触发时才执行事件处理函数
  • .once 只对事件进行一次响应后失效
  • .capture 使用事件捕获模式
  • .passive 一把针对移动端 scroll 事件进行优化,提高性能(滚动事件将立即发生而非等待 onScroll 完成)
    注意

    但不要把 .passive.prevent 一起使用 .prevent 将会被忽略,因为 .passive 已经向浏览器表明了你不想阻止事件的默认行为。

    同时使用浏览器可能会向你展示一个警告

  • 一系列的鼠标和按键修饰符:UI 用户界面的交互事件十分常见,如鼠标事件、按键事件等,Vue 提供了相应的鼠标按键修饰符,例如可以直接将键盘事件对象 KeyboardEvent.key 所暴露的任意有效按键名转换为 kebab-case 来作为修饰符,这样就可以针对特定的按键设置响应函数
    html
    <!-- 处理函数只会在 $event.key 等于 'PageDown' 时被调用 -->
    <input @keyup.page-down="onPageDown" />
    
    注意

    在 Vue 3 中不再支持使用数字 (即键码) 作为 v-on 修饰符

注意

使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。

因此使用 @click.prevent.self 会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent 则只会阻止对元素本身的点击事件的默认行为。

使用自定义事件抛出一个值

在父子组件中间监听抛出的事件时,子组件在模板中使用 $emit() 方法抛出事件,第一个参数是事件名称,第二参数(可选)是需要传递的数据

child.vue
vue
<button @click="$emit('enlargeText', 0.1)">Enlarge text</button>
提示

如果希望抛出子组件的事件,可以使用特殊的变量 $event 作为参数

child.vue
vue
<!-- 使用 $event 访问子组件的点击事件,然后作为自定义事件 enlargeText 的参数抛出,让父组件可以接收 -->
<button @click="$emit('enlargeText', $event)">
  Enlarge text
</button>

然后在父组件中监听这个事件,其回调函数会将子组件所抛出的数据作为第一个参数

parent.vue
vue
<template>
  <blog-post @enlarge-text="onEnlargeText"></blog-post>
</template>

<script>
  // ...
  methods: {
    onEnlargeText(enlargeAmount) {
      this.postFontSize += enlargeAmount
    }
  }
</script>
提示

如果父组件中不使用回调函数,而直接使用内联处理器的写法,则可以使用特殊的变量 $event 来访问子组件所被抛出的这个值

parent.vue
vue
<blog-post @enlarge-text="postFontSize += $event"></blog-post>

emits 选项

Vue 3 新增了一个 emits 选项(其作用和选项 props 作用类似),用来定义子组件可以向其父组件触发的事件类型

提示

强烈建议使用 emits 记录/声明每个组件可以触发的所有类型的(自定义)事件,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。

因为 Vue 3 移除.native 修饰符,对于在子组件中的选项 emits 定义的事件,Vue 现在将把它们作为原生事件监听器(这一点和 Vue 2 不同,Vue 2 对于在使用组件时才添加的事件监听,都认为是监听自定义事件),添加到子组件的根元素中(除非在子组件的选项中设置了 inheritAttrs: false

vue
<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>

<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

该选项可以接受字符串作为元素的数组,也可以接收一个对象(可以为事件设置验证器,和 props 定义里的验证器类似)。

在对象语法中,每个 property 的值可以为 null 或验证函数,该函数将接收调用 $emit 时传递的数据。验证函数应返回布尔值,以表示事件参数是否有效。

vue
<script>
  const app = createApp({})

  // 数组语法
  app.component('todo-item', {
    emits: ['check'],
    created() {
      this.$emit('check')
    }
  })

  // 对象语法
  app.component('reply-form', {
    emits: {
      // 没有验证函数
      click: null,

      // 带有验证函数
      submit: payload => {
        if (payload.email && payload.password) {
          return true
        } else {
          console.warn(`Invalid submit event payload!`)
          return false
        }
      }
    }
  })
</script>

Copyright © 2024 Ben

Theme BlogiNote

Icons from Icônes