Vue3 基本语法

模板语法

将数据插入 HTML

双大括号文本插值

javascript
1<span>Message: {{ msg }}</span>

绑定事件

javascript
1  <button @click="increment">Count is: {{ count }}</button>

插入 HTML

js
1<script setup>
2import { ref } from 'vue'
3
4const a='<div style="color:red">1123</div>'
5</script>
6
7<template>
8  <div v-html='a'>
9  </div>
10</template>

将 Attribute 属性插入到 HTML 中

js
1// v-bind 指令
2<div v-bind:id="dynamicId"></div>
3// 简写
4<div :id="dynamicId"></div>

布尔值 Attribute

javascript
1<button :disabled="isButtonDisabled">Button</button>
2<button :hidden="status">Button</button>

如果isButtonDisabled和status是真值或者空字符串,则表示 true

绑定多个 Attribute

js
1const objectOfAttrs = {
2  id: 'container',
3  class: 'wrapper',
4}
js
1<div v-bind="objectOfAttrs"></div>

使用表达式

js
1{{ number + 1 }}
2
3{{ ok ? 'YES' : 'NO' }}
4
5{{ message.split('').reverse().join('') }}
6
7<div :id="`list-${id}`"></div>

在模板中使用函数

js
1<span :title="toTitleDate(date)">
2  {{ formatDate(date) }}
3</span>

指令

常用指令:

v-if 根据某个条件的真假插入/移除该 html

javascript
1<p v-if="seen">Now you see me</p>

v-else跟v-if对应的指令,配合使用

js
1<div v-if="Math.random() > 0.5">
2  Now you see me
3</div>
4<div v-else>
5  Now you don't
6</div>

v-else-if 同上

v-bind 绑定 Attribute

v-html插入 HTML

v-for

v-show 根据某个条件的真假来给元素加上 display-none属性

动态参数

html
1<div :[attributeName]="attr"></div>
2<a @[eventName]="doSomething"></a>

动态参数的意思就是修改绑定的特性名,比如从class 修改成 id,把click事件改成focus事件等。

动态参数值需要时字符串或者是 null。如果是 null 则显式移除该绑定。

动态参数语法上不能用空格和引号,否则会报编译器错误。

html
1<!-- 这会触发一个编译器警告 -->
2<a :['foo' + bar]="value"> ... </a>

如果动态参数很复杂,则需要使用计算属性来替换复杂的表达式。

修饰符

使用 vue 指令时,可以额外使用一些修饰符来做特殊的处理,比如.prevent修饰符表示触发事件时顺便调用event.preventDefault()

html
1<form @submit.prevent="onSubmit">...</form>

响应式状态

声明响应式状态

使用reactive声明响应式状态:

js
1import { reactive } from 'vue'
2
3const state = reactive({ count: 0 })

使用 ts 标注类型,官方不推荐使用泛型,所以我们是将类型写在变量前面:

js
1import { reactive } from 'vue'
2
3interface Book {
4  title: string
5  year?: number
6}
7
8const book: Book = reactive({ title: 'Vue 3 指引' })

组件模板中使用状态需要在 setup 函数中定义并返回

js
1import { reactive } from 'vue'
2
3export default {
4  // `setup` 是一个专门用于组合式 API 的特殊钩子
5  setup() {
6    const state = reactive({ count: 0 })
7    function increment() {
8      state.count++
9    }
10
11    // 暴露 state 到模板
12    return {
13      state,
14    }
15  },
16}

使用 SFC 的<script setup>可以简化上面的样板代码

html
1<script setup>
2  import { reactive } from 'vue'
3
4  const state = reactive({ count: 0 })
5
6  function increment() {
7    state.count++
8  }
9</script>
10
11<template>
12  <button @click="increment">{{ state.count }}</button>
13</template>

DOM 更新时机

当对某一个状态进行更改后,DOM 会自动更新,但是这并不是同步进行的。Vue 将它们缓冲到下一个周期一起更新以便每个组件只需要更新一次。

如果需要等一个状态改变完成后的 DOM 更新完成后做某个操作,则需要使用nextTick()这个 API。

js
1import { nextTick } from 'vue'
2
3function increment() {
4  state.count++
5  nextTick(() => {
6    // 访问更新后的 DOM
7  })
8}

深层响应

Vue 内部用了 proxy 监听状态改变,在 ES6 中 proxy 是深层监听的,所以任何状态改动都能够被检测到。

js
1import { reactive } from 'vue'
2
3const obj = reactive({
4  nested: { count: 0 },
5  arr: ['foo', 'bar'],
6})
7
8function mutateDeeply() {
9  // 以下都会按照期望工作
10  obj.nested.count++
11  obj.arr.push('baz')
12}

还可以创建浅层响应式对象。

代理不等于原始对象

响应式状态返回的是 proxy 对象,它跟原始对象是不相等的。

js
1const raw = {}
2const proxy = reactive(raw)
3
4// 代理和原始对象不是全等的
5console.log(proxy === raw) // false

只有更改代理才会触发更新,更改原始对象不会触发更新操作。

对同一个对象调用reactive会返回同样的代理,对一个已存在的代理调用reactive也会返回同样的代理。

js
1// 在同一个对象上调用 reactive() 会返回相同的代理
2console.log(reactive(raw) === proxy) // true
3
4// 在一个代理上调用 reactive() 会返回它自己
5console.log(reactive(proxy) === proxy) // true

reactive 限制

  1. reactive只对集合类型有效,即(对象、数组、Map、Set)等,对原始类型无效

  2. 不能改变响应式对象的引用

    js
    1let state = reactive({ count: 0 })
    2
    3// 这行不通!
    4state = reactive({ count: 1 })

    如果将响应式对象的数据结构出来,或者传递进一个函数,这也是不行的,会失去响应性

    js
    1const state = reactive({ count: 0 })
    2
    3// n 是一个局部变量,同 state.count
    4// 失去响应性连接
    5let n = state.count
    6// 不影响原始的 state
    7n++
    8
    9// count 也和 state.count 失去了响应性连接
    10let { count } = state
    11// 不会影响原始的 state
    12count++
    13
    14// 该函数接收一个普通数字,并且
    15// 将无法跟踪 state.count 的变化
    16callSomeFunction(state.count)

ref 定义

为了解除reactive的限制,Vue 引进了 ref来创建任何值类型的响应式 ref

js
1import { ref } from 'vue'
2
3const count = ref(0)

ref() 从参数中获取到值,将其包装为一个带 .value property 的 ref 对象:

js
1const count = ref(0)
2
3console.log(count) // { value: 0 }
4console.log(count.value) // 0
5
6count.value++
7console.log(count.value) // 1

给 ref 标注类型的两种方式:

js
1// 得到的类型:Ref<string | number>
2const year = (ref < string) | (number > '2020')
3
4year.value = 2020 // 成功!
html
1import { ref, Ref } from 'vue' const year: Ref<string | number> = ref('2020') year.value = 2020 // 成功!</string>

