VUE3:基础篇官网笔记

系列文章目录

VUE3:深入组件


文章目录

  • 系列文章目录
  • 前言
  • [一、 创建一个应用](#一、 创建一个应用)
    • [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 允许在一个页面创建多个应用实例?)
    • [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 函数开始的。它会创建一个应用实例,用于管理全局资源(组件、指令、插件)和配置。

  • 创建语法

    javascript 复制代码
    import { 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 做了三件事:

  1. 记录谁在使用 count
  2. count 变化
  3. 通知这些地方重新渲染

这就是:依赖收集 → 触发更新

1.2 如果用普通变量会怎样?

  • JavaScript 本身:不能监听普通变量的变化
  • 实际上,JavaScript 监听的是"对属性的操作",而不是对象这个"死物"本身。

目前主流有两种方式来实现这种监听:

A. 早期方案:Object.defineProperty (Vue 2 使用)

  • 这种方式监听的是具体的属性。

  • 原理: 它通过为一个已存在的属性定义 getter 和 setter。

  • 局限:

    1. 你必须知道属性名。如果后来给对象新增了一个属性,它监听不到(这就是为什么 Vue 2 有 Vue.set)。
    2. 无法监听数组下标的变化。

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。相反,它执行了以下操作:

  1. 标记: 把当前受影响的组件(Watcher/Effect)丢进一个名为 queue 的全局队列里。

  2. 去重: 如果你在同一个函数里连着写了 100 次 count.value++,Vue 只会将该组件在队列里存一次。

  3. 预约向浏览器申请一个"微任务",说:"等现在的同步代码执行完了,请立刻执行我的更新任务。"

(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 绑定对象

  1. 绑定一个对象:键名是类名,属性值是布尔值
javascript 复制代码
<div :class="{ active: isActive }"></div>
  1. 多类绑定示例
javascript 复制代码
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
  1. 也可以绑定一个返回对象的计算属性
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-valuefalse-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 的第一个参数可以是不同形式的"数据源":

  1. 它可以是一个 ref (包括计算属性)、
  2. 一个响应式对象、
  3. 一个 getter 函数:Vue会执行这个函数,并 自动收集依赖
  4. 或多个数据源组成的数组

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

回调触发顺序:

  1. 响应式状态变化
  2. Vue 更新组件 DOM
  3. 执行 watch/watchEffect 回调

✅ 适用场景:

  • 你需要访问更新后的 DOM,比如测量元素尺寸、操作滚动条、动画等。

10.7.2 同步触发

javascript 复制代码
watch(source, callback, { flush: 'sync' })
watchEffect(callback, { flush: 'sync' })

或者用别名:

javascript 复制代码
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  // 数据变化后立即执行
})

回调触发顺序:

  1. 响应式状态变化
  2. 执行 watch/watchEffect 回调
  3. 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

十三 生命周期钩子

相关推荐
郝学胜-神的一滴19 小时前
[力扣 227] 双栈妙解表达式计算:从思维逻辑到C++实战,吃透反向波兰式底层原理
java·前端·数据结构·c++·算法
淼淼爱喝水19 小时前
基于DOM型XSS漏洞与利用实验教程
前端·xss·dom·dvwa
Aotman_20 小时前
Element UI 表格搜索高亮
前端·javascript·vue.js·ui·elementui
Cathy Bryant20 小时前
微分几何:度规(度量)metric
笔记·线性代数·矩阵·高等数学·物理
yqcoder21 小时前
[特殊字符] Vue 3 中 Keep-Alive 对生命周期的影响:深度解析
前端·javascript·vue.js
jiayong2321 小时前
第 33 课:任务看板视图(按状态分列)与本地持久化
开发语言·前端·javascript·学习
GISer_Jing21 小时前
Dify可视化编排:技术架构与实战指南
前端·人工智能·ai编程
宇宙realman_99921 小时前
DSP28335-FlashAPI使用
linux·前端·python
fengxin_rou21 小时前
JVM 核心笔记:对象创建、生命周期与类加载器详解
java·jvm·笔记
weixin_7042660521 小时前
Spring Cloud Gateway 完整版笔记
笔记