Vue 3.0

1/24/2024 Vue

# Vue 3.0

Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

# Vue 基础部分

# 一、创建一个 Vue 应用


每个 Vue 应用都是通过 createApp (opens new window) 函数创建一个新的 应用实例.

import { createApp } from 'vue'

const app = createApp({
  /* 根组件选项 */
})
1
2
3
4
5

我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。

# 1.导入组件

我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。

import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'

const app = createApp(App)
1
2
3
4
5

# 2.挂载应用

应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:

html

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

js

app.mount('#app')
1

应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分。

.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。

# 二、模板语法


# 1.文本插值

最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):

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

双大括号标签会被替换为相应组件实例中 (opens new window) msg 属性的值。同时每次 msg 属性更改时它也会同步更新。

# 2.原始 HTML

双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令 (opens new window)

<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
1
2

这里看到的 v-html attribute 被称为一个指令。指令由 v- 作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,你可能已经猜到了,它们将为渲染的 DOM 应用特殊的响应式行为。

# 3.Attribute 绑定

双大括号不能在 HTML attributes 中使用。 想要响应式地绑定一个 attribute,应该使用 v-bind 指令 (opens new window)

<div v-bind:id="dynamicId"></div>
1

v-bind 指令指示 Vue 将元素的 id attribute 与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

# 简写

因为 v-bind 非常常用,我们提供了特定的简写语法:

<div :id="dynamicId"></div>
1

开头为 : 的 attribute 可能和一般的 HTML attribute 看起来不太一样,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。

# 动态绑定多个值

如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
}
1
2
3
4

通过不带参数的 v-bind,你可以将它们绑定到单个元素上:

<div v-bind="objectOfAttrs"></div>
1

至此,我们仅在模板中绑定了一些简单的属性名。但是 Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>
1
2
3
4
5
6
7

# 4.调用函数

可以在绑定的表达式中使用一个组件暴露的方法:

template

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

[!info] TIPS 绑定在表达式中的方法在组件每次更新时都会被重新调用,因此应该产生任何副作用,比如改变数据或触发异步操作。

# 5.动态参数

同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:

<!--
注意,参数表达式有一些约束,
参见下面“动态参数值的限制”与“动态参数语法的限制”章节的解释
-->
<a v-bind:[attributeName]="url"> ... </a>

<!-- 简写 -->
<a :[attributeName]="url"> ... </a>
1
2
3
4
5
6
7
8

这里的 attributeName 会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName,其值为 "href",那么这个绑定就等价于 v-bind:href

动态参数同样可以绑定在 v-on 参数上

<a @[var]="func"> ... </a>
1

其中 var 是一个变量,表示监听的行为,可以是 click, hover 等等。

# 三、响应式基础


# 1.ref()

在组合式 API 中,推荐使用 ref() (opens new window) 函数来声明响应式状态:

import { ref } from 'vue'

const count = ref(0)
1
2
3

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
1
2
3
4
5
6
7

要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:

import { ref } from 'vue'

export default {
  // `setup` 是一个特殊的钩子,专门用于组合式 API。
  setup() {
    const count = ref(0)

    // 将 ref 暴露给模板
    return {
      count
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<div>{{ count }}</div>
1

[!INFO] 注意

  • return 里的内容是暴露给HTML的,只有暴露的变量或函数才能被调用。
  • 在模板中使用 ref 时,我们不需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包。

# 2.<script setup>

在 setup() 函数中手动暴露大量的状态和方法非常繁琐。 幸运的是,我们可以通过使用单文件组件 (SFC) (opens new window) 来避免这种情况。我们可以使用 <script setup> 来大幅度地简化代码:

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }}
  </button>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.reactive()

还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:

import { reactive } from 'vue'

const state = reactive({ count: 0 })
1
2
3

响应式对象是 JavaScript 代理 (opens new window),其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。

reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装。当 ref 的值是一个对象时,ref() 也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() (opens new window) API 可以选择退出深层响应性。

即:reactive 是用在对象中的,产生一个响应式的对象。

# 4.注意事项

  • 与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型(如 Map) 中的元素被访问时,它不会被解包:
    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)
    
    1
    2
    3
    4

# 四、计算属性


如果一个响应式状态是通过一些方法计算出来的,可以使用 计算属性 来避免在HTML中过于复杂的表示。

如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。

const publishedBooksMessage = computed(() => {
	return author.books.length > 0 ? 'Yes' : 'No' 
})
1
2
3

计算属性computed(() => {}) 来定义。其中返回的是复杂处理后的结果。

# 1.计算属性与函数的区别

也许,一个计算属性也可以这么写,执行结果与计算属性无区别:

function calculateBooksMessage() {
  return author.books.length > 0 ? 'Yes' : 'No'
}
1
2
3

但实际上是有区别的。一个计算属性仅会在其响应式依赖更新时才重新计算。 这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。

而函数则不一样,调用一次函数就会重新计算一遍结果,如果计算过程复杂,会占用大量系统资源。