如果标注了泛型但是没有给出初始值,最后得到的是一个包含undefined的联合类型

html
1// 推导得到的类型:Ref<number | undefined> const n = ref<number>()</number></number>

ref.value也是响应式的,当值为对象类型时,会用reactive自动转换它的.value

一个包含对象类型值的 ref 可以响应式地替换整个对象:

js
1const objectRef = ref({ count: 0 })
2
3// 这是响应式的替换
4objectRef.value = { count: 1 }

ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:

js
1const obj = {
2  foo: ref(1),
3  bar: ref(2),
4}
5
6// 该函数接收一个 ref
7// 需要通过 .value 取值
8// 但它会保持响应性
9callSomeFunction(obj.foo)
10
11// 仍然是响应式的
12const { foo, bar } = obj

也就是说 ref 不会丢失响应性,并且能够监听原始类型的变量。

ref 在模板中的解包

在模板中不需要.value,会自动被解包

js
1<script setup>
2import { ref } from 'vue'
3
4const count = ref(0)
5
6function increment() {
7  count.value++
8}
9</script>
10
11<template>
12  <button @click="increment">
13    {{ count }} <!-- 无需 .value -->
14  </button>
15</template>

顶层 property 才能解包成功,非顶层会解包失败

js
1const object = { foo: ref(1) }

下面的表达式会导致解包失败

js
1{
2  {
3    object.foo + 1
4  }
5}

解构成顶级的 property 才能解包成功

js
1const { foo } = object
js
1{
2  {
3    foo + 1
4  }
5}

不写表达式的情况下,正常读取值是能够正常解包的

js
1{
2  {
3    object.foo
4  }
5}

把 ref 写在 reactive 里的解包

js
1const count = ref(0)
2const state = reactive({
3  count,
4})
5
6console.log(state.count) // 0
7
8state.count = 1
9console.log(count.value) // 1

ref 放在 reactive 里被访问会更改时,自动解包。

将新的 ref 赋值给一个关联了已有 ref 的 property,会替换掉旧的 ref

js
1const otherCount = ref(2)
2
3state.count = otherCount
4console.log(state.count) // 2
5// 原始 ref 现在已经和 state.count 失去联系
6console.log(count.value) // 1

数组和集合类型的 ref 解包

当 ref 作为响应式数组或者 Map 这样的原生集合类型的元素被访问时,不会解包

js
1const books = reactive([ref('Vue 3 Guide')])
2// 这里需要 .value
3console.log(books[0].value)
4
5const map = reactive(new Map([['count', ref(0)]]))
6// 这里需要 .value
7console.log(map.get('count').value)

ref.value 语法糖

使用开发工具编译转换,可以在适当的位置自动添加.value来提高开发体验。

官方教程

计算属性

计算属性的用法

计算属性可以用来简化模板中的表达式逻辑,如果表达式中有非常复杂的逻辑,那么最终可能难以维护。

比如,现在有一个响应式数组里面的内容是这样的:

js
1const author = reactive({
2  name: 'John Doe',
3  books: ['Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery'],
4})

如果根据author在模板中展示一些信息,比如需要根据 books 的数量决定展示什么内容:

<p>Has published books:</p>

<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

可以这样简化:

html
1<script setup>
2  import { reactive, computed } from 'vue'
3
4  const author = reactive({
5    name: 'John Doe',
6    books: ['Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery'],
7  })
8
9  // 一个计算属性 ref
10  const publishedBooksMessage = computed(() => {
11    return author.books.length > 0 ? 'Yes' : 'No'
12  })
13</script>
14
15<template>
16  <p>Has published books:</p>
17  <span>{{ publishedBooksMessage }}</span>
18</template>

使用 computed来做属性计算,它会接受一个 getter 函数,返回一个计算属性ref。

计算属性会自动追踪依赖的更新,当author.book发生改变时,publishedBooksMessage会更新,然后所有跟它相关的组件会更新。

计算属性的好处

上面的代码逻辑实际上用一个函数也可以解决,比如这样:

js
1// 组件中
2function calculateBooksMessage() {
3  return author.books.length > 0 ? 'Yes' : 'No'
4}
js
1<p>{{ calculateBooksMessage() }}</p>

当 author.books发生改变时,下面的函数也会重新运行,在结果上看都是没有问题的。

不同之处在于计算属性值会基于其对应的响应式依赖被缓存起来,只有在响应式依赖改变时才会重新执行computed里面的getter函数。

而调用函数总是会在重新渲染发生时再次执行,这样有时候非常消耗性能。

缓存函数也是提升性能的一种手段。 vue

可写的计算属性

如果我们希望有些计算属性是可写的,那么就需要通过 getter 和 setter 来创建一个可写的计算属性。

js
1<script setup>
2import { ref, computed } from 'vue'
3
4const firstName = ref('John')
5const lastName = ref('Doe')
6
7const fullName = computed({
8  // getter
9  get() {
10    return firstName.value + ' ' + lastName.value
11  },
12  // setter
13  set(newValue) {
14    // 注意:我们这里使用的是解构赋值语法
15    [firstName.value, lastName.value] = newValue.split(' ')
16  }
17})
18</script>

当写计算属性时,需要根据逻辑反向设置其依赖,也就是说在 set内要给其依赖重新设置属性值。

上面的 set被调用后,重新设置了firstName和lastName

计算属性注意点

  • 计算属性中的计算函数应该只做计算,而不应该有副作用。副作用指的是在计算函数做异步请求或者操作 DOM。计算函数只应该计算和返回计算值。
  • 不能直接修改计算属性值。计算属性返回的值是派生状态,是根据源状态而创建的临时快照,更改它是没有意义的,应该更新它的源状态以触发重新计算。

绑定 Class 和 Style

使用v-bind 可以绑定Attribute,HTML 的 Attribute 都是字符串,但频繁返回字符串容易出错,因此 Vue 为 Class 和 Style设置了v-bind的功能增强,可以放字符串或对象或数组。

绑定 Class

绑定对象

html
1<div :class="{ active: isActive }"></div>

当 isActive是真值时,会添加上名叫active的类。

:class和 原生class属性会共存。

html
1<div class="static" :class="{ active: isActive }"></div>

上面的模板语法会被渲染成

html
1<div class="static active"></div>

传递给:class的需要是一个对象,所以也可以设置一个响应状态

js
1const classObject = reactive({
2  active: true,
3  'text-danger': false,
4})
html
1<div :class="classObject"></div>

最佳实践是返回一个对象的计算属性

js
1const isActive = ref(true)
2const error = ref(null)
3
4const classObject = computed(() => ({
5  active: isActive.value && !error.value,
6  'text-danger': error.value && error.value.type === 'fatal',
7}))

绑定数组

使用数组来给模板添加 class

