系列文章目录
文章目录
- 系列文章目录
- 前言
- [一、 创建一个应用](#一、 创建一个应用)
-
- [1. 核心知识点总结](#1. 核心知识点总结)
-
- [1.1 应用实例的创建](#1.1 应用实例的创建)
- [1.2 根组件与组件树](#1.2 根组件与组件树)
- [1.3 挂载应用 (Mounting)](#1.3 挂载应用 (Mounting))
- [1.4 应用级配置与资源注册](#1.4 应用级配置与资源注册)
- [2. 面试高频问题](#2. 面试高频问题)
-
- [Q1:`createApp` 返回的对象和 `.mount()` 返回的对象有什么不同?](#Q1:
createApp返回的对象和.mount()返回的对象有什么不同?) - [Q2:什么是"DOM 内模板"?](#Q2:什么是“DOM 内模板”?)
- [Q3:为什么 Vue 允许在一个页面创建多个应用实例?](#Q3:为什么 Vue 允许在一个页面创建多个应用实例?)
- [Q1:`createApp` 返回的对象和 `.mount()` 返回的对象有什么不同?](#Q1:
- [3. 开发易错点 (Pitfalls)](#3. 开发易错点 (Pitfalls))
-
- [❌ 易错点 1:挂载顺序错误](#❌ 易错点 1:挂载顺序错误)
- [❌ 易错点 2:在容器元素上使用 Vue 指令](#❌ 易错点 2:在容器元素上使用 Vue 指令)
- [❌ 易错点 3:滥用全局注册](#❌ 易错点 3:滥用全局注册)
- [🛠️ 代码范例](#🛠️ 代码范例)
- [二、 模板语法](#二、 模板语法)
-
- [1. 核心知识点总结](#1. 核心知识点总结)
-
- [1.1 文本插值与原始 HTML](#1.1 文本插值与原始 HTML)
- [1.2 Attribute 绑定 (`v-bind`)](#1.2 Attribute 绑定 (
v-bind)) - [1.3 JavaScript 表达式](#1.3 JavaScript 表达式)
- [1.4 指令 (Directives) 语法结构](#1.4 指令 (Directives) 语法结构)
- [2. 面试高频问题](#2. 面试高频问题)
-
- [Q1:为什么 Vue 模板中只能使用"表达式"而不能使用"语句"?](#Q1:为什么 Vue 模板中只能使用“表达式”而不能使用“语句”?)
- [Q2:`v-html` 的原理是什么?为什么它不能用于组件组合?](#Q2:
v-html的原理是什么?为什么它不能用于组件组合?) - [Q3:动态参数绑定(如 `:[arg]`)在 DOM 内模板中有哪些陷阱?](#Q3:动态参数绑定(如
:[arg])在 DOM 内模板中有哪些陷阱?)
- [3. 开发易错点 (Pitfalls)](#3. 开发易错点 (Pitfalls))
-
- [❌ 易错点 1:在属性绑定中使用双大括号](#❌ 易错点 1:在属性绑定中使用双大括号)
- [❌ 易错点 2:在表达式中产生副作用](#❌ 易错点 2:在表达式中产生副作用)
- [❌ 易错点 3:动态参数语法错误](#❌ 易错点 3:动态参数语法错误)
- [🛠️ 代码实战范例](#🛠️ 代码实战范例)
- 三、响应式基础
-
- [1. 为什么我们需要使用带有 .value 的 ref,而不是普通的变量?](#1. 为什么我们需要使用带有 .value 的 ref,而不是普通的变量?)
-
- [1.1 先理解 Vue 的目标:数据变 → DOM 自动更新](#1.1 先理解 Vue 的目标:数据变 → DOM 自动更新)
- [1.2 如果用普通变量会怎样?](#1.2 如果用普通变量会怎样?)
- [1.3 Vue 如何解决?------对象 getter / setter](#1.3 Vue 如何解决?——对象 getter / setter)
- [2. ref 的第二个好处:可传递响应式引用](#2. ref 的第二个好处:可传递响应式引用)
- [3. 深层响应性](#3. 深层响应性)
- [4. DOM 更新时机](#4. DOM 更新时机)
-
- [4.1 理解 Vue 怎么评判"时间到了"。更新Dom](#4.1 理解 Vue 怎么评判“时间到了”。更新Dom)
- [5 reactive()](#5 reactive())
-
- [5.1 Reactive Proxy vs. Original](#5.1 Reactive Proxy vs. Original)
- [5.2 reactive() 的局限性](#5.2 reactive() 的局限性)
- [5.3 ✨ "Vue 的响应式跟踪是通过属性访问实现的"是什么意思?](#5.3 ✨ “Vue 的响应式跟踪是通过属性访问实现的”是什么意思?)
- [5.4 ✨ref 传对象不是也会用 reactive 吗?那为什么 Vue 推荐用 ref?](#5.4 ✨ref 传对象不是也会用 reactive 吗?那为什么 Vue 推荐用 ref?)
- [5.5 ✨Vue.js 响应式依赖是"绑定在谁身上的"。](#5.5 ✨Vue.js 响应式依赖是“绑定在谁身上的”。)
- [6 额外的 ref 解包细节](#6 额外的 ref 解包细节)
-
- [6.1 Vue 3 的 ref() 底层主要不是用 Proxy,也不是单纯用 Object.defineProperty,而是使用了一个简单的 JavaScript Class(类)配合 getter/setter 拦截。](#6.1 Vue 3 的 ref() 底层主要不是用 Proxy,也不是单纯用 Object.defineProperty,而是使用了一个简单的 JavaScript Class(类)配合 getter/setter 拦截。)
- [四 计算属性](#四 计算属性)
-
- [4.1 基础示例](#4.1 基础示例)
- [4.2 计算属性缓存 vs 方法](#4.2 计算属性缓存 vs 方法)
- [4.3 可写计算属性](#4.3 可写计算属性)
- [4.4 获取上一个值](#4.4 获取上一个值)
- [4.5 最佳实践](#4.5 最佳实践)
- [五 类与样式绑定](#五 类与样式绑定)
-
- [5.1 绑定 HTML class](#5.1 绑定 HTML class)
-
- [5.1.1 绑定对象](#5.1.1 绑定对象)
- [5.1.2 绑定数组](#5.1.2 绑定数组)
- [5.2 在组件上使用](#5.2 在组件上使用)
- [5.3 绑定内联样式](#5.3 绑定内联样式)
-
- [5.3.1 绑定对象](#5.3.1 绑定对象)
- [5.3.2 绑定数组](#5.3.2 绑定数组)
- [六 条件渲染](#六 条件渲染)
-
- [6.1 v-if](#6.1 v-if)
- [6.2 v-show](#6.2 v-show)
- [6.3 v-if vs. v-show](#6.3 v-if vs. v-show)
- [6.3 v-if 和 v-for](#6.3 v-if 和 v-for)
- [七 列表渲染](#七 列表渲染)
-
- [1 v-for 与 v-if](#1 v-for 与 v-if)
- [2 通过 key 管理状态](#2 通过 key 管理状态)
-
- [2.1 ✨ Vue 默认的更新策略:就地更新(in-place patch)](#2.1 ✨ Vue 默认的更新策略:就地更新(in-place patch))
- [2.2 就地更新引发的问题](#2.2 就地更新引发的问题)
- [2.3 key 的作用](#2.3 key 的作用)
- [2.4 为什么不能用 index 作为 key](#2.4 为什么不能用 index 作为 key)
- [2.5 ✨ template v-for 为什么 key 写在 template](#2.5 ✨ template v-for 为什么 key 写在 template)
- [八 事件处理](#八 事件处理)
-
- [8.1 监听事件](#8.1 监听事件)
- [8.2 在内联处理器中调用方法](#8.2 在内联处理器中调用方法)
- [8.3 事件修饰符](#8.3 事件修饰符)
- [8.3 按键修饰符](#8.3 按键修饰符)
- [8.4 鼠标按键修饰符](#8.4 鼠标按键修饰符)
- [九 表单输入绑定](#九 表单输入绑定)
-
- [1. 知识点汇总](#1. 知识点汇总)
-
- [1.1 核心原理](#1.1 核心原理)
- [1.2 基本用法](#1.2 基本用法)
- [1.3 值绑定 (动态值)](#1.3 值绑定 (动态值))
- [1.4 修饰符 (Modifiers)](#1.4 修饰符 (Modifiers))
- [2、 面试考点 (High Frequency)](#2、 面试考点 (High Frequency))
-
- [Q1: v-model 的本质是什么?](#Q1: v-model 的本质是什么?)
- [Q2: 在中文输入法 (IME) 下 v-model 的表现?](#Q2: 在中文输入法 (IME) 下 v-model 的表现?)
- [Q3: 为什么 v-model 会忽略初始的 value/checked/selected 属性?](#Q3: 为什么 v-model 会忽略初始的 value/checked/selected 属性?)
- [Q4: 如何在自定义组件上实现 v-model?](#Q4: 如何在自定义组件上实现 v-model?)
- [十 侦听器](#十 侦听器)
-
- [10.1 ✨ 什么叫"副作用"](#10.1 ✨ 什么叫“副作用”)
- [10.2 侦听数据源类型](#10.2 侦听数据源类型)
-
- [10.2.1 你不能直接侦听响应式对象的属性值](#10.2.1 你不能直接侦听响应式对象的属性值)
- [10.3 深层侦听器](#10.3 深层侦听器)
-
- [10.3.1 为什么 getter 不会深层监听](#10.3.1 为什么 getter 不会深层监听)
- [10.3 即时回调的侦听器](#10.3 即时回调的侦听器)
- [10.4 一次性侦听器](#10.4 一次性侦听器)
- [10.5 watchEffect()](#10.5 watchEffect())
- [10.6 副作用清理 onWatcherCleanup](#10.6 副作用清理 onWatcherCleanup)
- [10.7 回调的触发时机](#10.7 回调的触发时机)
-
- [10.7.1 后置触发 watchPostEffect](#10.7.1 后置触发 watchPostEffect)
- [10.7.2 同步触发](#10.7.2 同步触发)
- [10.8 停止侦听器](#10.8 停止侦听器)
- [十一 模板引用](#十一 模板引用)
-
- [1 访问模板引用 useTemplateRef](#1 访问模板引用 useTemplateRef)
- [2 组件上的 ref](#2 组件上的 ref)
-
- [2.1 Vue 3 组件 Ref 访问权限对比](#2.1 Vue 3 组件 Ref 访问权限对比)
- [3 v-for 中的模板引用](#3 v-for 中的模板引用)
- [4 函数模板引用](#4 函数模板引用)
- [十二 组件基础](#十二 组件基础)
-
- [12.1 传递 props](#12.1 传递 props)
- [12.2 监听事件](#12.2 监听事件)
- [12.3 通过插槽来分配内容](#12.3 通过插槽来分配内容)
- [12.4 动态组件](#12.4 动态组件)
- [12.5 DOM 内模板解析注意事项](#12.5 DOM 内模板解析注意事项)
- [十三 生命周期钩子](#十三 生命周期钩子)
前言
根据vue官网记录的笔记
vue官网链接
一、 创建一个应用
1. 核心知识点总结
1.1 应用实例的创建
每个 Vue 应用都是从 createApp 函数开始的。它会创建一个应用实例,用于管理全局资源(组件、指令、插件)和配置。
-
创建语法 :
javascriptimport { createApp } from 'vue' import App from './App.vue' // 导入根组件 const app = createApp(App)
1.2 根组件与组件树
每个应用都需要一个"根组件",其他组件将作为其子组件嵌套在内,形成一棵组件树。这种层级结构决定了数据流向和事件冒泡的路径。
- App (Root) : 顶层容器,通常是
App.vue。 - 子组件 : 如
Header,TodoList,Footer等,逐层嵌套。
1.3 挂载应用 (Mounting)
应用实例在调用 .mount() 方法之前不会渲染任何内容。
- 参数 :可以是一个实际的 DOM 元素,也可以是一个 CSS 选择器字符串(如
'#app')。 - 渲染位置 :根组件的内容会替换容器元素内部的所有内容。容器元素本身不被视为应用的一部分。
- 返回值 :
.mount()返回的是根组件实例 (即响应式 Proxy 对象),而createApp返回的是应用实例。
1.4 应用级配置与资源注册
应用实例暴露了 .config 对象,并提供了一系列方法来注册全局可用的资源。
| 功能 | 示例代码 | 说明 |
|---|---|---|
| 错误处理 | app.config.errorHandler = (err) => {} |
捕获所有子组件未处理的错误。 |
| 全局组件 | app.component('MyBtn', MyBtn) |
在应用任何地方都能直接使用。 |
| 全局指令 | app.directive('focus', { ... }) |
注册自定义指令。 |
| 安装插件 | app.use(router) |
安装路由、Pinia 等功能库。 |
注意 :确保在调用
.mount()之前完成所有配置!
2. 面试高频问题
Q1:createApp 返回的对象和 .mount() 返回的对象有什么不同?
参考回答:
createApp返回的是 应用实例 (App Instance) 。它包含了应用全局配置的方法(如.use(),.component()),支持链式调用。.mount()返回的是 根组件实例 (Root Component Instance)。它是根组件渲染后的响应式代理(Proxy),常用于在非单文件组件(SFC)环境下访问根数据或进行底层调试。
Q2:什么是"DOM 内模板"?
参考回答 :
当根组件没有显式设置 template 选项且不是通过 SFC 导入时,Vue 会自动使用挂载容器(如 <div id="app">)的 innerHTML 作为模板。这常用于服务端渲染框架(如 PHP, Laravel)生成的初始 HTML 内容,再由 Vue 进行接管。
Q3:为什么 Vue 允许在一个页面创建多个应用实例?
参考回答:
- 局部化增强:如果只想让 Vue 控制页面中的某一个小部件(如侧边栏),而不是整个大型页面,创建多个小型实例更灵活。
- 环境隔离:每个实例拥有独立的全局配置、插件和组件作用域,互不干扰,非常适合在旧项目中逐步迁移。
3. 开发易错点 (Pitfalls)
❌ 易错点 1:挂载顺序错误
- 现象 :先执行了
app.mount('#app'),后注册全局组件或插件。 - 后果:挂载时组件尚未注册,Vue 会抛出"解析组件失败"的警告,插件功能(如路由)也无法生效。
- 规则 :必须在挂载之前完成所有配置和资源注册。
❌ 易错点 2:在容器元素上使用 Vue 指令
- 现象 :
<div id="app" v-if="isAdmin">...</div> - 原因 :容器元素
div#app只是挂载点,它不属于根组件模板。Vue 只接管容器内部的内容,容器上的指令将被忽略。
❌ 易错点 3:滥用全局注册
- 代价 :通过
app.component()全局注册的组件无法被构建工具(如 Vite)进行 Tree-shaking。即使该组件从未被使用,也会被打包进最终文件,增加体积。 - 建议:除非是高频基础组件(如 Button, Icon),否则优先使用局部注册。
🛠️ 代码范例
javascript
import { createApp } from 'vue'
import App from './App.vue'
import MyGlobalComponent from './components/MyGlobalComponent.vue'
// 1. 创建应用实例
const app = createApp(App)
// 2. 全局配置 (必须在 mount 之前)
app.config.errorHandler = (err) => {
console.error('Captured Global Error:', err)
}
// 3. 全局资源注册
app.component('MyGlobalComponent', MyGlobalComponent)
// 4. 挂载应用 (这是最后一步)
app.mount('#app')
二、 模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将组件实例的数据绑定到呈现的 DOM 上。
1. 核心知识点总结
1.1 文本插值与原始 HTML
- 文本插值 :使用"Mustache"语法(双大括号
{``{ }})。它是响应式的,会将数据解释为纯文本。 - 原始 HTML :使用
v-html指令。- 作用 :将元素的
innerHTML与组件属性保持同步。 - ⚠️ 安全警告 :仅在内容安全可信时使用,永远不要 对用户提供的内容使用
v-html,以防 XSS 攻击。
- 作用 :将元素的
1.2 Attribute 绑定 (v-bind)
- 基本语法 :
v-bind:id="dynamicId",简写为:id="dynamicId"。 - 同名简写 (Vue 3.4+) :如果变量名与属性名相同,可简写为
:id。 - 布尔型 Attribute :依据真假值决定属性是否存在(如
:disabled)。若值为真值或空字符串,属性存在;若为假值,属性移除。 - 动态绑定多个值:使用包含多个 attribute 的 JavaScript 对象,批量绑定对象中的所有属性。
1.3 JavaScript 表达式
Vue 支持在所有数据绑定中编写 JavaScript 表达式。
- 要求 :必须是单一表达式 (即可以合法地写在
return后面的代码)。 - 调用函数:绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。
- 受限访问 :表达式只能访问内置全局对象白名单(如
Math,Date)。
1.4 指令 (Directives) 语法结构
指令是带有 v- 前缀的特殊属性。其完整语法如下:
- 参数 (Arguments) :冒号后的内容(如
:href,@click)。 - 动态参数 :用方括号包裹,如
:[attributeName]="url"。值需为 字符串 或null。 - 修饰符 (Modifiers) :点号后的后缀,用于指出指令以特殊方式绑定(如
@submit.prevent)。
2. 面试高频问题
Q1:为什么 Vue 模板中只能使用"表达式"而不能使用"语句"?
参考回答 :
Vue 的模板会被编译成渲染函数。在 JavaScript 中,表达式(如三元运算)会产生一个具体的值,可以直接嵌入到执行流中;而语句(如 if 分支、for 循环、变量声明)不产生值,无法作为渲染函数返回结果的一部分。
Q2:v-html 的原理是什么?为什么它不能用于组件组合?
参考回答 :
v-html 通过设置 DOM 的 innerHTML 来更新内容。它绕过了 Vue 的虚拟 DOM 差异算法(Diff),且内容不会被 Vue 编译器解析。因此,在 v-html 中写的 Vue 指令或组件标签是不会生效的。Vue 提倡使用组件作为 UI 重用的基本单元。
Q3:动态参数绑定(如 :[arg])在 DOM 内模板中有哪些陷阱?
参考回答 :
浏览器解析 HTML 时会将属性名强制转为小写。在非单文件组件(SFC)的环境下,:[someAttr] 会被解析为 :[someattr]。如果 JS 中的变量名为驼峰式 someAttr,绑定将失效。
3. 开发易错点 (Pitfalls)
❌ 易错点 1:在属性绑定中使用双大括号
- 错误 :
<div id="{``{ id }}"></div> - 原因 :双大括号仅用于文本插值。属性绑定必须使用
v-bind或:。
❌ 易错点 2:在表达式中产生副作用
- 现象 :在
{``{ }}中调用一个会修改数据的函数。 - 后果 :修改数据 -> 触发重新渲染 -> 再次调用函数 -> 修改数据......导致死循环。
❌ 易错点 3:动态参数语法错误
- 现象 :
<a :['foo' + bar]="value"></a> - 原因 :动态参数表达式中不能包含空格或引号,这些在 HTML 属性名称中是非法的。推荐使用计算属性替代复杂表达式。
🛠️ 代码实战范例
javascript
<script setup>
const msg = "Hello Vue!"
const rawHtml = '<span style="color: red">这是红色文字</span>'
const dynamicId = "container-01"
const attributeName = "href"
const url = "[https://vuejs.org](https://vuejs.org)"
const onSubmit = () => {
alert("表单已提交,且阻止了默认刷新行为")
}
</script>
<template>
<p>{{ msg.split('').reverse().join('') }}</p>
<div v-html="rawHtml"></div>
<div :id="dynamicId">绑定 ID</div>
<div :dynamicId>Vue 3.4+ 同名简写</div>
<a :[attributeName]="url">官网链接</a>
<form @submit.prevent="onSubmit">
<button type="submit">提交并阻止默认行为</button>
</form>
</template>
三、响应式基础
1. 为什么我们需要使用带有 .value 的 ref,而不是普通的变量?
1.1 先理解 Vue 的目标:数据变 → DOM 自动更新
javascript
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">
{{ count }}
</button>
</template>
点击按钮:DOM 会自动更新。
Vue 做了三件事:
- 记录谁在使用 count
- count 变化
- 通知这些地方重新渲染
这就是:依赖收集 → 触发更新
1.2 如果用普通变量会怎样?
- JavaScript 本身:不能监听普通变量的变化
- 实际上,JavaScript 监听的是"对属性的操作",而不是对象这个"死物"本身。
目前主流有两种方式来实现这种监听:
A. 早期方案:Object.defineProperty (Vue 2 使用)
-
这种方式监听的是具体的属性。
-
原理: 它通过为一个已存在的属性定义 getter 和 setter。
-
局限:
- 你必须知道属性名。如果后来给对象新增了一个属性,它监听不到(这就是为什么 Vue 2 有 Vue.set)。
- 无法监听数组下标的变化。
B. 现代方案:Proxy (Vue 3 使用)
这种方式监听的是整个对象的交互过程。
- 原理: 它在对象外层套了一个"代理"。不管是读取属性、修改属性、删除属性,甚至是遍历对象,都会经过这个代理。
1.3 Vue 如何解决?------对象 getter / setter
JavaScript 可以监听 对象属性:
javascript
const obj = {
get value() {
console.log("读取")
},
set value(v) {
console.log("修改")
}
}
所以 Vue 想到一个办法:把变量变成对象属性。
2. ref 的第二个好处:可传递响应式引用
ref 可以传递给函数,同时保留响应式连接
javascript
function useAdd(counter) {
counter.value++
}
const count = ref(0)
useAdd(count)
count 会更新。因为传递的是:ref 对象,而不是值。
3. 深层响应性
- Ref 会使它的值具有 深层响应性 。这意味着即使改 变嵌套对象或数组时,变化也会被检测到:
- 也可以通过 shallowRef 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。
4. DOM 更新时机
- 当你修改了 响应式状态 时,DOM 会被 自动 更新。
- 但是需要注意的是,DOM 更新不是同步的。
- Vue 会在"next tick "更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
4.1 理解 Vue 怎么评判"时间到了"。更新Dom
浏览器底层的 事件循环 (Event Loop) 和 Vue 的异步更新队列。
Vue 并不是通过"计时器"(比如等待几毫秒)来评判的,而是利用了 JavaScript 执行机制中的 微任务 (Microtask)。
(1)核心机制:异步缓冲
Vue 的更新逻辑遵循一个原则:"攒一波大的,再一起动手。"
当你修改 count.value++ 时,Vue 并不会立即去改 DOM。相反,它执行了以下操作:
-
标记: 把当前受影响的组件(Watcher/Effect)丢进一个名为 queue 的全局队列里。
-
去重: 如果你在同一个函数里连着写了 100 次 count.value++,Vue 只会将该组件在队列里存一次。
-
预约 : 向浏览器申请一个"微任务",说:"等现在的同步代码执行完了,请立刻执行我的更新任务。"
(2)评判标准:微任务 (Microtask)
Vue 评判"时间到了"的依据是:当前的同步任务执行完毕,微任务队列开始清空。
在 JavaScript 的事件循环中,任务分为两种:
-
宏任务 (Macrotask): 整个脚本执行、setTimeout、用户交互事件(点击、滚动)。
-
微任务 (Microtask): Promise.then、MutationObserver。
Vue 的做法是:
- 当状态改变时,Vue 会通过 Promise.resolve().then(flushJobs) 开启一个微任务。
- 由于微任务会在当前同步逻辑(即你写的那个函数里的所有代码)运行完之后、浏览器渲染 DOM 之前 立即执行。
(3) nextTick 的本质
为什么 await nextTick() 之后就能拿到最新的 DOM?
因为 nextTick 本身就是一个 Promise。
javascript
// nextTick 的极简伪代码实现
function nextTick(fn) {
const p = currentFlushPromise || Promise.resolve()
return fn ? p.then(fn) : p
}
当你调用 nextTick 时,你实际上是在 Vue 已经预约好的那个"更新任务(微任务)"后面,又排了一个新的微任务。
-
微任务 1: Vue 的 DOM 更新任务(由状态改变触发)。
-
微任务 2: 你的 nextTick 回调。
根据队列"先进先出"的原则,当你的代码执行到微任务 2 时,微任务 1 肯定已经执行完了,所以你拿到的 DOM 是最新的。
5 reactive()
- reactive 让对象本身变成响应式
- 底层实现是 Proxy
- reactive 是深层响应式
- shallowReactive 可以关闭深层响应
5.1 Reactive Proxy vs. Original
- reactive 返回的是 Proxy,而不是原对象
- 只有 代理对象是响应式 的,更改原始对象不会触发更新
- 对 同一个 原始对象调用 reactive() 会总是返回同样的代理对象。
- 而对一个已存在的代理对象调用 reactive() 会 返回其本身
- 这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理
5.2 reactive() 的局限性
- 有限的值类型:它只能用于 对象类型
- 不能替换整个对象:由于 Vue 的 响应式跟踪 是通过 属性访问 实现的,因此我们必须始终保持对响应式对象的相同引用。
- 对解构操作不友好
5.3 ✨ "Vue 的响应式跟踪是通过属性访问实现的"是什么意思?
这句话的意思是:Vue 只能在你访问对象属性时收集依赖,因为 reactive() 的底层是 Proxy。
javascript
function reactive(target) {
return new Proxy(target, {
get(obj, key) {
track(obj, key) // 依赖收集
return obj[key]
},
set(obj, key, value) {
obj[key] = value
trigger(obj, key) // 触发更新
return true
}
})
}
所以 Vue 的响应式依赖是这样建立的:
- 读取属性 → track(依赖收集)
- 修改属性 → trigger(触发更新)
举个例子:
javascript
const state = reactive({ count: 0 })
console.log(state.count)
执行流程:
javascript
读取 state.count
↓
触发 Proxy.get
↓
track(state, "count")
↓
记录依赖
当修改:
javascript
Proxy.set
↓
trigger(state, "count")
↓
通知组件更新
所以 Vue 文档说:响应式跟踪是通过属性访问实现的
意思就是:必须访问属性,Vue 才能知道依赖关系
5.4 ✨ref 传对象不是也会用 reactive 吗?那为什么 Vue 推荐用 ref?
(1) reactive 不能替换对象,ref 可以替换
javascript
const state = ref({ count: 0 })
state.value = { count: 1 }
因为:Proxy 一直是 ref.value,响应式链不会断。
(2)ref 对解构更友好,Vue 提供:toRef、toRefs
5.5 ✨Vue.js 响应式依赖是"绑定在谁身上的"。
- reactive:依赖绑定在 Proxy(target) + key 上,而不是原始对象 target 上
- ref:依赖绑定在 ref 这个容器上
6 额外的 ref 解包细节
- 一个 ref 会在作为 响应式对象的属性被访问或修改时 自动解包。
- 当 ref 作为响应式数组或原生集合类型 (如 Map) 中的元素被访问时,它不会被解包
| 特性 | ref() |
reactive() |
|---|---|---|
| 支持的数据类型 | 所有类型(基本类型 + 对象/数组) | 仅限引用类型(Object, Array, Map, Set) |
| 底层实现 | RefImpl 类(getter/setter) |
ES6 Proxy |
| 访问方式 (JavaScript) | 必须使用 .value |
直接访问属性 |
| 访问方式 (Template) | 自动解包(顶级属性无需 .value) | 直接访问属性 |
| 整体替换对象 | 支持 (修改 .value 即可) |
不支持(直接赋值会导致响应性丢失) |
| 解构操作 | 不支持直接解构(需配合 toRefs) |
不支持直接解构(原始类型属性会丢失响应性) |
| 官方推荐 | 首选方案(更通用且语义明确) | 仅在处理复杂、高度关联的对象时使用 |
6.1 Vue 3 的 ref() 底层主要不是用 Proxy,也不是单纯用 Object.defineProperty,而是使用了一个简单的 JavaScript Class(类)配合 getter/setter 拦截。
(1)ref() 的真实底层实现
当你调用 const count = ref(0) 时,Vue 内部创建了一个名为 RefImpl(Ref Implementation)的类实例。
javascript
// 简化版的 Vue 3 内部实现逻辑
class RefImpl {
constructor(value) {
this._value = value;
// 标记:这是一个 ref 对象,模板解包时会用到
this.__v_isRef = true;
}
get value() {
// 1. 依赖收集:记录是谁在用我
track(this, 'value');
return this._value;
}
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = newVal;
// 2. 派发更新:通知大家我变了
trigger(this, 'value');
}
}
}
关键点:
-
它用的是原生 Class 的 getter/setter。这在语法表现上和 Object.defineProperty 有点像,但它不需要动态去定义属性,而是在类定义时就确定了。
-
它不直接用 Proxy。因为 Proxy 是用来代理对象 的,而 ref 往往需要处理原始值(如数字、字符串)。在 JS 中,你无法给数字 0 开启 Proxy 代理,所以必须用一个对象(即 RefImpl 实例)把它包起来。
(2)为什么大家总提到 Proxy?
这是因为 ref 在处理复杂对象时,会通过"外包"给 reactive 来工作。
-
如果你传的是原始值(如 ref(0)):它只用到上面的 RefImpl 类逻辑,不涉及 Proxy。
-
如果你传的是对象(如 ref({ name: 'Vue' })):
-
RefImpl 会接收这个对象。
-
内部会调用 toReactive() 函数。
-
toReactive 发现这是一个对象,就会调用 reactive()。
-
此时,.value 实际上指向的是一个由 Proxy 实现的响应式代理。
-
(3)为什么不全用 Proxy?
面试中可能会问:既然 Vue 3 号称全面拥抱 Proxy,为什么 ref 还要自己写类拦截?
-
性能更好:对于简单的值包装,Class 的 getter/setter 比起创建一个完整的 Proxy 代理对象要轻量得多,内存占用更小,初始化更快。
-
解决原始值问题:如前所述,Proxy 只能代理对象。
-
结构统一:无论你存什么,ref 暴露出来的总是一个拥有 .value 属性的对象,这种确定性让 Vue 的模板编译器(Compiler)可以非常方便地做自动化处理。
四 计算属性
4.1 基础示例
- computed() 方法期望接收一个
getter函数,返回值为一个 计算属性 ref。 - Vue 的计算属性会 自动追踪响应式依赖。
4.2 计算属性缓存 vs 方法
- 计算属性值会基于其响应式依赖被缓存,一个计算属性仅会在其响应式依赖更新时才重新计算。
- 方法调用总是会在重渲染发生时再次执行函数。
4.3 可写计算属性
- 计算属性 默认是只读 的。只在某些特殊场景中你可能才需要用到"可写"的属性,你可以通过同时提供 getter 和 setter 来创建。
4.4 获取上一个值
- 可以通过访问计算属性的 getter 的第一个参数来获取计算属性返回的上一个值
4.5 最佳实践
- Getter 不应有副作用:不要改变其他状态、在 getter 中做异步请求或者更改 DOM!
- 避免直接修改计算属性值,可以把它看作是一个"临时快照"。
五 类与样式绑定
5.1 绑定 HTML class
5.1.1 绑定对象
- 绑定一个对象:键名是类名,属性值是布尔值
javascript
<div :class="{ active: isActive }"></div>
- 多类绑定示例
javascript
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
- 也可以绑定一个返回对象的计算属性
javascript
const isActive = ref(true)
const error = ref(null)
const classObject = computed(() => ({
active: isActive.value && !error.value,
'text-danger': error.value && error.value.type === 'fatal'
}))
<div :class="classObject"></div>
5.1.2 绑定数组
javascript
<div :class="[isActive ? activeClass : '', errorClass]"></div>
5.2 在组件上使用
- 对于 只有一个根元素 的组件,当你使用了 class attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。
- 如果你的组件有多个根元素 ,你将需要指定 哪个根元素来接收这个 class。你可以通过组件的 $attrs 属性来指定接收的元素
5.3 绑定内联样式
5.3.1 绑定对象
javascript
<div :style="{ 'font-size': fontSize + 'px' }"></div>
5.3.2 绑定数组
javascript
<div :style="[baseStyles, overridingStyles]"></div>
六 条件渲染
6.1 v-if
- v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时 才被渲染。
- 因为 v-if 是一个指令,他必须依附于某个元素。但如果我们想要切换不止一个元素呢?在这种情况下我们可以在一个
<template>元素上使用 v-if
6.2 v-show
- v-show 会在 DOM 渲染中 保留该元素 ;v-show 仅切换了该元素上名为
display的 CSS 属性。
6.3 v-if vs. v-show
-
v-if 是"真实的"按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件 都会被销毁与重建。
-
v-if 也是 惰性 的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
-
v-show 简单许多,元素无论初始条件如何,始终会被渲染 ,只有 CSS display 属性会被切换。
总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。
6.3 v-if 和 v-for
- 当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行。
七 列表渲染
- v-for 指令基于一个数组来渲染一个列表
- 你也可以使用 v-for 来遍历一个对象的所有属性
- v-for 可以直接接受一个整数值。
- 你也可以在
<template>标签上使用 v-for 来渲染一个包含多个元素的块。
1 v-for 与 v-if
- 当它们同时存在于一个节点上时,v-if 比 v-for 的优先级更高。
- 这意味着 v-if 的条件将 无法访问 到 v-for 作用域内定义的变量别名
- 在外先包装一层
<template>再在其上使用 v-for 可以解决这个问题
2 通过 key 管理状态
2.1 ✨ Vue 默认的更新策略:就地更新(in-place patch)
javascript
items = ['A', 'B', 'C']
<li v-for="item in items">
{{ item }}
</li>
DOM:
javascript
li0 A
li1 B
li2 C
如果数据变成:['C', 'B', 'A'],Vue 默认不会移动 DOM,而是直接修改内容
javascript
li0 C
li1 B
li2 A
也就是说:DOM位置不变,只修改内容
这就叫:就地更新(in-place patch)
- 优点:非常快。因为 不用移动 DOM
2.2 就地更新引发的问题
如果你的 DOM 有状态,就会出问题。会产生 状态错乱
javascript
<div v-for="item in items">
<input :value="item">
</div>
2.3 key 的作用
- Vue 就会知道:每个节点是谁
- Vue会:移动 DOM,而不是只修改内容
- key 是一个:VNode 的唯一标识
- Vue diff 算法会用它判断:旧节点 ↔ 新节点,是否是同一个
- 简化理解:key 相同 → 复用节点,key 不同 → 创建新节点
2.4 为什么不能用 index 作为 key
- 如果数组发生:头部插入、删除、排序,index 就变了。
- Vue就会认为:节点不同
2.5 ✨ template v-for 为什么 key 写在 template
- 原因:template 本身不会渲染成 DOM。它只是逻辑容器。
- 所以:key 必须挂在 template。Vue才能知道这一组 DOM 属于哪个循环项。
默认 v-for:
- 不移动DOM
- 只更新内容
加 key:
- Vue可以识别节点
- 正确复用 / 移动 DOM
作用:
- 避免状态错乱
- 提高 diff 准确性
八 事件处理
8.1 监听事件
- 内联事件处理器
- 方法事件处理器
方法与内联事件判断:
- foo、foo.bar 和 foo['bar'] 会被视为方法事件处理器
- 而 foo() 和 count++ 会被视为内联事件处理器。
8.2 在内联处理器中调用方法
- 除了直接绑定方法名,你还可以在内联事件处理器中调用方法。
- 这允许我们向方法 传入自定义参数 以代替原生事件:
有时我们需要在内联事件处理器中访问原生 DOM 事件。
- 你可以向该处理器方法传入一个特殊的
$event变量, - 或者使用 内联箭头函数
8.3 事件修饰符
javascript
<!-- 单击事件将停止传递 -->
<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>
8.3 按键修饰符
- Vue 为一些常用的按键提供了别名
8.4 鼠标按键修饰符
- .left
- .right
- .middle
九 表单输入绑定
v-model 是 Vue 提供的一个指令,用于在表单输入元素和 JavaScript 状态之间建立双向绑定。它不仅简化了代码,还自动处理了不同输入类型下的 DOM 属性映射与事件监听。
1. 知识点汇总
1.1 核心原理
v-model 是一个语法糖,它根据元素类型自动切换绑定的属性和监听的事件:
- 文本 (
input/textarea) : 绑定value属性,监听input事件。 - 复选框/单选框 (
checkbox/radio) : 绑定checked属性,监听change事件。 - 选择器 (
select) : 绑定value属性,监听change事件。
1.2 基本用法
- 文本: 实时同步输入值。
- 多行文本 (
textarea) : 不支持插值{``{ text }},必须使用v-model。 - 复选框 :
- 单一复选框:绑定布尔值。
- 多个复选框:绑定到同一个数组 ,自动收集选中项的
value。
- 单选按钮 : 绑定到同一变量,选中项的
value即为变量值。 - 选择器 :
- 单选:绑定到字符串。
- 多选:添加
multiple属性,绑定到数组。
1.3 值绑定 (动态值)
通过 v-bind 可以绑定非字符串类型的值(如对象、数字):
- 复选框自定义值 : 使用
true-value和false-value指定选中与未选中时的值。 - 动态解构 :
<option :value="{ id: 123 }">,选中后变量将获得该对象。
1.4 修饰符 (Modifiers)
.lazy: 转为在change事件(失焦或回车)时同步,而非input。.number: 自动将输入转为数字类型(调用parseFloat)。.trim: 自动过滤首尾空格。
2、 面试考点 (High Frequency)
Q1: v-model 的本质是什么?
答 : 它是一个语法糖。在底层,对于原生 input,它等价于:
<input :value="text" @input="text = $event.target.value">。
Q2: 在中文输入法 (IME) 下 v-model 的表现?
答 : 默认情况下,v-model 不会 在 IME 拼字阶段触发更新,只有当字符正式落入输入框时才会同步数据。如果需要拼字阶段也实时同步,需手动绑定 :value 和 @input。
Q3: 为什么 v-model 会忽略初始的 value/checked/selected 属性?
答 : 因为 Vue 的响应式系统遵循"数据驱动"原则。它认为 JavaScript 中的状态(Data/Ref)是唯一的真理来源(Single Source of Truth),因此会用 JS 的初始值覆盖 HTML 标签上的初始属性。
Q4: 如何在自定义组件上实现 v-model?
答 : 在 Vue 3 中,组件上的 v-model 默认利用 modelValue 作为 prop 和 update:modelValue 作为事件。
十 侦听器
watch 的作用是:
- 监听某个 响应式数据源
- 当它变化时执行回调
10.1 ✨ 什么叫"副作用"
- 副作用 = 函数除了返回值之外,还对外部世界产生了影响
- 纯函数:不会改变外部任何东西。这种函数叫 纯函数(Pure Function),没有副作用。
如果函数做了这些事情,就是副作用:
| 行为 | 为什么是副作用 |
|---|---|
| 修改 DOM | 改变页面 |
| 修改全局变量 | 改变外部状态 |
| 发请求 | 与外部系统交互 |
| console.log | 产生外部输出 |
| 操作 localStorage | 修改浏览器存储 |
| setTimeout | 创建异步行为 |
Vue 官方建议:
- computed → 用来计算衍生值
- watch → 用来处理副作用
10.2 侦听数据源类型
watch 的第一个参数可以是不同形式的"数据源":
- 它可以是一个 ref (包括计算属性)、
- 一个响应式对象、
- 一个 getter 函数:Vue会执行这个函数,并 自动收集依赖
- 或多个数据源组成的数组:
10.2.1 你不能直接侦听响应式对象的属性值
这里需要用一个返回该属性的 getter 函数:
javascript
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
javascript
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
10.3 深层侦听器
- 直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器------该回调函数在所有嵌套的变更时都会被触发:
javascript
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})
obj.count++
- 一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调
javascript
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)
10.3.1 为什么 getter 不会深层监听
- 因为这里监听的是:state.someObject 这个 引用。替换对象时才触发。
- 你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器
10.3 即时回调的侦听器
- watch 默认是懒执行的:仅当数据源变化时,才会执行回调。
- 我们希望在创建侦听器时,立即执行一遍回调。我们可以通过传入 immediate: true
10.4 一次性侦听器
- 每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true 选项。
10.5 watchEffect()
-
watchEffect() 在创建时会立即执行一次。
-
允许我们 自动追踪 响应式依赖。消除手动维护依赖列表的负担。
-
侦听一个 嵌套数据结构中的几个属性 :watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
-
watchEffect 仅会在其同步执行期间,才追踪依赖。 在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖收集 | 手动指定 | 自动收集 |
| 回调触发 | 只有 source 改变 | 依赖变化触发 |
| 初始化执行 | 默认不执行,需要 immediate |
创建时立即执行 |
| 异步依赖 | 不会自动追踪副作用 | await 后的依赖不会被追踪 |
| 适合场景 | 精确控制、副作用依赖单一 | 自动依赖收集、多依赖、简洁副作用 |
10.6 副作用清理 onWatcherCleanup
-
onWatcherCleanup 仅在 Vue 3.5+ 中支持,并且必须在 watchEffect 效果函数或 watch 回调函数的同步执行期间调用:你不能在异步函数的 await 语句之后调用它。
-
作为替代,onCleanup 函数还作为第三个参数传递给侦听器回调,以及 watchEffect 作用函数的第一个参数
10.7 回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
-
默认 flush: 'pre'。
-
回调触发顺序:
- 响应式状态变化
- 父组件更新(如有)
- 执行 watch/watchEffect 回调
- Vue 更新所属组件 DOM
✅ 结论:
-
默认回调在 DOM 更新前 执行。
-
如果回调中访问 DOM,会拿到旧的 DOM。
10.7.1 后置触发 watchPostEffect
javascript
watch(source, callback, { flush: 'post' })
watchEffect(callback, { flush: 'post' })
或者直接用别名:
javascript
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
// 此时 Vue 已经更新了组件的 DOM
})
回调触发顺序:
- 响应式状态变化
- Vue 更新组件 DOM
- 执行 watch/watchEffect 回调
✅ 适用场景:
- 你需要访问更新后的 DOM,比如测量元素尺寸、操作滚动条、动画等。
10.7.2 同步触发
javascript
watch(source, callback, { flush: 'sync' })
watchEffect(callback, { flush: 'sync' })
或者用别名:
javascript
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
// 数据变化后立即执行
})
回调触发顺序:
- 响应式状态变化
- 执行 watch/watchEffect 回调
- Vue 更新组件 DOM
✅ 特点:
-
同步触发:不会等待 Vue 批处理。
-
可能多次触发,尤其是数组或对象多次修改时。
-
⚠️ 警告:不建议在高频数据变化上使用,会导致性能问题。
| flush 选项 | 执行时机 | 可访问 DOM | 批处理 |
|---|---|---|---|
pre (默认) |
响应式状态变化 → 父组件更新 → 回调 → DOM 更新 | ❌ 旧 DOM | ✅ 批处理,避免重复触发 |
post |
响应式状态变化 → 父组件更新 → DOM 更新 → 回调 | ✅ 最新 DOM | ✅ 批处理 |
sync |
响应式状态变化 → 回调 → 父组件 & DOM 更新 | ❌ 旧 DOM | ❌ 不批处理,立即触发 |
批处理 (Batching):是 响应式系统用来合并多次状态更新,避免重复渲染 DOM 的机制。它是 Vue 提升性能的核心优化之一。
10.8 停止侦听器
-
在 setup() 或
<script setup>中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。 -
侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。
javascript
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
- 要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
javascript
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
十一 模板引用
1 访问模板引用 useTemplateRef
javascript
<script setup>
import { useTemplateRef, onMounted } from 'vue'
// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="my-input" />
</template>
-
useTemplateRef('my-input') 会返回一个 ref 对象,类型是 HTMLElement | null 或推断的组件类型。
-
必须在组件挂载之后访问,否则 input.value 可能是 null。
2 组件上的 ref
- 当 ref 作用于子组件而非普通 HTML 元素时,引用的 value 指向的是该子组件的 实例,而非 DOM 节点。
2.1 Vue 3 组件 Ref 访问权限对比
| 子组件写法 | 父组件访问权限 | 说明 |
|---|---|---|
| 选项式 API (Options API) | 完全开放 | 实例与子组件的 this 一致,父组件可直接访问其所有数据、属性和方法。 |
<script setup> |
默认私有 | 遵循关闭原则,父组件默认无法访问子组件内部的任何变量或方法。 |
<script setup> + defineExpose |
按需开放 | 只有在子组件中通过 defineExpose 显式暴露的属性和方法,父组件才能访问。 |
3 v-for 中的模板引用
- 当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素
- 应该注意的是,ref 数组 并不保证与源数组相同的顺序。
4 函数模板引用
- 除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。
- 该函数会收到元素引用作为其第一个参数:
javascript
<input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }">
注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。
十二 组件基础
| 维度 | 全局注册 (Global) | 局部注册 (Local) |
|---|---|---|
| 注册位置 | 在 main.js 中通过 app.component() 注册 |
在 SFC 的 <script setup> 中通过 import 导入 |
| 可用范围 | 应用内所有组件模板均可直接使用 | 仅限于当前导入该组件的父组件模板 |
| 依赖关系 | 隐式依赖:不看注册文件很难发现组件来源 | 显式依赖:代码结构清晰,易于追踪引用关系 |
| Tree-shaking | 不支持:即使未被使用,也会被包含在最终产物中 | 支持:如果组件未被导入使用,会被打包工具剔除 |
| 命名限制 | 必须定义一个全局唯一的标签名 | 随导入变量名定义,建议使用 PascalCase |
| 标签闭合 | 必须遵循浏览器 HTML 规范(双标签) | 支持自闭合写法 <ChildComponent /> |
| 适用场景 | 极高频的基础组件(如:Icon, BaseButton) | 业务逻辑组件、特定页面的功能拆分 |
12.1 传递 props
| 维度 | 关键内容 | 详细说明 |
|---|---|---|
| 定义 (What) | 自定义 Attributes | Props 是在组件上注册的自定义属性,用于父组件向子组件传递数据。 |
| 声明方式 (How) | defineProps(['title']) |
在 <script setup> 中使用的编译器宏,无需导入,声明后直接在模板可用。 |
| 返回值 | Props 对象 | defineProps 返回一个包含所有传入属性的对象,JS 中需通过 props.title 访问。 |
| 非 Setup 语法 | props 选项 |
在选项式 API 中,props 需定义在 props 配置项中,并作为 setup(props) 的首个参数。 |
| 动态传递 | v-bind 或 : |
使用 :title="post.title" 绑定动态数据,常配合 v-for 进行列表渲染。 |
| 数据流向 | 单向数据流 | 数据从父组件流向子组件,默认情况下 prop 接受任何类型的值。 |
12.2 监听事件
| 维度 | 关键内容 | 详细说明 |
|---|---|---|
| 核心机制 | 自定义事件系统 | 子组件通过抛出事件通知父组件,父组件通过 v-on 或 @ 监听。 |
| 模板内抛出 | $emit('event-name') |
在子组件 <template> 中直接使用内置方法抛出事件。 |
| 脚本内声明 | defineEmits(['name']) |
在 <script setup> 中使用的编译器宏,用于声明组件可能触发的事件。 |
| 脚本内调用 | const emit = defineEmits() |
defineEmits 返回一个函数,在 JS 逻辑中通过 emit('name') 触发。 |
| 非 Setup 语法 | context.emit |
在选项式 API 中,需在 emits 选项中声明,并从 setup(props, { emit }) 中调用。 |
| 数据流向 | 向上通信 | 与 Props 相反,事件实现了从子组件向父组件的逆向信息传递。 |
| 事件校验 | 显式声明的好处 | 声明事件可以避免 Vue 将其作为原生 DOM 事件应用于子组件根元素,并支持参数验证。 |
12.3 通过插槽来分配内容
| 维度 | 关键内容 | 详细说明 |
|---|---|---|
| 核心定位 | 内容占位符 | <slot> 元素是一个占位符,决定了父组件提供的"模板内容"渲染在子组件的具体位置。 |
| 使用方式 | <slot /> |
在子组件模板中定义 <slot />,父组件在子组件标签对之间写入的内容将替换它。 |
| 渲染内容 | 不仅限于文本 | 父组件可以传递 HTML 元素、甚至是其他 Vue 组件作为插槽内容。 |
| 作用范围 | 父级模板作用域 | 插槽内容是在父组件中定义的,因此它可以访问父组件的数据,但无法直接访问子组件内部的数据。 |
| 默认行为 | 内容分发 | 如果子组件中没有定义 <slot>,则父组件在标签间写入的任何内容都会被 Vue 忽略。 |
12.4 动态组件
| 维度 | 关键内容 | 详细说明 |
|---|---|---|
| 核心元素 | <component> |
Vue 内置的特殊组件,用作动态组件的载体。 |
| 核心属性 | :is |
决定当前渲染哪个组件的关键属性。 |
| 接受类型 | 组件名 或 组件对象 | :is 的值可以是全局注册的组件名,也可以是直接导入的组件对象。 |
| 普通 HTML | 支持原生标签 | :is 也可以接收字符串(如 'div' 或 'button')来渲染原生的 HTML 元素。 |
| 生命周期 | 默认卸载 | 切换时,旧组件会被销毁(unmounted),新组件会被挂载(mounted)。 |
| 状态保持 | <KeepAlive> |
若希望切换后保留组件状态(不被销毁),需用 <KeepAlive> 包裹 <component>。 |
12.5 DOM 内模板解析注意事项
| 限制维度 | 具体规则 (DOM 内模板) | 解决方案 / 示例 |
|---|---|---|
| 大小写限制 | 浏览器会自动将标签和属性名转为小写 | 必须使用 kebab-case (如 post-title) |
| 标签闭合 | 不支持自闭合。必须写完整的结束标签 | 使用 <my-component></my-component> |
| 元素位置 | <table>, <ul> 等内部只能放特定标签 |
使用 is="vue:component-name" 属性委婉挂载 |
| 属性命名 | camelCase 的 Prop 和 Emit 会失效 | 将 updatePost 改为 @update-post |
十三 生命周期钩子