# 2.注意事项

# Getter 不应有副作用​

计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。 举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。 在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。

# 避免直接修改计算属性值​

从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

# 五、Class 与 Style 绑定


数据绑定的一个常见需求场景是操纵元素的CSS class 列表和内联样式。因为 classstyle 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。

但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 classstylev-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

# 1.Class 绑定

我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class:

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

上面代码 active 是否存在,取决于 isActive 的值。

绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:

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

我们也可以绑定一个返回对象的 计算属性 。这是一个常见且很有用的技巧。

如果你也想在数组中有条件地渲染某个 class,你可以使用三元表达式:

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

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

# 2.绑定内联样式

直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:

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

# 六、条件渲染


# 1.v-if (opens new window)

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

<h1 v-if="awesome">Vue is awesome!</h1>
1

# 2.<template> 上的 v-if (opens new window)

因为 v-if 是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>
1
2
3
4
5

v-else 和 v-else-if 也可以在 <template> 上使用。

# 3.v-show (opens new window)

另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:

<h1 v-show="ok">Hello!</h1>
1

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

# 4.v-showv-if 的区别

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

# 七、列表渲染


# 1.v-for

我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名

JS:

const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
1

HTML:

<li v-for="item in items">
  {{ item.message }}
</li>
1
2
3

在 v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
1
2
3

对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域:

HTML:

<li v-for="item in items">
  <span v-for="childItem in item.children">
    {{ item.message }} {{ childItem }}
  </span>
</li>
1
2
3
4
5

你也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法

HTML:

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

# 2.<template> 上的 v-for​

与模板上的 v-if 类似,你也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。例如:

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

# 3.通过 key 管理状态

为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:

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

# 八、事件处理


# 1.监听事件

我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler" 或 @click="handler"

事件处理器 (handler) 的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。

  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

# 2.内联事件处理器

内联事件处理器通常用于简单场景,例如:

JS:

const count = ref(0)
1

HTML:

<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>
1
2

# 3.方法事件处理器​

随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此** v-on 也可以接受一个方法名或对某个方法的调用。**

举例来说:

JS:

const name = ref('Vue.js')

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

HTML:

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

# 4.事件修饰符

在处理事件时调用 event.preventDefault() 或 event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。

为解决这一问题,Vue 为 v-on 提供了事件修饰符。修饰符是用 . 表示的指令后缀,包含以下这些:

  • .stop
  • .prevent
  • .self
  • .capture
  • .once
  • .passive
<!-- 单击事件将停止传递 --> 
<a @click.stop="doThis"></a> 

<!-- 提交事件将不再重新加载页面 --> 
<form @submit.prevent="onSubmit"></form> 

<!-- 修饰语可以使用链式书写 --> 
<a @click.stop.prevent="doThat"></a> 

<!-- 也可以只有修饰符 --> 
<form @submit.prevent></form> 

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 --> 
<!-- 例如:事件处理器不来自子元素 --> 
<div @click.self="doThat">...</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 九、表单输入绑定


在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦,v-model 指令帮我们简化了这一步骤:

<input v-model="text">
1

这样,这个 input 元素的内容被保存在了 text 变量中。

# 十、生命周期 HOOK


# 注册周期钩子

举例来说,onMounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:

<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  console.log(`the component is now mounted.`)
})

</script>
1
2
3
4
5
6
7
8

还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 onMounted (opens new window)onUpdated (opens new window) 和 onUnmounted (opens new window)

# 十一、侦听器


计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:

<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = 'Thinking...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = 'Error! Could not reach the API. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    Ask a yes/no question:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

解释:

  • watch 的第一个参数可以是不同形式的“数据源”:它可以是一个:
    • ref (包括计算属性)
    • 一个响应式对象
    • 一个 getter 函数
    • 多个数据源组成的数组
  • 注意,你不能直接侦听响应式对象的属性值

# 组件部分


组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

# 一、定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做 单文件组件

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>
1
2
3
4
5
6
7
8
9

# 二、使用组件

要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。

<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>Here is a child component!</h1>
  <!-- 使用组件!!!!-->
  <ButtonCounter />
</template>
1
2
3
4
5
6
7
8
9

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

# 三、参数传递 props

Props 是一种特别的 attributes,你可以在组件上声明注册。我们必须在组件的 props 列表上声明它。这里要用到 defineProps (opens new window) 宏:

<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <!-- 在这里使用prop-->
  <h4>{{ title }}</h4>
</template>
1
2
3
4
5
6
7
8
9

如果你没有使用 <script setup>,props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入:

export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}
1
2
3
4
5
6

当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />
1
2
3

除了使用字符串数组来声明 prop 外,还可以使用对象的形式:

// 使用 <script setup>
defineProps({
  title: String,
  likes: Number
})
js