js
1const activeClass = ref('active')
2const errorClass = ref('text-danger')
js
1<div :class="[activeClass, errorClass]"></div>

数组中使用三元表达式

js
1<div :class="[isActive ? activeClass : '', errorClass]"></div>

errorClass 会一直存在,但 activeClass 只会在 isActive 为真时才存在。

数组中也能够使用对象语法

js
1<div :class="[{ active: isActive }, errorClass]"></div>

class 透传

当子组件只有一个根元素,那么在父组件上传递的 class会添加到子组件的根元素上,与该元素已有的 class 合并。

举例:

html
1<!-- 子组件模板 -->
2<p class="foo bar">Hi!</p>

父组件上使用并传递了class

html
1<!-- 在使用组件时 -->
2<my-component class="baz boo"></my-component>

最后子组件渲染结果为:

html
1<p class="foo bar baz boo">Hi</p>

使用:class绑定也是一样的。

html
1<my-component :class="{ active: isActive }"></my-component>

当 isActive 为真时,被渲染的 HTML 会是:

html
1<p class="foo bar active">Hi</p>

如果子组件有多个根元素,那么需要用$attrs来指定哪个根元素会得到父元素传下去的 class 属性。

html
1<!-- my-component 模板使用 $attrs 时 -->
2<p :class="$attrs.class">Hi!</p>
3<span>This is a child component</span>

绑定 Style

绑定对象

使用:style来绑定内联样式,可以传递对象。

js
1const activeColor = ref('red')
2const fontSize = ref(30)
html
1<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

上面是小驼峰的方式来写样式属性,也可以用原生的kebab-cased形式来写

html
1<div :style="{ 'font-size': fontSize + 'px' }"></div>

推荐使用小驼峰。

也可以直接绑定一个样式对象

js
1const styleObject = reactive({
2  color: 'red',
3  fontSize: '13px',
4})
html
1<div :style="styleObject"></div>

如果很复杂的话,也推荐使用计算属性。

绑定数组

我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并和应用到同一元素上

html
1<div :style="[baseStyles, overridingStyles]"></div>

样式多值

可以对一个样式属性添加上多个不同前缀的值,举例:

html
1<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

数组仅会渲染浏览器支持的最后一个值。也就是说如果浏览器支持flex,那么只会渲染flex

条件渲染

条件渲染需要用到指令:

  • v-if:当指令上的表达式为真时,才会渲染一个模板

    html
    1<h1 v-if="awesome">Vue is awesome!</h1>
  • v-if:可以跟 v-if互相配合使用

html
1<button @click="awesome = !awesome">Toggle</button>
2
3<h1 v-if="awesome">Vue is awesome!</h1>
4<h1 v-else>Oh no 😢</h1>
  • v-else-if:跟上面的也可以一起使用

    html
    1<div v-if="type === 'A'">A</div>
    2<div v-else-if="type === 'B'">B</div>
    3<div v-else-if="type === 'C'">C</div>
    4<div v-else>Not A/B/C</div>
  • template 上的 v-if、v-else、v-else-if

    这里的template不是 Vue 的模板语法上的template,而是指 HTML 的<template>标签,可以在上面添加条件渲染。这只是一个不可见的包装器元素,浏览器不会将<template>渲染在上面。

    html
    1<template>
    2  <!-- 下面的是 HTML 标签的 <template> -->
    3  <template v-if="isActive">
    4    <div>{{isActive}}</div>
    5    <button @click="setIsActive">设置 isActive</button>
    6  </template>
    7</template>

    上面的template标签在条件渲染为真时不会渲染到浏览器上。

  • v-show:跟v-if不同的是,v-show是切换display的 CSS 属性,DOM 渲染时,始终保留该元素,而v-if是直接从 DOM 中移除,组件会直接销毁或重建。

    不能在template上面使用v-show

  • v-if和v-for:不推荐同时使用v-for和v-if,因为这样二者的优先级不明显。当同时存在于一个元素时,优先级是v-if更高。

列表渲染

v-for

v-for可以用来渲染列表。v-for需要特殊的语法item in items,其中items是源数据的数组,item是迭代项别名:

js
1const _items = ref([{ message: 'Foo' }, { message: 'Bar' }])
html
1<ul>
2  <li v-for="item in _items" :key="item.message">{{ item.message}}</li>
3</ul>

v-for中拿到index

html
1<ul>
2  <li v-for="(item,index) in _items" :key="index">{{ item.message}}</li>
3</ul>

v-for中解构赋值

html
1<!-- 有 index 索引时 -->
2<li v-for="({ message }, index) in items">{{ message }} {{ index }}</li>

推荐使用of替代in,因为这跟 ES6 的迭代器很像

html
1<div v-for="item of items"></div>

v-for 迭代对象

js
1const myObject = reactive({
2  title: 'How to do lists in Vue',
3  author: 'Jane Doe',
4  publishedAt: '2016-04-10',
5})
html
1<li v-for="(value, key, index) in myObject">{{ index }}. {{ key }}: {{ value }}</li>

Vue 内部会调用Object.keys得到枚举顺序,所以顺序可能不一致。

v-for 使用值范围

可以直接传给 v-for 一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。

html
1<span v-for="n in 10">{{ n }}</span>

注意此处 n 的初值是从 1 开始而非 0。

在 template 上用 v-for

html
1<ul>
2  <template v-for="item in items">
3    <li>{{ item.msg }}</li>
4    <li class="divider" role="presentation"></li>
5  </template>
6</ul>

v-if 和 v-for

不推荐两者一起使用,由于v-if 优先级更高,所以v-if的条件无法访问到v-for作用域内定义的变量别名

html
1<!--
2 这会抛出一个错误,因为属性 todo 此时
3 没有在该实例上定义
4-->
5<li v-for="todo in todos" v-if="!todo.isComplete">{{ todo.name }}</li>

在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

html
1<template v-for="todo in todos">
2  <li v-if="!todo.isComplete">{{ todo.name }}</li>
3</template>

key

在虚拟 DOM 比较时,需要传入一个 key来给列表渲染的每个节点打标记,这样有利于跟踪元素的变化。默认情况下 Vue使用就地更新的策略。

html
1<div v-for="item in items" :key="item.id">
2  <!-- 内容 -->
3</div>

推荐在任何使用v-for的地方绑定一个key。key需要使用基础类型的值,不要用复杂类型。

组件使用 v-for

可以直接在组件上使用 v-for

html
1<my-component v-for="item in items" :key="item.id"></my-component>

如果要给组件传递数据,那么需要才采用 props 的方式:

html
1<my-component v-for="(item, index) in items" :item="item" :index="index" :key="item.id"></my-component>

数组变化侦测

vue 包装以下侦听数组的变更方法,当使用以下方法时会触发视图更新。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

