Vue3学习笔记
学习时间:2024年7月30日
学习来源:Vue官方文档
1 简介
Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。
1.1 单文件组件
在大多数启用了构建工具的 Vue 项目中,我们可以使用一种类似 HTML 格式的文件来书写 Vue 组件,它被称为单文件组件 (也被称为 *.vue 文件,英文 Single-File Components,缩写为 SFC)。顾名思义,Vue 的单文件组件会将一个组件的逻辑 (JavaScript),模板 (HTML) 和样式 (CSS) 封装在同一个文件里。
计数器示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script setup> import { ref } from 'vue' const count = ref(0) </script>
<template> <button @click="count++">Count is: {{ count }}</button> </template>
<style scoped> button { font-weight: bold; } </style>
|
1.2 API风格
Vue 的组件可以按两种不同的风格书写:选项式 API(Options API) 和组合式 API(Composition API)。
1.2.1 Options API
使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 data、methods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。
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
| <script> export default { // data() 返回的属性将会成为响应式的状态 // 并且暴露在 `this` 上 data() { return { count: 0 } },
// methods 是一些用来更改状态与触发更新的函数 // 它们可以在模板中作为事件处理器绑定 methods: { increment() { this.count++ } },
// 生命周期钩子会在组件生命周期的各个不同阶段被调用 // 例如这个函数就会在组件挂载完成后被调用 mounted() { console.log(`The initial count is ${this.count}.`) } } </script>
<template> <button @click="increment">Count is: {{ count }}</button> </template>
|
1.2.2 Composition API
在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup 是一个标识,告诉 Vue 需要在编译时进行一些处理,让我们可以更简洁地使用组合式 API。比如,<script setup> 中的导入和顶层变量/函数都能够在模板中直接使用。
下面是使用了组合式 API 与 <script setup> 改造后和上面的模板完全一样的组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script setup> import { ref, onMounted } from 'vue'
// 响应式状态 const count = ref(0)
// 用来修改状态、触发更新的函数 function increment() { count.value++ }
// 生命周期钩子 onMounted(() => { console.log(`The initial count is ${count.value}.`) }) </script>
<template> <button @click="increment">Count is: {{ count }}</button> </template>
|
核心概念
- setup 函数:这是 Composition API 的核心。
setup 函数在组件实例创建之前执行,并且作为组件选项传递。它用于初始化组件的状态、定义响应式数据和计算属性、并声明生命周期钩子。
- 响应式引用(refs):使用
ref 创建响应式数据。响应式引用是包含 .value 属性的对象,可以用来追踪和更新数据变化。ef() 函数可以根据给定的值来创建一个响应式的数据对象,返回值是一个对象,且只包含一个 .value 属性。
- 响应式对象(reactive):使用
reactive 创建一个响应式对象。与 ref 不同,reactive 直接返回一个响应式代理对象,适用于更复杂的嵌套对象。
- 计算属性(computed):使用
computed 创建计算属性,它依赖于响应式数据并会在依赖发生变化时自动更新。
- 生命周期钩子:Vue 3 提供了一组新的生命周期钩子函数,可以在
setup 函数中调用。
与选项式API的对比

图中色块表示逻辑一致的代码块。可以看到组合式 API允许我们编写更有条理的代码。
生命周期函数对比
| Vue2 Options-based API |
Vue Composition API |
| beforeCreate |
setup() |
| created |
setup() |
| beforeMount |
onBeforeMount |
| mounted |
onMounted |
| beforeUpdate |
onBeforeUpdate |
| updated |
onUpdated |
| beforeDestroy |
onBeforeUnmount |
| destroyed |
onUnmounted |
| errorCaptured |
onErrorCaptured |
因为 setup 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。
2 基础
以下内容均基于组合式API。
2.1 模板语法
2.1.1 文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
1
| <span>Message: {{ msg }}</span>
|
双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。
2.1.2 原始HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,需要使用 v-html 指令:
1 2
| <p>Using text interpolation: {{ rawHtml }}</p> <p>Using v-html directive: <span v-html="rawHtml"></span></p>
|
当rawHtml设置为:<span style="color: red">This should be red.</span>时的效果:
1 2 3
| Using text interpolation: <span style="color: red">This should be red.</span>
Using v-html directive: This should be red.
|
2.1.3 v-bind绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令:
1
| <div v-bind:id="dynamicId"></div>
|
简写:
1
| <div :id="dynamicId"></div>
|
2.2 响应式基础
2.2.1 ref
在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:
1 2 3
| import { ref } from 'vue'
const count = ref(0)
|
ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:
1 2 3 4 5 6 7
| const count = ref(0)
console.log(count) console.log(count.value)
count.value++ console.log(count.value)
|
ref会根据初始化时的值推导其类型:
1 2 3 4 5 6 7
| import { ref } from 'vue'
const year = ref(2020)
year.value = '2020'
|
其他的示例:
1 2 3
| let hMS = ref(null); let titleName = ref(""); let bigScreenFlightDetail = ref([]);
|
要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { ref } from 'vue'
export default { setup() { const count = ref(0)
return { count } } }
|
1 2 3
| <button @click="count++"> {{ count }} </button>
|
2.2.2 <script setup>
① 基本语法
在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup> 来大幅度地简化代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <script setup> import { ref } from 'vue' // 导入 import { capitalize } from './helpers'
// 声明的变量 const count = ref(0)
// 声明的函数 function increment() { count.value++ } </script>
<template> <button @click="increment"> {{ count }} </button> <div>{{ capitalize('hello') }}</div> </template>
|
<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。这是实际开发时最常用的格式。
<script setup> 中的代码会在每次组件实例被创建的时候执行。
② defineProps和defineEmits
defineProps 和 defineEmits 是 Vue 3 中 <script setup> 语法的两个核心 Composition API 函数,用于简化组件的 props 和事件声明。使用 TypeScript 时,能提供更好的类型推断。
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <div>{{ message }}</div> </template>
<script setup> // 引入 defineProps const props = defineProps({ message: String });
// 现在可以直接使用 props.message </script>
|
使用类型推断:
1 2 3 4 5 6 7 8 9 10 11 12
| <template> <div>{{ message }}</div> </template>
<script setup lang="ts"> // 使用 TypeScript 进行类型声明 const props = defineProps<{ message: string; // TypeScript 会推断 props.message 的类型为 string }>(); // 现在可以直接使用 props.message </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <template> <button @click="handleClick">Click me</button> </template>
<script setup> // 1.定义了一个名为 emit 的函数 // 2.类型注解定义了一个名为 click 的事件,并且该事件不接受任何参数(即参数类型为 void),返回值为void const emit = defineEmits<{ (event: 'click'): void; }>();
const handleClick = () => { // 调用emit函数,触发其中的click事件 emit('click'); // TypeScript 会确保 emit 事件名称和参数类型是正确的 } </script>
|
名称对应关系:

更复杂的事件示例:
1 2 3 4
| const emit = defineEmits<{ (event: 'click'): void; (event: 'submit', payload: { username: string; password: string }): void; }>();
|
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
| <template> <button @click="handleClick">{{ label }}</button> </template>
<script setup> import { ref } from 'vue';
// 定义 props const props = defineProps({ label: { type: String, default: 'Click me' } });
// 定义 emit 事件 const emit = defineEmits<{ (event: 'click'): void; }>();
// 事件处理函数 const handleClick = () => { emit('click'); }; </script>
|
TypeScript的类型推断
TypeScript 自动推断出变量、函数返回值、参数等的类型,而无需显式地声明这些类型。这使得 TypeScript 能够在许多情况下提供类型安全,而无需过多的类型注释。
变量初始化推断: 当你给一个变量赋值时,TypeScript 可以根据这个值自动推断出变量的类型。
1 2
| let num = 42; let message = "Hello";
|
函数返回值推断: TypeScript 可以根据函数体中的代码自动推断函数的返回类型。
1 2 3
| function add(a: number, b: number) { return a + b; }
|
参数类型推断: 当你传递参数给一个函数时,TypeScript 会根据参数的类型推断函数的类型。
1 2 3
| function greet(name: string) { return `Hello, ${name}`; }
|
泛型推断: 当使用泛型函数或类时,TypeScript 可以根据传递给它的参数推断出泛型的具体类型。
1 2 3
| function identity<T>(value: T): T { return value; }
|
③ defineExpose
在 Vue 3 Composition API 中,defineExpose 是一个函数,用于在组合式组件中暴露内部方法或数据,以便其父组件可以访问这些方法或数据。
示例
考虑一个简单的计数器组件,我们想要在父组件中调用这个计数器组件内部的递增和重置方法。
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
| <!-- Counter.vue --> <template> <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> <button @click="reset">Reset</button> </div> </template>
<script setup> import { ref, defineExpose } from 'vue';
const count = ref(0);
const increment = () => { count.value++; };
const reset = () => { count.value = 0; };
// 将需要暴露给父组件的方法暴露出去 defineExpose({ increment, reset }); </script>
|
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
| <!-- ParentComponent.vue --> <template> <div> <Counter ref="counterRef" /> <button @click="callIncrement">Call Increment</button> <button @click="callReset">Call Reset</button> </div> </template>
<script setup> import { ref } from 'vue'; import Counter from './Counter.vue';
const counterRef = ref(null);
const callIncrement = () => { if (counterRef.value) { counterRef.value.increment(); } };
const callReset = () => { if (counterRef.value) { counterRef.value.reset(); } }; </script>
|
2.2.3 DOM更新时机 — nextTick
当修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在nextTick更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
函数类型:
1
| function nextTick(callback?: () => void): Promise<void>
|
- **
function nextTick**:这是一个函数声明,函数名为 nextTick。
- **
callback?: () => void**:这是一个可选的回调函数参数,类型是 () => void,表示这个回调函数不接受任何参数,也不返回任何值(void)。问号 ? 表示这个参数是可选的。
- **
: Promise<void>**:表示这个函数的返回类型是一个 Promise,这个 Promise 不会返回任何值(void)。
示例:
1 2 3 4 5 6 7 8 9
| import { nextTick } from 'vue'
async function increment() { count.value++ nextTick(() => { console.log('DOM has been updated'); }) }
|
在这个示例中,nextTick 接受一个回调函数,该回调函数将在 DOM 更新之后执行。
1 2 3 4 5 6 7 8
| import { nextTick } from 'vue'
async function increment() { count.value++ await nextTick(); console.log('DOM has been updated'); }
|
在这个示例中,nextTick 返回一个 Promise,可以使用 async/await 语法等待这个 Promise,确保在 DOM 更新之后再执行后续代码。
异步相关补充
async 关键字:
- 将一个普通函数转换为异步函数。
- 异步函数返回一个
Promise。
- 允许在异步函数内部使用
await 关键字等待 Promise 完成。
await 关键字:
- 只能在
async 函数内部使用
- 它用于等待一个
Promise 对象的解决(resolved)或拒绝(rejected)。当 await 关键字等待的 Promise 被解决时,它会暂停函数的执行,并返回 Promise 的值。如果 Promise 被拒绝,它会抛出一个异常。
默认情况下,nextTick 接受一个普通的回调函数,在下一个 DOM 更新循环之后执行该回调函数。这时候的回调函数本身不是异步函数。
1 2 3 4 5
| import { nextTick } from 'vue';
nextTick(() => { console.log('This is a normal callback function.'); });
|
如果希望在 nextTick 的回调函数中执行异步操作,可以将回调函数定义为异步函数。这允许你在回调函数内部使用 await。
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
| import { nextTick } from 'vue';
export default { data() { return { message: 'Hello' }; }, methods: { async fetchData() { return new Promise((resolve) => { setTimeout(() => { resolve('Data fetched'); }, 1000); }); }, updateMessage() { this.message = 'Hello, Vue!'; nextTick(async () => { console.log('DOM has been updated'); const data = await this.fetchData(); console.log(data); }); } } };
|
在这个示例中:
nextTick 的回调函数被定义为 async 函数(异步函数)。
- 在回调函数内部,可以使用
await 来等待 fetchData 方法完成。
- 这样可以确保在 DOM 更新之后再执行异步操作,并处理其结果。
2.2.4 reactive
还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:
1 2 3
| import { reactive } from 'vue'
const state = reactive({ count: 0 })
|
在模板中使用:
1 2 3
| <button @click="state.count++"> {{ state.count }} </button>
|
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <button @click="increment">Count is: {{ state.count }}</button> <p>{{ state.message }}</p> </template>
<script> import { reactive } from 'vue';
export default { setup() { const state = reactive({ count: 0, message: 'Hello Vue!' }); const increment = () => state.count++; return { state, increment }; } } </script>
|
2.9 生命周期
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
最常用的是 onMounted、onUpdated 和 onUnmounted。
生命周期钩子的执行顺序如下:
- 创建(Creation)阶段:(
setup)
- 挂载(Mounting)阶段:
onBeforeMount:在组件实例被挂载到 DOM 之前调用。
onMounted:在组件实例被挂载到 DOM 后调用。可以操作DOM。
- 更新(Updating)阶段:
onBeforeUpdate:在组件数据更新之前调用,适合在 DOM 更新之前执行一些逻辑。
onUpdated:在组件数据更新并且 DOM 更新完成后调用。
- 卸载(Unmounting)阶段:
onBeforeUnmount:在组件实例卸载之前调用。
onUnmounted:在组件实例卸载之后调用。

2.9.1 onMounted
注册一个回调函数,在组件挂载完成后执行。
1
| function onMounted(callback: () => void): void
|
示例
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { ref, onMounted } from 'vue'
const el = ref()
onMounted(() => { console.log(el.value) // <div> }) </script>
<template> <div ref="el"></div> </template>
|
2.9.2 onUpdated
注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
1
| function onUpdated(callback: () => void): void
|
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup> import { ref, onUpdated } from 'vue'
const count = ref(0)
// 当count更新时调用 onUpdated(() => { // 文本内容应该与当前的 `count.value` 一致 console.log(document.getElementById('count').textContent) }) </script>
<template> <button id="count" @click="count++">{{ count }}</button> </template>
|
2.9.3 onUnmounted
注册一个回调函数,在组件实例被卸载之后调用。
1
| function onUnmounted(callback: () => void): void
|
2.11 模板引用
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref:
它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
2.11.1 访问模板引用
为了通过组合式 API 获得该模板引用,我们需要声明一个匹配模板 ref 属性值的 ref:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup> import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用 // 必须和模板里的 ref 同名 const aaa = ref(null)
onMounted(() => { aaa.value.focus() // 获得焦点 }) </script>
<template> <input ref="aaa" /> </template>
|
注意,只可以在组件挂载后才能访问模板引用。
2.11.2 组件上的ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例。
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <!-- ParentComponent.vue --> <template> <div> <ChildComponent ref="childComponent" /> <button @click="callChildMethod">Call Child Method</button> </div> </template>
<script setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue';
const childComponent = ref(null); // 组件上的引用
const callChildMethod = () => { if (childComponent.value) { // childComponent.value是子组件的实例 childComponent.value.someMethod(); } }; </script>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <!-- ChildComponent.vue --> <template> <div>Child Component</div> </template>
<script setup> import { defineExpose } from 'vue';
const someMethod = () => { console.log('Child method called!'); };
defineExpose({ someMethod }); </script>
|
3 组件
详见:[[Vue组件学习笔记]]
4 路由
详见:[[Vue Router学习笔记]]
5 逻辑复用
5.1 组合式函数 Composable
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。组合式函数本质上就是一个 普通的 JavaScript 函数, 它内部使用了 Vue 的 响应式 API(如 ref, reactive, computed, watch 等), 并返回需要暴露给组件使用的状态和方法。
组合式函数的命名通常以 use 开头,比如 useUser(), useMouse(), useFetch()。
项目结构:
1 2 3 4 5 6 7 8
| src/ ├─ components/ ├─ composables/ │ ├─ useMouse.ts │ ├─ useCounter.ts │ ├─ useFetch.ts │ └─ useUserSession.ts └─ views/
|
5.1.1 基本使用
鼠标跟踪功能的组件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script setup> import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0) const y = ref(0)
function update(event) { x.value = event.pageX y.value = event.pageY }
onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) </script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
|
将功能代码封装到useMouse.ts中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { ref, onMounted, onUnmounted } from 'vue'
export const useMouse = () => { const x = ref(0) const y = ref(0)
function update(event) { x.value = event.pageX y.value = event.pageY }
onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y } }
|
注意:组合式函数中也能直接使用 onMounted, onUnmounted, watch, watchEffect 等生命周期或侦听器。它们会“自动绑定”到调用该函数的组件实例。
在组件中使用:
1 2 3 4 5 6 7
| <script setup> import { useMouse } from './mouse.js'
const { x, y } = useMouse() </script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
|
5.1.2 参数和嵌套组合
Composable 可以相互组合嵌套使用,并且可以设定接收的参数,以上面为例,可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:
1 2 3 4 5 6
| import { onMounted, onUnmounted } from 'vue'
export const useEventListener = (target, event, callback) => { onMounted(() => target.addEventListener(event, callback)) onUnmounted(() => target.removeEventListener('mousemove', callback)) }
|
useMouse中可以进一步简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { ref } from 'vue' import { useEventListener } from './event'
export const useMouse = () => { const x = ref(0) const y = ref(0)
useEventListener(window, 'mousemove', (event) => { x.value = event.pageX y.value = event.pageY })
return { x, y } }
|
5.2 自定义指令 Directive
暂略
5.3 插件 Plugin
暂略
6 状态管理
6.1 概念
理论上来说,每一个 Vue 组件实例都已经在“管理”它自己的响应式状态了。我们以一个简单的计数器组件为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <script setup> import { ref } from 'vue'
// 状态 const count = ref(0)
// 动作 function increment() { count.value++ } </script>
<!-- 视图 --> <template>{{ count }}</template>
|
它是一个独立的单元,由以下几个部分组成:
- 状态:驱动整个应用的数据源;
- 视图:对状态的一种声明式映射;
- 交互:状态根据用户在视图中的输入而作出相应变更的可能方式。
状态管理:一些状态需要在多个组件实例间共享,即状态不属于某个固定的组件实例。
示例:使用响应式变量进行状态共享管理
1 2 3 4 5 6
| import { reactive } from 'vue'
export const store = reactive({ count: 0 })
|
1 2 3 4 5 6
| <script setup> // A组件 import { store } from './store.js' </script>
<template>From A: {{ store.count }}</template>
|
1 2 3 4 5 6
| <script setup> // B组件 import { store } from './store.js' </script>
<template>From B: {{ store.count }}</template>
|
优化:在 store 上定义方法,方法的名称应该要能表达出行动的意图:
1 2 3 4 5 6 7 8 9
| import { reactive } from 'vue'
export const store = reactive({ count: 0, increment() { this.count++ } })
|
6.2 Pinia
Pinia 是一个由 Vue 核心团队维护的状态管理库,对 Vue 2 和 Vue 3 都可用。
Vuex 是 Vue 之前的官方状态管理库。由于 Pinia 在生态系统中能够承担相同的职责且能做得更好,因此 Vuex 现在处于维护模式。它仍然可以工作,但不再接受新的功能。对于新的应用,建议使用 Pinia。
学习笔记详见:[[Pinia学习笔记]]
7 响应式原理
暂略
8 渲染机制
8.1 虚拟DOM
虚拟 DOM(VNode)是 JavaScript 对真实 DOM 的一个轻量级描述(树)。当数据变更时,框架先在内存里重新创建/比较这些描述(diff),只把必要的最小差异(patch)应用到真实 DOM 上,从而减少昂贵的 DOM 操作,提高性能并简化更新逻辑。
VNode(Virtual Node):用 JS 对 DOM 节点的结构进行“描述”。一个简单的 VNode·` 可能长这样(伪结构):
1 2 3 4 5 6 7 8 9
| const vnode = { type: 'div', props: { id: 'hello' }, children: [ ] }
|
它不是 真实的 DOM 元素,而是虚拟 DOM 对象,不会触发布局、绘制。只是纯 JS 对象,创建和比较都很快。
8.2 渲染过程

8.2.1 编译(创建VNode)
Vue 的模板会被编译成 render 函数(渲染函数),render 函数返回 VNode(虚拟DOM),该步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
1 2 3 4 5 6 7 8 9 10
| <template> <div class="card"> <h3>{{ title }}</h3> <p>{{ msg }}</p> </div> </template>
<script setup lang="ts"> const props = defineProps<{ title: string, msg: string }>() </script>
|
编译为类似的render函数,该函数返回VNode:
1 2 3 4 5 6 7
| import { h } from 'vue' export function render(ctx) { return h('div', { class: 'card' }, [ h('h3', null, ctx.title), h('p', null, ctx.msg) ]) }
|
其中:h()(也叫 createVNode)就是创建 VNode 的工厂函数。
- 手写渲染函数:也可以直接手写渲染函数,在处理高度动态的逻辑时,渲染函数相比于模板更加灵活,因为可以完全地使用 JavaScript 来构造想要的 vnode。
1 2 3 4 5 6 7 8 9 10 11 12
| import { defineComponent, h } from 'vue'
export default defineComponent({ props: ['title', 'msg'], render() { return h('div', { class: 'card' }, [ h('h3', null, this.title), h('p', null, this.msg) ]) } })
|
8.2.2 挂载 mount
将VNode初次渲染到真实DOM上的过程称为挂载(mount)。
挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
流程:render -> 得到 VNode 树 -> 将 VNode 转成真实 DOM(createElm)并插入页面
8.2.3 更新 patch
当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
数据变化时:再次执行 render -> 得到新 VNode 树 -> 把新旧 VNode 做 diff -> 把最小改动(patch)应用到真实 DOM。
8.3 渲染函数h
在绝大多数情况下,Vue 推荐使用模板语法来创建应用。然而在某些使用场景下,我们真的需要用到 JavaScript 完全的编程能力。这时渲染函数就派上用场了。
8.3.1 定义
h() 是 Vue 提供的一个用于创建虚拟节点(VNode)的函数,定义如下(简化):
1 2 3 4 5
| h( type: string | Component, props?: object | null, children?: string | VNode[] ): VNode
|
h是 “Hyperscript” 的缩写 —— 意思是“生成 HTML 的脚本”。
8.3.2 用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { defineComponent, h } from 'vue'
export default defineComponent({ name: 'RenderExample', props: { title: String, items: Array }, render() { return h('div', { class: 'wrapper' }, [ h('h2', null, this.title), h('ul', null, this.items?.map((item: any) => h('li', { key: item.id }, item.name) ) ) ]) } })
|
9 常用API
9.1 单文件组件
9.1.1 语法定义
① 总览
一个 Vue 单文件组件 (Single File Component,SFC),通常使用 *.vue 作为文件扩展名,它是一种使用了类似 HTML 语法的自定义文件格式,用于定义 Vue 组件。一个 Vue 单文件组件在语法上是兼容 HTML 的。
每一个 *.vue 文件都由三种顶层语言块构成:<template>、<script> 和 <style>,以及一些其他的自定义块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div class="example">{{ msg }}</div> </template>
<script> export default { data() { return { msg: 'Hello world!' } } } </script>
<style> .example { color: red; } </style>
<custom1> This could be e.g. documentation for the component. </custom1>
|
<template>块:包裹的内容将会被提取、传递给 @vue/compiler-dom,预编译为 JavaScript 渲染函数,并附在导出的组件上作为其 render 选项。
<script setup>块:这个脚本块将被预处理为组件的 setup() 函数,这意味着它将为每一个组件实例都执行。
<style>块: 帮助封装当前组件的样式。
② 预处理器
代码块可以使用 lang 这个 attribute 来声明预处理器语言,最常见的用例就是在 <script> 中使用 TypeScript:
1 2 3
| <script lang="ts"> // use TypeScript </script>
|
lang 在任意块上都能使用,比如我们可以在 <style> 标签中使用 Sass 或是 <template> 中使用 Pug:
1 2 3 4 5 6 7 8 9 10
| <template lang="pug"> p {{ msg }} </template>
<style lang="scss"> $primary-color: #333; body { color: $primary-color; } </style>
|
9.1.2 <script setup>
暂略
9.1.3 组件样式
1 2 3 4 5 6 7
| <style> /* 全局样式 */ </style>
<style scoped> /* 局部样式 */ </style>
|
- 全局样式:作用于整个页面;
- 局部样式:只作用于当前组件,不会受父组件样式影响;Vue 在编译带
scoped 的样式时,会自动为组件生成一个唯一的特征属性;
代码示例
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
| <template> <div class="box"> <p class="text">Hello Vue!</p> </div> </template>
<style> .box { background-color: lightgray; }
.text { color: red; } </style>
<style scoped> .box { background-color: yellow; }
.text { color: blue; } </style>
|
编译后:
1 2 3
| <div class="box" data-v-123abc> <p class="text" data-v-123abc>Hello Vue!</p> </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .box { background-color: red; } .text { color: red; }
.box[data-v-123abc] { background-color: yellow; } .text[data-v-123abc] { color: blue; }
|
以box为例,最终样式结果的分析:
.box 会被两个选择器命中:
.box { background-color: red; }
.box[data-v-123abc] { background-color: yellow; }
由于选择器优先级相同(都是类选择器), scoped 样式编译 顺序靠后,所以最终显示:background-color: yellow;
如果反过来写,则会显示红色:
1 2 3 4 5 6 7
| <style scoped> .text { color: yellow; } </style>
<style> .text { color: red; } </style>
|
9.2 全局API
9.2.1 应用实例API
暂略
9.2.2 通用API
① nextTick()
详见:[[Vue3学习笔记#2.2.3 DOM更新时机 — nextTick]]
② defineComponent()
defineComponent 用于定义一个标准的 Vue 组件,是从 vue 中导出的一个辅助函数:
1
| import { defineComponent } from 'vue'
|
基本使用
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 32 33 34 35 36 37 38 39 40 41 42 43
| <template> <div> <h3>{{ user.name }} ({{ user.age }})</h3> <button @click="sayHello">打招呼</button> </div> </template>
<script lang="ts"> import { defineComponent, PropType } from 'vue'
interface User { name: string age: number }
export default defineComponent({ name: 'Counter', props: { count: { type: Number, default: 0 }, user: { type: Object as PropType<User>, required: true } }, emits: ['update:count', 'reset'], setup(props, { emit }) { // 组件的入口函数 const increase = () => emit('update:count', props.count + 1) const reset = () => emit('reset') // 自动提示 props.user.name 和 props.user.age const sayHello = () => { alert(`你好,${props.user.name}!`) } return { increase, reset, sayHello } } }) </script>
|
在 Vue 3 的 <script setup> 中,其实 不需要显式调用 defineComponent,因为编译器会自动包裹,即 <script setup> 是 defineComponent和setup() 的语法糖,例如:
1 2 3
| <script setup lang="ts"> defineProps<{ msg: string }>() </script>
|
在编译后会变成:
1 2 3 4
| export default defineComponent({ props: { msg: String }, setup(props) { ... } })
|
③ defineAsyncComponent()
详见:[[Vue组件学习笔记#8 异步组件]]
9.3 组合式API
9.3.1 setup()
在 Vue 3 中,setup() 是 组件的入口函数,是使用 Composition API 的基础。 它的主要作用是:定义组件的响应式状态(state)、计算属性(computed)、方法(methods),并决定模板中能访问到哪些变量。
执行时机:setup()在组件 创建实例之前 执行(即生命周期中的最早阶段);此时:**this 不可用(因为组件实例尚未创建);props 已经解析完成(父组件传递到这里);emit** 函数已可使用。
函数签名:
父组件中使用:
1 2 3 4 5 6 7
| <template> <Counter msg="Hello" /> </template>
<script setup lang="ts"> import Counter from './Counter.vue' </script>
|
示例2:返回渲染函数h
在这种情况下,模板将被完全替代,手动写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { h, ref } from 'vue'
export default { props: { msg: String }, setup(props) { const count = ref(0) const increment = () => count.value++ return () => h('button', { onClick: increment }, `${props.msg}: ${count.value}`) } }
|
等价于:
1 2 3
| <template> <button @click="increment">{{ msg }}: {{ count }}</button> </template>
|
父组件中使用:
1 2 3 4 5 6 7
| <template> <Counter msg="点击次数" /> </template>
<script setup lang="ts"> import Counter from './Counter' </script>
|
- 结合
defineComponent:让 TypeScript 自动推断 props、emit 的类型,能让 IDE 自动补全;详见:[[Vue3学习笔记#2️⃣ defineComponent()]]
- 语法糖
<script setup>:详见[[Vue3学习笔记#9.1.2 <script setup>]]
9.3.2 响应式API:核心
① ref()
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
具体详见[[Vue3学习笔记#2.2.1 ref]]
② computed()
在 Vue 3 中,computed() 是一个函数,用于创建 计算属性(派生数据)。计算属性是基于响应式依赖(reactive/ref)自动计算的值。
函数类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function computed<T>( getter: (oldValue: T | undefined) => T, debuggerOptions?: DebuggerOptions ): Readonly<Ref<Readonly<T>>>
function computed<T>( options: { get: (oldValue: T | undefined) => T set: (value: T) => void }, debuggerOptions?: DebuggerOptions ): Ref<T>
|
示例1:传入getter函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <script setup lang="ts"> import { ref, computed } from 'vue'
const firstName = ref('Takumi') const lastName = ref('Kisugi')
// 定义一个计算属性 const fullName = computed( () => { // 相当于getter函数 console.log('计算 fullName...') return `${firstName.value} ${lastName.value}` } )
fullName.value = "Joe Dan" // 错误,返回值只读 </script>
<template> <div> <p>名:{{ firstName }}</p> <p>姓:{{ lastName }}</p> <p>全名:{{ fullName }}</p> </div> </template>
|
第一次访问 fullName 时,会执行 console.log('计算 fullName...'),不访问则不执行(惰性)。
之后如果 firstName 或 lastName 没变,再访问 fullName 时不会重新计算(缓存生效)。
当 firstName 或 lastName 更新时,fullName 会自动重新计算。
示例2:传入get和set函数
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 32 33
| <script setup lang="ts"> import { ref, computed } from 'vue'
const firstName = ref('Hongyi') const lastName = ref('Zeng')
// 带 get和set 的计算属性 const fullName = computed({ get() { return `${firstName.value} ${lastName.value}` }, set(newValue: string) { const parts = newValue.split(' ') firstName.value = parts[0] || '' lastName.value = parts[1] || '' } })
// 修改 fullName 也会影响 firstName / lastName const updateName = () => { fullName.value = 'John Doe' } </script>
<template> <div> <p>名字:{{ firstName }}</p> <p>姓氏:{{ lastName }}</p> <p>全名:{{ fullName }}</p> <button @click="updateName">修改 fullName</button> </div> </template>
|
示例3:配合模板使用
模板中可以直接使用计算属性,无需 .value:
1 2 3 4 5 6 7 8 9 10 11
| <template> <p>{{ doubleCount }}</p> </template>
<script setup lang="ts"> import { ref, computed } from 'vue'
const count = ref(10) const doubleCount = computed(() => count.value * 2) </script>
|
③ reactive()
详见:[[Vue3学习笔记#2.2.4 reactive]]
④ watchEffect()
watchEffect() 是一个立即执行的响应式副作用函数。 当它内部访问到的任意响应式数据(ref、reactive、computed)发生变化时,它会自动重新运行。
函数类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function watchEffect( effect: (onCleanup: OnCleanup) => void, options?: WatchEffectOptions ): WatchHandle
type OnCleanup = (cleanupFn: () => void) => void
interface WatchEffectOptions { flush?: 'pre' | 'post' | 'sync' onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void }
interface WatchHandle { (): void pause: () => void resume: () => void stop: () => void }
|
示例1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup lang="ts"> import { ref, watchEffect } from 'vue'
const count = ref(0)
// 自动追踪依赖 count watchEffect(() => { console.log('count 变化了:', count.value) })
// 改变 count setTimeout(() => { count.value++ }, 1000) </script>
|
输出结果:
1 2
| count 变化了: 0 ← 初始化时立即执行一次 count 变化了: 1 ← 1 秒后再次执行
|
示例2:自动计算并触发异步请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup lang="ts"> import { ref, watchEffect } from 'vue'
const keyword = ref('vue')
watchEffect(async () => { if (!keyword.value) return const res = await fetch(`https://api.example.com/search?q=${keyword.value}`) console.log('搜索结果:', await res.json()) }) </script>
<template> <input v-model="keyword" placeholder="输入搜索关键字" /> </template>
|
⑤ 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 32 33 34 35 36 37 38 39 40 41 42 43
| function watch<T>( source: WatchSource<T>, callback: WatchCallback<T>, options?: WatchOptions ): WatchHandle
function watch<T>( sources: WatchSource<T>[], callback: WatchCallback<T[]>, options?: WatchOptions ): WatchHandle
type WatchCallback<T> = ( value: T, oldValue: T, onCleanup: (cleanupFn: () => void) => void ) => void
type WatchSource<T> = | Ref<T> | (() => T) | (T extends object ? T : never)
interface WatchOptions extends WatchEffectOptions { immediate?: boolean deep?: boolean | number flush?: 'pre' | 'post' | 'sync' onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void once?: boolean }
interface WatchHandle { (): void pause: () => void resume: () => void stop: () => void }
|
示例1:基本用法
单个侦听源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup lang="ts"> import { ref, watch } from 'vue'
const count = ref(0)
// 监听单个 ref watch(count, (newVal, oldVal) => { console.log(`count 从 ${oldVal} → ${newVal}`) })
// 模拟更新 setTimeout(() => { count.value++ }, 1000) </script>
|
多个侦听源:
1 2 3 4 5 6 7 8 9
| <script setup lang="ts"> import { ref, watch } from 'vue' const firstName = ref('Takumi') const lastName = ref('Kisugi')
watch([firstName, lastName], ([newF, newL], [oldF, oldL]) => { console.log(`新值: ${newF} ${newL}`) }) </script>
|
示例2:侦听选项
1 2 3 4 5 6 7 8 9
| <script setup lang="ts"> import { reactive, watch } from 'vue'
const user = reactive({ name: 'Tom', age: 20 })
watch(user, (newVal) => { console.log('user 内部属性变化:', newVal) }, { deep: true }) // 修改 `user.age++` 会触发 </script>
|
1 2 3
| watch(count, (newVal) => { console.log('立即执行:', newVal) }, { immediate: true })
|
1 2 3 4 5 6
| watch(keyword, async (newVal, oldVal, onInvalidate) => { const controller = new AbortController() fetch(`/api/search?q=${newVal}`, { signal: controller.signal })
onInvalidate(() => controller.abort()) })
|
computed,watch和watchEffect对比
| 对比维度 |
computed() |
watch() |
watchEffect() |
| 用途 |
派生数据 |
精确监听数据变化 |
自动执行响应式副作用 |
| 依赖追踪方式 |
自动 |
手动指定 |
自动 |
| 是否立即执行 |
否(惰性) |
否(可配置immediate) |
是 |
| 是否缓存结果 |
✅ 有缓存 |
❌ 无缓存 |
❌ 无缓存 |
| 返回值 |
有(Ref) |
无 |
无 |
| 访问新旧值 |
❌ 否 |
✅ 可以 |
❌ 不可以 |
| 典型场景 |
模板绑定、数据派生 |
API 请求、日志、状态监控 |
自动响应逻辑、调试输出 |
| 是否清理副作用 |
不适用 |
✅ onInvalidate |
✅ onInvalidate |
| 是否返回停止函数 |
❌ 否 |
✅ 是 |
✅ 是 |
| 场景 |
推荐使用 |
| 需要显示在模板中 |
computed() |
| 需要在值变化时执行操作(API 请求、打印等) |
watch() |
| 只想自动执行一些逻辑,无需指定依赖 |
watchEffect() |
9.3.3 响应式API:工具
暂略
9.3.4 响应式API:进阶
① shallowRef()
shallowRef() 是一个 浅层响应式引用(shallow reactive reference),只追踪 ref 自身的 .value 变化,而不对 .value 内部的对象进行深层响应式转换。
shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。
示例
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
| <script setup lang="ts"> import { shallowRef } from 'vue'
const user = shallowRef({ name: 'Takumi', age: 25 })
function updateName() { user.value.name = 'Hongyi' // ❌ 不会触发视图更新 }
function replaceUser() { user.value = { name: 'Hongyi', age: 25 } // ✅ 会触发视图更新(因为 .value 本身被替换) } </script>
<template> <div> <p>名字:{{ user.name }}</p> <button @click="updateName">修改名字(无效)</button> <button @click="replaceUser">替换对象(有效)</button> </div> </template>
|
② triggerRef()
当使用 shallowRef() 且修改了内部对象(但 .value 没换), 可以通过 **手动触发更新triggerRef()**:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <script setup lang="ts"> import { shallowRef, triggerRef } from 'vue'
const user = shallowRef({ name: 'Takumi', age: 25 })
function updateName() { user.value.name = 'Hongyi' triggerRef(user) // ✅ 手动触发视图更新 } </script>
<template> <div> <p>{{ user.name }}</p> <button @click="updateName">更新名字</button> </div> </template>
|
9.3.5 生命周期钩子
详见:[[Vue3学习笔记#2.9 生命周期]]
9.4 其他
9.4.1 TypeScript工具类型
① PropType<T>
用于在用运行时 props 声明时给一个 prop 标注更复杂的类型定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import type { PropType } from 'vue'
interface Book { title: string author: string year: number }
export default { props: { book: { type: Object as PropType<Book>, required: true } } }
|
② CSSProperties
用于扩展在样式属性绑定上允许的值的类型。
1 2 3 4 5
| declare module 'vue' { interface CSSProperties { [key: `--${string}`]: string } }
|
1
| <div :style="{ '--bg-color': 'blue' }"></div>
|