Vue3 基础 - Vue3 Essentials

  • 想起来回炉一下 6 年前学的 Vue,AI 用太多了,一点不会也不行
  • 基本概念,完全基于 SFC(单文件组件)和 Composition API(组合式 API)
  • ref: https://cn.vuejs.org/

创建应用

开始尝试 pnpm.

1
pnpm create vue@latest

响应式基础

Ref

通常采用 ref 来创建响应式数据,ref 会返回一个对象,这个对象有一个 .value 属性来存储实际的值。

从 Rust 等有引用类型的语言来看,ref 是一个相当符合直觉的概念,在模版中使用时,甚至有自动 deref,通过 ref 创建的响应式对象相当于是到处传递指针,而非原始对象,这实现了 proxy。

1
2
3
import { ref } from "vue";

const count = ref(0);

这创建了一个响应式的 count 变量,初始值为 0,要在 js 中访问或修改它的值,需要使用 count.value,而在模版中可以直接使用 count

模版中会自动解一层 ref,但是如果二层 ref 是文本插值的最终计算值,那么其也会自动解
使用 .value 的原因是需要 setter/getter 实现 proxy

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { ref } from "vue";

const count = ref(0);

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

<template>
<button @click="increment">{{ count }}</button>
</template>

这里的 setup 是一个特殊的 <script> 标签,里面的内容会在组件实例化时执行,并且可以直接在模版中使用。
模版采用 Mustache 语法,采用 {{ }} 包裹一个表达式。

Ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构,比如 Map。 同时 Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到

1
2
3
4
5
6
7
8
9
10
11
12
import { ref } from "vue";

const obj = ref({
nested: { count: 0 },
arr: ["foo", "bar"],
});

function mutateDeeply() {
// 以下都会按照期望工作
obj.value.nested.count++;
obj.value.arr.push("baz");
}

 也可以通过 shallowRef 创建浅层响应式对象,shallowRef 只会使对象的第一层属性具有响应性,嵌套对象或数组的变化不会被检测到。

Reactive

与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性

1
2
3
import { reactive } from "vue";

const state = reactive({ count: 0 });

reactive 会返回一个响应式的对象,直接访问和修改对象的属性即可触发响应式更新。在上面这个例子中,直接可以使用 state.count 来访问和修改 count 的值,而不需要使用 .value。

需要注意的是 ,reactive 只能用于对象类型的值,事实上在 Ref 中遇到对象类型会自动用 reactive 包装,但它不支持基本类型,这是因为基本类型的修改本身无法被捕获,但是对象上的操作可以被捕获,同时如果将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接

一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:

1
2
3
4
5
6
7
8
9
const count = ref(0);
const state = reactive({
count,
});

console.log(state.count); // 0

state.count = 1;
console.log(count.value); // 1

只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。

模板语法

文本插值

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

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

原始 HTML

采用 v-html 指令来输出原始 HTML,注意不要在不受信任的内容上使用 v-html,以防止 XSS 攻击。

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

Attribute 绑定

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

通过 v-bind 指令来绑定 HTML 属性到一个表达式,上面这个例子中将 id 属性绑定到一个名为 dynamicId 的变量上。v-bind 可以绑定任何有效的 HTML 属性,包括 class、style、src 等。

绑定对象可以是 ref 或 reactive 对象的属性。也可以是普通类型的变量,或者是计算属性,v-bind: 可以简写为 “:”

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

同时 3.4 以上版本支持同名简写,例如有同名的 id 变量时,可以直接写成:

1
<div :id></div>

布尔型 Attribute

布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。

1
<button :disabled="isButtonDisabled">Button</button>

isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。

动态绑定多个值

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

1
2
3
4
5
6
7
8
9
10
11
<script setup>
const objectOfAttrs = {
id: "container",
class: "wrapper",
style: "background-color:green",
};
</script>

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

计算属性

只读计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { reactive, computed } from "vue";

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

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? "Yes" : "No";
});
</script>

<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>