有一些没被包装过的数组方法会返回新的数组,比如filter、concat、slice等等,这些都不会更改源数组,而是返回一个新的数组。那么如果需要使用这些时,需要将旧的数组换成新的

json
1// _items 是 ref 对象
2(_items.value = _items.value.filter(item => item.message !== "Bar"))

计算属性做数组过滤

用计算属性来完成数组过滤或排序等操作,主要是用计算属性新定义一个派生状态,这样做的好处是可以不改变原数组

js
1const numbers = ref([1, 2, 3, 4, 5])
2
3const evenNumbers = computed(() => {
4  return numbers.value.filter((n) => n % 2 === 0)
5})
html
1<li v-for="n in evenNumbers">{{ n }}</li>

多重嵌套的 v-for中可以定义一个方法来替代计算属性。

js
1const sets = ref([
2  [1, 2, 3, 4, 5],
3  [6, 7, 8, 9, 10],
4])
5
6function even(numbers) {
7  return numbers.filter((number) => number % 2 === 0)
8}
html
1<ul v-for="numbers in sets">
2  <li v-for="n in even(numbers)">{{ n }}</li>
3</ul>

计算属性中不要使用reverse和sort,因为这两个方法会改变原始数组。

计算属性的目的在于生成一个新的派生状态,而不是修改原始状态。如果在计算属性中的操作会改变原始数组,那么要创建一个原数组的副本,再返回出去。

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

事件处理

v-on或者@能够监听 DOM 事件,写法是v-on:click="methodName" 和 @click="handler"。

事件处理器的值可以有两种:

  1. 内联事件处理器
  2. 方法事件处理器

内联这样写:

html
1<button @click="count++">Add 1</button>

方法这样写

html
1<!-- `greet` 是定义过的方法名 -->
2<button @click="greet">Greet</button>

方法作为事件处理器会自动接收原生 DOM 事件并触发执行,我们可以拿到event对象

js
1const name = ref('Vue.js')
2
3function greet(event) {
4  alert(`Hello ${name.value}!`)
5  // `event` 是 DOM 原生事件
6  if (event) {
7    alert(event.target.tagName)
8  }
9}

内联处理器中调用方法

js
1function say(message) {
2  alert(message)
3}
html
1<button @click="say('hello')">Say hello</button> <button @click="say('bye')">Say bye</button>

模板编译器会自动判断say()或者count++为内联处理器。

内联处理器中访问事件

html
1<!-- 使用特殊的 $event 变量 -->
2<button @click="warn('Form cannot be submitted yet.', $event)">Submit</button>
3
4<!-- 使用内联箭头函数 -->
5<button @click="(event) => warn('Form cannot be submitted yet.', event)">Submit</button>

建议统一使用内联箭头函数和方法事件处理器来处理事件。

事件修饰符

事件修饰符可以帮我们在处理事件时加上某些额外的行为,比如调用event.preventDefault或者event.stopPropagation阻止事件继续传递。

事件修饰符相当于 addEventListener 的某些语法糖

  • .stop:阻止事件继续传递

  • .prevent:阻止默认行为

  • .self:仅当 event.target 是元素本身时才会触发事件处理器

  • .capture:用捕获的方式处理事件程序

  • .once :只调用一次事件后删除

  • .passive:承诺不会阻止默认行为,移动设备的滚动事件会滚屏,但可以用event.preventDefault阻止滚动,浏览器在滚动前会检查事件处理程序里面是不是有设置阻止默认行为,这个过程就会造成卡顿。使用.passive则是表示不会阻止默认行为,浏览器就可以放心滚动。

    html
    1<!-- 单击事件将停止传递 -->
    2<a @click.stop="doThis"></a>
    3
    4<!-- 提交事件将不再重新加载页面 -->
    5<form @submit.prevent="onSubmit"></form>
    6
    7<!-- 修饰语可以使用链式书写 -->
    8<a @click.stop.prevent="doThat"></a>
    9
    10<!-- 也可以只有修饰符 -->
    11<form @submit.prevent></form>
    12
    13<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
    14<!-- 例如:事件处理器不来自子元素 -->
    15<div @click.self="doThat">...</div>
    16
    17<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
    18<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
    19<div @click.capture="doThis">...</div>
    20
    21<!-- 点击事件最多被触发一次 -->
    22<a @click.once="doThis"></a>
    23
    24<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
    25<!-- 以防其中包含 `event.preventDefault()` -->
    26<div @scroll.passive="onScroll">...</div>

使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用 @click.prevent.self 会阻止元素内的所有点击事件而 @click.self.prevent 则只会阻止对元素本身的点击事件。

请勿同时使用 .passive 和 .prevent。

按键修饰符

监听键盘事件时,经常需要判断用户按下的按键,Vue也给了语法糖,通过监听按键事件添加按键修饰符可以简化这个过程。

html
1<!-- 仅在 `key` 为 `Enter` 时调用 `vm.submit()` -->
2<input @keyup.enter="submit" />

也可以用原生KeyboardEvent.key给的按键名称作为修饰符,一定需要用 kebab-case 形式。

html
1<input @keyup.page-down="onPageDown" />

上面的意思是当$event.key为PageDown时调用事件。