// 非 <script setup>
export default {
  props: {
    title: String,
    likes: Number
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 四、事件传递

$emit 方法用于”子组件传递给父组件“。子组件使用 $emit 触发一个自定义事件,父组件使用 @ 来监听子组件的行为。

# 1.声明触发的事件

如果我们需要触发一个事件,需要在子组件设置触发的事件名。组件可以显式地通过 defineEmits() (opens new window) 宏来声明它要触发的事件:

<script setup>
defineEmits(['inFocus', 'submit'])
</script>
1
2
3

我们在 <template> 中使用的 $emit 方法不能在组件的 <script setup> 部分中使用,但 defineEmits() 会返回一个相同作用的函数供我们使用:

<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
  emit('submit')
}
</script>
1
2
3
4
5
6
7

这里定义了两个事件,一个 inFocus ,一个 submit 。如果想在子组件的函数中调用这个事件,需要使用 emit 函数。

# 2.触发与监听事件

在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件 (例如:在 v-on 的处理函数中):

<!-- MyComponent -->
<!-- 说明点击了这个按钮,会像父组件传递一个信息。这个信息是什么看下文 -->
<button @click="$emit('someEvent')">click me</button>
1
2
3

父组件可以通过 v-on (缩写为 @) 来监听事件:

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

有时候我们会需要在触发事件时附带一个特定的值。举例来说,我们想要 <BlogPost> 组件来管理文本会缩放得多大。在这个场景下,我们可以给 $emit 提供一个额外的参数:

<!-- 注意,这里的 emit 是这么写的!! -->
<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>
1
2
3
4

说明,点击了这个按钮,会向父组件传递一个1。

然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:

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

或者,也可以用一个组件方法来作为事件处理函数:

<MyButton @increase-by="increaseCount" />
1

该方法也会接收到事件所传递的参数:

function increaseCount(n) {
  count.value += n
}
1
2
3

注意,这里的 n 是用来监听内容的,默认这里面是监听返回的值,不用赋值。

# 五、组件 v-model

如果,我们想把子组件产生的值(如input的值)传递给父组件的一个变量,可以使用 v-model 方法。

从 Vue 3.4 开始,推荐的实现方式是使用 defineModel() (opens new window) 宏:

<!-- Child.vue -->
<script setup>
const model = defineModel()

function update() {
  model.value++
}
</script>

<template>
  <div>parent bound v-model is: {{ model }}</div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12

父组件可以用 v-model 绑定一个值:

<!-- Parent.vue -->
<Child v-model="count" />
1
2

这里的 model 默认是父组件的一个变量,只要父组件使用 v-model 绑定了,子组件就能直接使用父组件这个变量的值。这叫做 双向绑定

defineModel() 返回的值是一个 ref。它可以像其他 ref 一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定的作用:

  • 它的 .value 和父组件的 v-model 的值同步;
  • 当它被子组件变更了,会触发父组件绑定的值一起更新。

这意味着你也可以用 v-model 把这个 ref 绑定到一个原生 input 元素上,在提供相同的 v-model 用法的同时轻松包装原生 input 元素:

<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model" />
</template>
1
2
3
4
5
6
7

意思是子组件里的 model 值变了,父组件绑定的值也会跟着变。

# 六、透传 Attributes

# 1.Attributes 继承

透传 attribute” 指的是传递给一个组件,却**没有被该组件声明为 props 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyle 和 id

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton> 组件,它的模板长这样:

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

一个父组件使用了这个组件,并且传入了 class

<MyButton class="large" />
1

最后渲染出的 DOM 结果是:

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

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。

就是一些自带的属性,比如 class id 等,父组件定义了,会自动加到子组件上,尽管子组件没有定义叫做 classidprop

# 2.class id 属性的合并

如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton> 组件的模板改成这样:

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

则最后渲染出的 DOM 结果会变成:

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

# 3.v-on 监听器继承 (opens new window)

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

<MyButton @click="onClick" />
1

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

# 4.在 JavaScript 中访问透传 Attributes

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

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>
1
2
3
4
5

如果没有使用 <script setup>attrs 会作为 setup() 上下文对象的一个属性暴露:

export default {
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
}
1
2
3
4
5
6

# 七、插槽 Slots

如果子组件想接收 模板内容 ,就是两个 HTML 标签之间的内容,就需要使用 插槽 Slots

举例来说,这里有一个 <FancyButton> 组件,可以像这样使用:

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>
1
2
3

而 <FancyButton> 的模板是这样的:

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>
1
2
3

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

插槽内容无法访问子组件的数据。 Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。换言之:

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

# 1.默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。

如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 <slot> 标签之间来作为默认内容。

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>
1
2
3
4
5

# 2.具名插槽

如果我们想使用多个插槽,需要对每个插槽进行命名。对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>
1
2
3
4
5
6
7
8
9
10
11

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:

<BaseLayout>
  <template v-slot:header>
    <!-- header 插槽的内容放这里 -->
  </template>
</BaseLayout>
1
2
3
4
5