采用计算属性或是在模版中调用一个函数,Vue 都会自动捕获依赖并进行响应式更新,区别在于计算属性会缓存结果,只有当依赖发生变化时才会重新计算,而函数在每次 DOM 更新时都会重新执行。

可写计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { ref, computed } from "vue";

const firstName = ref("John");
const lastName = ref("Doe");

const fullName = computed({
// getter
get() {
return firstName.value + " " + lastName.value;
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(" ");
},
});
</script>

通过 getter 和 setter,计算属性可以实现双向绑定。上面的例子中,fullName 是一个可写计算属性,当 fullName 被修改时,会自动更新 firstName 和 lastName 的值。

当你再运行 fullName.value = ‘John Doe’ 时,setter 会被调用而 firstName 和 lastName 会随之更新。

计算属性是一个 ref,因此也需要 .value

获取上一个值

3.4 以上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { ref, computed } from "vue";

const count = ref(2);

// 这个计算属性在 count 的值小于或等于 3 时,将返回 count 的值。
// 当 count 的值大于等于 4 时,将会返回满足我们条件的最后一个值
// 直到 count 的值再次小于或等于 3 为止。
const alwaysSmall = computed((previous) => {
if (count.value <= 3) {
return count.value;
}

return previous;
});
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { ref, computed } from "vue";

const count = ref(2);

const alwaysSmall = computed({
get(previous) {
if (count.value <= 3) {
return count.value;
}

return previous;
},
set(newValue) {
count.value = newValue * 2;
},
});
</script>

计算属性的 getter 应只做计算而没有任何其他的副作用。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!

条件渲染

v-if / v-else-if / v-else

1
2
3
4
<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>

容易理解,当 type 符合对应分支条件时,只渲染(真正写入 DOM)对应分支的内容

对于多个元素,可以通过 <template> 包裹起来

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

v-show

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

v-if vs. v-show

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

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

v-if 和 v-for

v-ifv-for 同时存在于一个元素上的时候,v-if 会首先被执行。

列表渲染

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

1
2
3
4
5
6
7
8
9
<script setup>
import { ref } from "vue";
const items = ref([{ message: "Foo" }, { message: "Bar" }]);
</script>
<template>
<ul>
<li v-for="item of items">{{ item.message }}</li>
</ul>
</template>

也支持 v-for="(item, index) in items",其中 index 是当前迭代项的索引,以及解构语法 v-for="({ message }, index) in items",其中 message 是当前迭代项的属性。

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

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

通过 key 管理状态​

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。

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

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

事件处理

监听事件

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

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

  • 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  • 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

在内联事件处理器中访问事件参数

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

<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
1
2
3
4
5
6
7
function warn(message, event) {
// 这里可以访问原生事件
if (event) {
event.preventDefault();
}
alert(message);
}

事件修饰符

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

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

  • .stop
  • .prevent
  • .self
  • .capture
  • .once
  • .passive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 单击事件将停止传递 -->
<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>

表单输入绑定

 单行/多行文本输入框

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

1
2
<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />

1
2
3
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现 v-model 不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的 input 事件监听器和 value 绑定而不要使用 v-model

复选框

单一复选框的 v-model 绑定的值是一个布尔值,表示复选框是否被选中。

1
2
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

我们也可以将多个复选框绑定到同一个数组或集合的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script setup>
import { ref } from "vue";
const checkedNames = ref([]);
</script>
<template>
<div>Checked names: {{ checkedNames }}</div>

<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>

存在数组里的指是 value

单选按钮

1
2
3
4
5
6
7
<div>Picked: {{ picked }}</div>

<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>

<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>