按键别名:

  • .enter
  • .tab
  • .delete (捕获“Delete”和“Backspace”两个按键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

系统按键修饰符

  • .ctrl
  • .alt
  • .shift
  • .meta

其中 meta键在mac电脑上是 Command,window 电脑上是 win 键。

html
1<!-- Alt + Enter -->
2<input @keyup.alt.enter="clear" />
3
4<!-- Ctrl + 点击 -->
5<div @click.ctrl="doSomething">Do something</div>

请注意,系统按键修饰符和常规按键不同。与 keyup 事件一起使用时,该按键必须在事件发出时处于按下状态。换句话说,keyup.ctrl 只会在你仍然按住 ctrl 但松开了另一个键时被触发。若你单独松开 ctrl 键将不会触发。

.exact 修饰符

.exact 修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。

html
1<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
2<button @click.ctrl="onClick">A</button>
3
4<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
5<button @click.ctrl.exact="onCtrlClick">A</button>
6
7<!-- 仅当没有按下任何系统按键时触发 -->
8<button @click.exact="onClick">A</button>

鼠标按键修饰符

  • .left
  • .right
  • .middle

表单输入绑定

Vue 中的表单输入绑定是这样的

typescript
1let text = ref<string>('')
2
3const handleInput = (e: Event) => {
4  text.value = (e.target as HTMLInputElement).value
5}
html
1<input type="text" :value="text" @input="handleInput" />

上面的代码这样写略麻烦,所以 Vue 给了语法糖

html
1<input v-model="text" />

对于v-model还可以用于<textarea>,<select>等元素。它会根据所使用的的元素自动扩展到不同的 DOM 属性和事件的组合:

  1. <input>和<textarea>就使用 value 属性和 input 事件
  2. <input type="checkbox">和<input type="radio">就是使用checked属性和change事件
  3. <select>就是用 value,change 为事件

如果绑定了 v-model,那么使用初始的value或者checked属性就会被忽略。

用v-model时,如果用输入法打中文,会发现没有按下空格键是不会触发状态更新的,这点跟 React 不同,只要绑定好受控状态,React 会马上更新。

值绑定

单选、复选、选择器选项,v-model绑定的一般是静态字符串,或者复选框也可以绑定布尔值。

html
1<!-- `picked` 在被选择时是字符串 "a" -->
2<input type="radio" v-model="picked" value="a" />
3
4<!-- `toggle` 只会为 true 或 false -->
5<input type="checkbox" v-model="toggle" />
6
7<!-- `selected` 在第一项被选中时为字符串 "abc" -->
8<select v-model="selected">
9  <option value="abc">ABC</option>
10</select>

使用v-bind能够让我们将选项绑定为非字符串类型

复选框

true-value 和 false-value 是 Vue 特有的 attributes 且仅会在 v-model 存在时工作。这里 toggle 属性的值会在选中时被设为 'yes',取消选择时设为 'no'。

html
1<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />

也可以用v-bind绑定到其他动态值上面。

html
1<input type="checkbox" v-model="toggle" :true-value="dynamicTrueValue" :false-value="dynamicFalseValue" />

单选按钮

html
1<input type="radio" v-model="pick" :value="first" /> <input type="radio" v-model="pick" :value="second" />

pick 会在第一个按钮选中时被设为 first,在第二个按钮选中时被设为 second。

选择器选项

html
1<select v-model="selected">
2  <!-- 内联对象字面量 -->
3  <option :value="{ number: 123 }">123</option>
4</select>

v-model 同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected 会被设为该对象字面量值 { number: 123 }。

修饰符

.lazy

添加last修饰符可以让 input 在 change事件后更新状态

html
1<!-- 在 "change" 事件后同步更新而不是 "input" -->
2<input v-model.lazy="msg" />

.number

添加.number可以让用户输入自动转换为数字

html
1<input v-model.number="age" />

这个用法会内部调用parseFloat,如果输入的值没办法被parseFloat处理的话会返回原值

.trim

自动去除用户输入内容中两端的空格,则可以使用.trim修饰符

html
1<input v-model.trim="msg" />

生命周期

生命周期钩子就是一系列的回调函数,在 vue 组件实例初始化的过程中 vue 会调用这些钩子,这样开发者就可以在里面写代码,让 vue 在特定的阶段调用它。

这些阶段可以分为:挂载实例到 DOM 上、数据侦听时、编译模板时、数据改变时等。

常见生命周期

  • onMounted:组件完成初始渲染并创建 DOM 节点后运行
  • onUpdated:状态更新导致 DOM 更新之后调用
  • onUnmounted:组件被卸载之后调用

#官方图例

API 说明

侦听器

computed允许我们声明一个派生状态,有些情况下,我们并不需要它。

Vue用watch函数来监听状态改变,然后触发回调函数。

html
1<script setup lang="ts">
2  import { ref, watch } from 'vue'
3
4  let input = ref<string>('')
5  // 直接侦听某个 ref
6  watch(input, async (newInput, oldInput) => {
7    console.log(newInput)
8    console.log(oldInput)
9  })
10</script>
11
12<template>
13  <input v-model="input" />
14  <div>{{ input }}</div>
15</template>

侦听来源

watch的第一个参数可以有很多种:ref(以及它的computed状态)、一个响应式对象、一个getter函数、多个来源组成的数组:

js
1const x = ref(0)
2const y = ref(0)
3
4// 单个 ref
5watch(x, (newX) => {
6  console.log(`x is ${newX}`)
7})
8
9// getter 函数
10watch(
11  () => x.value + y.value,
12  (sum) => {
13    console.log(`sum of x + y is: ${sum}`)
14  },
15)
16
17// 多个来源组成的数组
18watch([x, () => y.value], ([newX, newY]) => {
19  console.log(`x is ${newX} and y is ${newY}`)
20})

不能侦听响应式对象的property,比如:

js
1const obj = reactive({ count: 0 })
2
3// 这不起作用,因为你是向 watch() 传入了一个 number
4watch(obj.count, (count) => {
5  console.log(`count is: ${count}`)
6})

而是用getter侦听:

js
1// 提供一个 getter 函数
2watch(
3  () => obj.count,
4  (count) => {
5    console.log(`count is: ${count}`)
6  },
7)

deep 选项

直接给watch传入一个响应式对象,会隐式创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发。

js
1import { reactive, ref, watch } from 'vue'
2
3let _ref = ref({ count: 0 })
4const obj = reactive({ count: 0 })
5
6watch(_ref.value, (newInput, oldInput) => {
7  // 嵌套的 property 变更时触发
8  // 两个对象相等,因为是一个引用
9  console.log(newInput === oldInput) // true
10})
11
12watch(obj, (newInput, oldInput) => {
13  console.log(newInput === oldInput) // true
14})
15_ref.value.count++
16obj.count++

跟返回响应式对象的getter不同,如果是getter函数,那么只有在返回的对象变了才会触发回调:

js
1let _ref = ref({ count: 0, name: '123' })
2// 只有 ref.value 整个变了才会触发,也就是引用地址改变才会触发
3watch(
4  () => _ref.value,
5  (newInput) => {
6    console.log(newInput)
7  },
8)
9_ref.value.count++

但是如果想要上面的函数能起作用,可以添加deep选项,这样就可以创建深层侦听器。

js
1watch(
2  () => _ref.value,
3  (newInput) => {
4    console.log(newInput)
5  },
6  { deep: true },
7)

使用deep选项会遍历侦听对象的所有嵌套属性,如果数据量很大,那么开销也很大。

watchEffect

当侦听源发生变化,那么watch会执行回调函数。有时我们希望在创建侦听器的时候立即执行一遍回调。

比如,下面的例子是一个 url,每次 url 变化了,都需要动态改变某个 data。但我们希望url 第一次创建时也调用一次函数给ref一个初始值,如果仅仅用 watch写,需要这样写:

js
1const url = ref('https://...')
2const data = ref(null)
3
4async function fetchData() {
5  const response = await fetch(url.value)
6  data.value = await response.json()
7}
8
9// 立即获取
10fetchData()
11// ...再侦听 url 变化
12watch(url, fetchData)

上面的代码可以用watchEffect函数简化。它会立即调用一遍回调函数,如果这时函数产生副作用,Vue 会自动追踪副作用的依赖关系,自动分析出响应源。

js
1watchEffect(async () => {
2  const response = await fetch(url.value)
3  data.value = await response.json()
4})

这个例子中,回调会立即执行。在执行期间,它会自动追踪 url.value 作为依赖(近似于计算属性)。每当 url.value 变化时,回调会再次执行。

watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的 property 才会被追踪。

watch 和 watchEffect

watch和watchEffect都能响应式执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的源。它不会追踪任何在回调中访问到的东西。另外,仅在响应源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式 property。这更方便,而且代码往往更简洁,但其响应性依赖关系不那么明确。

侦听器回调顺序

当更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。

默认情况下,侦听器回调会在 Vue 组件更新前被调用,这就代表侦听器回调中如果访问 DOM 则是被 Vue 更新前的状态。

如果希望在侦听器回调中能访问被 Vue 更新后的 DOM,需要flush:'post'选项:

js
1watch(source, callback, {
2  flush: 'post',
3})
4
5watchEffect(callback, {
6  flush: 'post',
7})

后置刷新的 watchEffect() 有个更方便的别名 watchPostEffect():

js
1import { watchPostEffect } from 'vue'
2
3watchPostEffect(() => {
4  /* 在 Vue 更新后执行 */
5})

停止侦听器

使用同步语句创建的侦听器,会自动绑定到组件宿主组件实例上,并且会在宿主组件卸载时自动停止。

侦听器在异步创建时,不会绑定到当前组件上,我们必须手动停止它,以防内存泄露。

js
1<script setup>
2  import {watchEffect} from 'vue' // 它会自动停止 watchEffect(() => {}) // ...这个则不会! setTimeout(() =>{' '}
3  {watchEffect(() => {})}, 100)
4</script>

要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:

js
1const unwatch = watchEffect(() => {})
2
3// ...当该侦听器不再需要时
4unwatch()

,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:

js
1// 需要异步请求得到的数据
2const data = ref(null)
3
4watchEffect(() => {
5  if (data.value) {
6    // 数据加载后执行某些操作...
7  }
8})

模板 ref

如果需要直接访问底层 DOM 元素,需要使用refattribute:

html
1<input ref="input" />

ref是一个特殊的attribute,我们可以用它获取一个 DOM 元素或子组件被挂载后的直接引用。

访问模板 ref

访问模板 ref,需要声明一个同名的 ref:

html
1<script setup>
2  import { ref, onMounted } from 'vue'
3
4  // 声明一个 ref 来存放该元素的引用
5  // 必须和模板 ref 同名
6  const input = ref(null)
7
8  onMounted(() => {
9    input.value.focus()
10  })
11</script>
12
13<template>
14  <input ref="input" />
15</template>

只有在组件被挂载后才能访问 ref。

如果要用侦听器观察一个模板ref的变化情况,需要考虑到ref 的值可能为 null:

js
1watchEffect(() => {
2  if (input.value) {
3    input.value.focus()
4  } else {
5    // 此时还未挂载,或此元素已经被卸载(例如通过 v-if 控制)
6  }
7})

模板 ref 标注类型

模板 ref 需要通过一个显式指定的泛型参数和一个初始值 null 来创建:

html
1<script setup lang="ts">
2  import { ref, onMounted } from 'vue'
3
4  const el = ref<HTMLInputElement | null>(null)
5
6  onMounted(() => {
7    el.value?.focus()
8  })
9</script>
10
11<template>
12  <input ref="el" />
13</template>

注意为了严格的类型安全,有必要在访问 el.value 时使用可选链或类型守卫。这是因为直到组件被挂载前,这个 ref 的值都是初始的 null,并且在由于 v-if 的行为将引用的元素卸载时也可以被设置为 null。

v-for 中的 ref

当 ref 在 v-for 中使用时,相应的 ref 中包含的值是一个数组,它将在元素被挂载后填充:

html
1<script setup>
2  import { ref, onMounted } from 'vue'
3
4  const list = ref([
5    /* ... */
6  ])
7
8  const itemRefs = ref([])
9
10  onMounted(() => console.log(itemRefs.value))
11</script>
12
13<template>
14  <ul>
15    <li v-for="item in list" ref="itemRefs">{{ item }}</li>
16  </ul>
17</template>

ref 数组不能保证与源数组相同的顺序。

函数型 ref

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。函数接受该元素引用作为第一个参数:

html
1<input :ref="(el) => { /* 将 el 分配给 property 或 ref */ }" />

如果你正在使用一个动态的 :ref 绑定,我们也可以传一个函数。当元素卸载时,这个 el 参数会是 null。你当然也可以使用一个方法而不是内联函数。

组件上的 ref

ref 也可以被用在一个子组件上。此时 ref 中引用的是组件实例

html
1<script setup>
2  import { ref, onMounted } from 'vue'
3  import Child from './Child.vue'
4
5  const child = ref(null)
6
7  onMounted(() => {
8    // child.value 是 <Child /> 组件的实例
9  })
10</script>
11
12<template>
13  <Child ref="child" />
14</template>

如果一个子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。

有一个例外的情况,使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

html
1<script setup>
2  import { ref } from 'vue'
3
4  const a = 1
5  const b = ref(2)
6
7  defineExpose({
8    a,
9    b,
10  })
11</script>

当父组件通过模板 ref 获取到了该组件的实例时,得到的实例类型为 { a: number, b: number } (ref 都会自动解包,和一般的实例一样)。

组件 ref 定义类型

html
1<!-- child.vue -->
2<script setup lang="ts">
3  import { ref } from 'vue'
4
5  const a = 1
6  const b = ref(2)
7  defineExpose({
8    a,
9    b,
10  })
11</script>
html
1<!-- parent.vue -->
2<script setup lang="ts">
3  import Child from './child.vue'
4  import { onMounted, ref } from 'vue'
5
6  let _ref = ref<InstanceType<typeof Child> | null>(null)
7  onMounted(() => {
8    console.log(_ref.value?.a)
9    console.log(_ref.value?.b)
10  })
11</script>
12
13<template>
14  <Child ref="_ref" />
15</template>

组件基础

定义组件

定义一个 SFC(单文件组件)

html
1<script setup>
2  import { ref } from 'vue'
3
4  const count = ref(0)
5</script>
6
7<template>
8  <button @click="count++">You clicked me {{ count }} times.</button>
9</template>

不适用 SFC 时,导出一个 JS 对象来定义:

js
1import { ref } from 'vue'
2
3export default {
4  setup() {
5    const count = ref(0)
6    return { count }
7  },
8  template: `
9    <button @click="count++">
10      You clicked me {{ count }} times.
11    </button>`
12  #// 或者 `template: '#my-template-element'`
13}

这里的模板是一个内联的 JS 字符串,Vue 会编译它。

上面的所有方式都会默认导出它自己。

使用组件

要使用一个子组件,我们需要在父组件中导入它。

html
1<script setup>
2  import ButtonCounter from './ButtonCounter.vue'
3</script>
4
5<template>
6  <h1>Here is a child component!</h1>
7  <ButtonCounter />
8</template>

通过 <script setup>,导入的组件都在模板中直接可用。

每当你使用一个组件,就创建了一个新的实例。每一个组件都维护着自己的状态。