存在 picked 里的是 `value

选择器

单选

1
2
3
4
5
6
7
8
<div>Selected: {{ selected }}</div>

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

如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。

多选

1
2
3
4
5
6
7
<div>Selected: {{ selected }}</div>

<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>

绑定的是一个数组,数组中包含所有被选中的值。

修饰符

默认情况下,v-model 会在每次 input 事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy 修饰符来改为在每次 change 事件后更新数据

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

如果你想让用户输入自动转换为数字,你可以在 v-model 后添加 .number 修饰符来管理输入:

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

如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:

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

侦听器

watch

可以使用 watch 函数在每次响应式状态发生变化时触发回调函数

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
<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>

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const x = ref(0);
const y = ref(0);

// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`);
});

// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`);
},
);

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`);
});

但不能侦听一个非响应式对象或响应式对象的属性,因为它们不会触发回调函数,你可以用一个 getter 函数作为 workaround。

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

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在建侦听器时,立即执行一遍回调。

1
2
3
4
5
6
7
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
},
{ immediate: true },
);

一次性侦听器

1
2
3
4
5
6
7
watch(
source,
(newValue, oldValue) => {
// 当 `source` 变化时,仅触发一次
},
{ once: true },
);

watchEffect

侦听器的回调使用与源完全相同的响应式状态是很常见的。

1
2
3
4
5
6
7
8
9
10
11
12
13
const todoId = ref(1);
const data = ref(null);

watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`,
);
data.value = await response.json();
},
{ immediate: true },
);

watchEffect() 允许我们自动跟踪回调的响应式依赖。

1
2
3
4
5
6
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`,
);
data.value = await response.json();
});

watch vs. watchEffect

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。

  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

模板引用

ref 是一个特殊的 attribute,和 v-forkey 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { useTemplateRef, onMounted } from "vue";

// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef("my-input");

onMounted(() => {
input.value.focus();
});
</script>

<template>
<input ref="my-input" />
</template>

注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还不存在呢!

如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null 的情况:

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

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script setup>
import { ref, useTemplateRef, onMounted } from "vue";

const list = ref([
/* ... */
]);

const itemRefs = useTemplateRef("items");

onMounted(() => console.log(itemRefs.value));
</script>

<template>
<ul>
<li v-for="item in list" ref="items">{{ item }}</li>
</ul>
</template>

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

1
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }" />

组件基础

定义

1
2
3
4
5
6
7
8
9
<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
<script setup>
import ButtonCounter from "./ButtonCounter.vue";
</script>

<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>

传递 props

Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。

BlogPost.vue
1
2
3
4
5
6
7
8
<script setup>
const props = defineProps(["title"]);
console.log(props.title);
</script>

<template>
<h4>{{ title }}</h4>
</template>

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

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

一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。

监听事件

组件实例提供了一个自定义事件系统。父组件可以通过 v-on 或 @ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:

BlogPost.vue
1
2
3
4
5
6
7
<!-- 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
1
2
3
4
5
6
7
8
<div :style="{ fontSize: postFontSize + 'em' }">
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
@enlarge-text="postFontSize += 0.1"
/>
</div>

我们可以通过 defineEmits 宏来声明需要抛出的事件,和 defineProps 类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 $emit 方法的 emit 函数。它可以被用于在组件的 <script setup> 中抛出事件,因为此处无法直接访问 $emit

1
2
3
4
5
<script setup>
const emit = defineEmits(["enlarge-text"]);

emit("enlarge-text");
</script>

通过插槽来分配内容​

可以通过 Vue 的自定义 <slot> 元素来实现组件的内容分发。父组件可以在子组件的 <slot> 元素中插入任意内容,子组件会将这些内容渲染到 <slot> 元素所在的位置。

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div class="alert-box">
<strong>This is an Error for Demo Purposes</strong>
<slot />
</div>
</template>

<style scoped>
.alert-box {
/* ... */
}
</style>
1
<AlertBox> Something bad happened. </AlertBox>

动态组件

有些场景会需要在两个组件间来回切换,比如 Tab 界面:

1
2
<!-- currentTab 改变时组件也改变 -->
<component :is="tabs[currentTab]"></component>

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

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

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

生命周期钩子

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

1
2
3
4
5
6
7
<script setup>
import { onMounted } from 'vue'

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

生命周期图示

lifecycle