传递 props

props 是一种特殊的attributes,父组件可以传递属性给子组件。

html
1<script setup lang="ts">
2  import { ref } from 'vue'
3  import Child from './child.vue'
4  const count = ref(0)
5  function addCount() {
6    count.value++
7  }
8</script>
9
10<template>
11  <Child :count="count" :addCount="addCount" />
12</template>

子组件使用则需要使用defineProps声明

html
1<script setup lang="ts">
2  defineProps(['count', 'addCount'])
3</script>
4
5<template>
6  <h1>{{ count }}</h1>
7  <h1>count:{{ count }}</h1>
8  <button @click="addCount">add</button>
9</template>

props 和v-for结合

html
1<script setup lang="ts">
2  import { ref } from 'vue'
3  import Child from './child.vue'
4  const count = ref([
5    { id: 1, title: 'book1' },
6    { id: 2, title: 'book2' },
7    { id: 3, title: 'book3' },
8  ])
9</script>
10
11<template>
12  <Child v-for="{ title, id } of count" :title="title" :key="id" />
13</template>
html
1<!-- child.vue -->
2<script setup lang="ts">
3  defineProps(['title'])
4</script>
5
6<template>
7  <h1>{{ title }}</h1>
8</template>

监听事件

除了直接将事件函数传递给子组件使用以外,还可以通过让父组件监听子组件事件,让子组件通过触发父组件里的方法。

具体做法:

  1. 父组件在子组件上设置监听某个事件

    js
    1const addCount = () => count.value.push({ id: 4, title: 'book4' })
    html
    1<Child v-for="{ title, id } of count" :title="title" :key="id" @addCount="addCount" />
  1. 子组件声明需要抛出的事件

    js
    1defineEmits(['addCount'])
  1. 通过内置的$emit抛出事件

    html
    1<button @click="$emit('addCount')">addCount</button>
  2. 由于父组件事先监听addCount,所以当子组件抛出后,父组件会捕获到这一事件并最终调用该方法。

插槽

插槽的概念类似于 React 的 children,Vue里用的是<slot>

父组件里这么用:

html
1<Child>
2  <div>this is slot</div>
3</Child>

子组件中把<slot>插到想要插入的位置

html
1<template>
2  <div class="slot">
3    <slot />
4  </div>
5</template>

组件切换

有些需求需要在多个组件间切换,可以用<component>元素和isattribute 实现。

案例:

两个子组件内容:

html
1<script setup>
2  const title = '组件 1'
3</script>
4<template>
5  <h1 class="slot">{{ title }}</h1>
6</template>
html
1<script setup>
2  const title = '组件 2'
3</script>
4<template>
5  <h1 class="slot">{{ title }}</h1>
6</template>

父组件里这样用:

html
1<script setup lang="ts">
2  import { ref } from 'vue'
3  import Child1 from './child1.vue'
4  import Child2 from './child2.vue'
5  let currentTab = ref('Child1')
6  const tabs = { Child1, Child2 }
7  function checkCurrentTab() {
8    if (currentTab.value === 'Child1') {
9      currentTab.value = 'Child2'
10    } else {
11      currentTab.value = 'Child1'
12    }
13  }
14</script>
15
16<template>
17  <div>
18    <component :is="tabs[currentTab]"></component>
19    <button @click="checkCurrentTab">check tab</button>
20  </div>
21</template>

被传给 :is 的值可以是以下几种:

  • 被注册的组件名
  • 导入的组件对象

也可以使用 is attribute 来创建一般的 HTML 元素。

当使用<component :is='...'来给多个组件切换时,组件会在被切换后卸载。我们可以换成<KeepAlive>组件来强制让不活跃的组件保持存活状态。

html
1<!-- 非活跃的组件将会被缓存! -->
2<KeepAlive>
3  <component :is="activeComponent" />
4</KeepAlive>

组件注册

全局注册

app.component()方法让组件全局可用。

在main文件中使用app.component来全局注册组件

js
1import { createApp } from 'vue'
2import App from './App.vue'
3import ChildA from './child1.vue'
4import ChildB from './child2.vue'
5
6const app = createApp(App)
7app.component('ChildA', ChildA).component('ChildB', ChildB)

在其他地方就可以直接用:

<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>

全局注册的组件即使没用也不会被tree-shaking删除,同时由于全局注册的组件不需要引入也可以使用,这就使得依赖关系不是很明确。

局部注册

局部注册就是通过import来引入子组件。

html
1<script setup>
2  import ComponentA from './ComponentA.vue'
3</script>
4
5<template>
6  <ComponentA />
7</template>

Props

子组件需要显式声明 prop,除了使用字符串数组外,还可以使用对象的形式

js
1defineProps({ title: String })
2defineProps(['title'])

对象形式声明中的每个属性,key 是 prop 的名称,而值应该是预期类型的构造函数。

使用 TypeScript 的类型标注:

js
1const props = defineProps<{ title: string }>();
2
3{{ props.title }}

传递 prop 细节

  • props 属性名应使用小驼峰形式

  • 动态 prop 使用v-bind或者:绑定

  • 使用动态 prop 传递字符串时需要带引号,否则会被认为是 number 类型

  • 传递 true 时可以简写 <BlogPost is-published />

  • 可以用一个对象来传递多个 prop

    html
    1<Child1 v-bind="{ title, name }" />

    等价于:

    html
    1<Child1 :title="title" :name="name" />

单项数据

所有的 prop 都遵循着单向绑定原则,prop 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改了父组件的状态,不然应用的数据流就会变得难以理解了。

子组件不能更改 prop,因为 prop 是只读的。(但是可以更改引用类型内部的值)

如果子组件想要更改 prop,可以这么做:

  1. prop 用于被传入初始值,子组件定义一个新的局部属性,将 prop 当做初始值

    js
    1const props = defineProps(['initialCounter'])
    2
    3// 计数器只是将 props.initialCounter 作为初始值
    4// 像下面这样做就使 prop 和后续更新无关了
    5const counter = ref(props.initialCounter)
  2. 子组件使用计算属性,而依赖则使用父组件传递下来的 prop

    js
    1const props = defineProps(['size'])
    2
    3// 该 prop 变更时计算属性也会自动更新
    4const normalizedSize = computed(() => props.size.trim().toLowerCase())

由于 prop 可以传递引用类型,所以其实可以更改引用类型内部的值,但是这样会使得数据变得难以推理,所以不要这么做,如果真的想改,则应该调用事件通知父组件更改。

组件事件

$emit触发组件自定义事件

html
1<!-- MyComponent -->
2<button @click="$emit('someEvent')">click me</button>

父组件监听

html
1<MyComponent @some-event="callback" />

自定义事件支持修饰符

html
1<MyComponent @some-event.once="callback" />

Vue 会自动将小驼峰方式转化成kebab-case,所以上面的示例中子组件使用someEvent触发,父组件则使用@some-event来监听。

组件事件不会冒泡

事件参数

子组件通过$emit传递参数

html
1<button @click="$emit('increaseBy', 1)">Increase by 1</button>

父组件接收

html
1<MyButton @increase-by="(n) => count += n" />

所有传入 $emit() 的额外参数都会被直接传向监听器。举个例子,$emit('foo', 1, 2, 3) 触发后,监听器函数将会收到这三个参数值。

声明触发的事件

子组件处声明:

js
1const emit = defineEmits(['inFocus', 'submit'])

使用TypeScript来标注类型

js
1<script setup lang="ts">
2const emit = defineEmits<{
3  (e: 'change', id: number): void
4  (e: 'update', value: string): void
5}>()
6</script>

如果一个原生事件的名字 (例如 click) 被定义在 emits 选项中,则监听器只会监听组件触发的 click 事件而不会再响应原生的 click 事件。

配合 v-model

子组件写法:

html
1<script setup lang="ts">
2  defineProps<{ modelValue: string }>()
3  defineEmits(['update:modelValue'])
4</script>
5<template>
6  <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target?.value)" />
7</template>

父组件写法:

html
1<script setup lang="ts">
2  import { ref } from 'vue'
3  import Child1 from './child1.vue'
4  const modelValue = ref<string>('')
5</script>
6
7<template>
8  <div>
9    <Child1 v-model="modelValue" />
10    modelValue: {{ modelValue }}
11  </div>
12</template>

父组件的这行代码:

html
1<Child1 v-model="modelValue" />

相当于

html
1<Child1 :modelValue="modelValue" @update:modelValue="(value:string) => (modelValue = value)" />

v-model 传参

默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,以 update:modelValue 作为对应的事件。我们可以通过给 v-model 指定一个参数来更改这些名字:

html
1<MyComponent v-model:title="bookTitle" />

在这个例子中,子组件应该有一个 title prop,并在变更时向父组件发射 update:title 事件

html
1<!-- MyComponent.vue -->
2<script setup>
3  defineProps(['title'])
4  defineEmits(['update:title'])
5</script>
6
7<template>
8  <input type="text" :value="title" @input="$emit('update:title', $event.target.value)" />
9</template>

多个 v-model

使用 v-model 参数配合来传递多个 v-model

html
1<UserName v-model:first-name="firstName" v-model:last-name="lastName" />

处理 v-model 修饰符

默认的修饰符通过modelModifiers这个 props属性传递给子组件。

比如以下代码:

html
1<script setup lang="ts">
2  const props = defineProps(['modelValue', 'modelModifiers'])
3
4  const emit = defineEmits(['update:modelValue'])
5
6  function input(e: Event) {
7    let value = e?.target?.value as string
8    if (props.modelModifiers.capitalize) {
9      value = value[0].toUpperCase() + value.slice(1)
10    }
11    emit('update:modelValue', value)
12  }
13</script>
14<template>
15  <input type="text" :value="modelValue" @input="input" />
16</template>

上面的代码是子组件通过modelModifiers里面有没有capitalize来将首字母大写。

capitalize是父组件传递过来的修饰符。

父组件是这样传递修饰符的

html
1<Child1 v-model.capitalize="value" />

对于又有参数又有修饰符的 v-model 绑定,生成的 prop 名是 arg + "Modifiers"。举个例子:

父组件里传递:

html
1<Child1 v-model:value.capitalize="value" />

那么子组件里就应该是这样的:

js
1const props = defineProps(['value', 'valueModifiers'])
2
3const emit = defineEmits(['update:value'])
4console.log(props.titleModifiers) // { capitalize: true }

透传 Attribute

Attribute 继承

“透传 attribute”是传递给组件的 attribute 或者 v-on 事件监听器,但并没有显式地声明在所接收组件的 props 或 emits 上。最常见的例子就是 class、style 和 id。

举个例子,一个子组件的模板是这样写的:

html
1<!-- <MyButton> 的模板 -->
2<button>click me</button>

父组件使用了这个组件:

html
1<MyButton class="large" />

最后渲染出的 DOM 结果是:

html
1<button class="large">click me</button>

如果子组件的根元素有了 class或者style,就会跟父组件上继承过来的值互相合并。

html
1<!-- <MyButton> 的模板 -->
2<button class="btn">click me</button>

最后渲染出来的是这样:

html
1<button class="btn large">click me</button>

v-on监听器继承

同样的规则也适用于 v-on 事件监听器:

html
1<MyButton @click="onClick" />

监听器 click 会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。如果原生 button 元素已经通过 v-on 绑定了一个事件监听器,则这些监听器都会被触发。

深层组件继承

如果一个组件在根节点上渲染另一个组件,即这样的:

html
1<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
2<BaseButton />

此时 <MyButton> 接收的透传 attribute 会直接传向 <BaseButton>。

  • 透传的 attribute 不会包含 <MyButton> 上声明过的 props 或是针对 emits 声明事件的 v-on 侦听函数,换句话说,声明过的 props 和侦听函数被 <MyButton>“消费”了。
  • 透传的 attribute 若符合声明,也可以作为 props 传入 <BaseButton>。

禁用 Attribute 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false。

如果你使用了 <script setup>,你需要一个额外的 <script> 块来书写这个选项声明:

js
1<script>
2// 使用一个简单的 <script> to declare options
3export default {
4  inheritAttrs: false
5}
6</script>
7
8<script setup>
9// ...setup 部分逻辑
10</script>

最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 如何应用。

这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。

html
1<span>Fallthrough attribute: {{ $attrs }}</span>

这个 $attrs 对象包含了除组件的 props 和 emits 属性外的所有其他 attribute,例如 class,style,v-on 监听器等等。

  • 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
  • 像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick。

有时候我们可能为了样式,需要在 <button> 元素外包装一层 <div>:

html
1<div class="btn-wrapper">
2  <button class="btn">click me</button>
3</div>

我们想要所有像 class 和 v-on 监听器这样的透传 attribute 都应用在内部的 <button> 上而不是外层的 <div> 上。我们可以通过设定 inheritAttrs: false 和使用 v-bind="$attrs" 来实现:

html
1<div class="btn-wrapper">
2  <button class="btn" v-bind="$attrs">click me</button>
3</div>

没有参数的 v-bind 会将一个对象的所有属性都作为 attribute 应用到目标元素上。

多根节点的 Attribute 继承

和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。

如果 $attrs 被显式绑定,则不会有警告:

html
1<header>...</header>
2<main v-bind="$attrs">...</main>
3<footer>...</footer>

查看组件中所有透传的 attribute

可以在 <script setup> 中使用 useAttrs() API 来访问一个组件的所有透传 attribute:

html
1<script setup>
2  import { useAttrs } from 'vue'
3
4  const attrs = useAttrs()
5</script>