Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

Vue 生态 / Vue Ecosystem

1. Vue 生态知识体系 / Vue Ecosystem Knowledge System

2. Vue 组件、模板与编译原理 / Vue Components, Templates, and Compilation

3. Vue 3 响应式系统与 Composition API / Vue 3 Reactivity System and Composition API

4. Vue Router、表单与组件架构 / Vue Router, Forms, and Component Architecture

5. Pinia 状态管理与持久化 / Pinia State Management and Persistence

6. Nuxt 路由、渲染与项目结构 / Nuxt Routing, Rendering, and Project Structure

7. Nuxt 数据获取、Server API 与缓存 / Nuxt Data Fetching, Server APIs, and Caching

8. Vue 测试、性能与生产工程 / Vue Testing, Performance, and Production Engineering

本页目录

Vue 组件、模板与编译原理 / Vue Components, Templates, and Compilation ​

📅 创建时间:2026-07-28 🏷️ 标签:#Vue #Components #Templates #SFC #vModel #Slots #Teleport #KeepAlive 📚 前置知识:[[00-overview]]


📋 本章目标 ​

  • 理解 SFC 三段式的编译流程:template 如何变成 render 函数,scoped style 如何实现隔离
  • 掌握 defineProps / defineEmits / defineExpose / defineSlots 的类型安全契约设计
  • 掌握 v-model 的全部变体(基础、多重、修饰符、defineModel)及其原理
  • 理解 v-for 与 v-if 的优先级关系、key 的身份标识本质、以及 Template v-for 的妙用
  • 掌握插槽全解:默认插槽、具名插槽、作用域插槽、动态插槽名及其编译结果
  • 理解 Teleport / Suspense / KeepAlive 的使用场景与内部机制
  • 掌握异步组件的注册方式、加载状态处理以及与 Suspense 的配合
  • 能够编写实用的自定义指令(v-focus、v-click-outside)

第1部分:SFC 三段式与编译原理 ​

1.1 SFC 三段式 ​

Vue 单文件组件将模板、逻辑和样式组织在同一 *.vue 文件中,围绕业务功能聚合代码:

vue
<template>
  <div class="greeting">{{ message }}</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const message = ref('Hello Vue!')
</script>

<style scoped>
.greeting { color: #42b883; }
</style>
1
2
3
4
5
6
7
8
9
10
11
12

1.2 SFC 编译流水线 ​

当你执行构建时,@vue/compiler-sfc 将 .vue 文件拆解为三个独立管道:

┌─────────────────────────────────────────────────────────────┐
│                    SFC 编译流水线                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  .vue 源文件                                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ <template>  │  <script setup>  │  <style scoped>    │   │
│  └──────┬──────┴────────┬─────────┴───────┬────────────┘   │
│         ▼               ▼                 ▼                 │
│  ┌──────────┐  ┌──────────────┐  ┌──────────────┐         │
│  │ Template │  │  Script      │  │  Style       │         │
│  │ Compiler │  │  Compiler    │  │  PostCSS     │         │
│  │          │  │              │  │  Processor   │         │
│  │ Parse →  │  │ <script      │  │              │         │
│  │ Transform│  │ setup> →     │  │ scoped →     │         │
│  │ → Codegen│  │ 标准 JS/TS   │  │ data-v-xxx   │         │
│  └────┬─────┘  └──────┬───────┘  └──────┬───────┘         │
│       ▼               ▼                 ▼                   │
│   render(){}      setup(){}        __scopeId:"data-v-xxx"   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Template 编译三阶段:

  1. Parse:HTML 字符串 → AST,识别元素、属性、指令、插值
  2. Transform:遍历 AST,将 v-if/v-for/v-model 等结构指令转换为 JavaScript 表达式节点
  3. Codegen:生成 render 函数字符串,使用 createElementBlock、renderList 等运行时辅助函数

编译后的 render 函数带 PatchFlag 静态标记,运行时 diff 可跳过静态内容只比较动态绑定:

js
// 编译结果示意
function render(_ctx, _cache) {
  return (openBlock(), createElementBlock('div', null, [
    _ctx.visible
      ? (openBlock(), createElementBlock('p', null, _ctx.message, 1 /* TEXT */))
      : null,
    (openBlock(), createElementBlock('ul', null, [
      renderList(_ctx.list, (item) =>
        (openBlock(), createElementBlock('li', { key: item.id }, item.name, 1))
      )
    ]))
  ]))
}
1
2
3
4
5
6
7
8
9
10
11
12
13

1.3 Scoped Style 实现原理 ​

┌─────────────────────────────────────────────────────────────┐
│                    Scoped Style 工作原理                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  源 CSS                编译后 CSS                            │
│  ──────────────────    ──────────────────────────           │
│  .title { ... }    →   .title[data-v-7ba5bd90] { ... }     │
│                                                             │
│  生成的 DOM:                                                │
│  <h1 class="title" data-v-7ba5bd90>Hello</h1>               │
│                                                             │
│  穿透子组件:       :deep(.child-class) { ... }              │
│  插槽内容:         :slotted(.slot-class) { ... }            │
│  全局样式:         :global(.global-class) { ... }           │
│                                                             │
│  注意:子组件根元素同时携带父组件的 scopeId,                │
│  因此父组件 scoped style 可影响子组件根元素                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

1.4 CSS Modules vs Scoped ​

特性ScopedCSS Modules
隔离方式data-v-xxx 属性选择器类名哈希映射
写法普通 CSS,学习成本低<style module> + $style.className
穿透控制:deep() / :slotted() / :global()天然隔离,需组合(composes)
适用场景大多数组件级样式强隔离需求、大型项目防命名冲突
vue
<!-- CSS Modules 示例 -->
<template>
  <div :class="$style.container">
    <h1 :class="$style.title">Hello</h1>
  </div>
</template>
<style module>
.container { padding: 20px; }
.title { font-size: 24px; }
</style>
1
2
3
4
5
6
7
8
9
10

第2部分:defineProps / defineEmits / defineExpose / defineSlots ​

2.1 组件契约全景 ​

┌─────────────────────────────────────────────────────────────┐
│                    组件契约全景图                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│         父组件                         子组件                 │
│  ┌──────────────────────┐    ┌──────────────────────┐      │
│  │ :propName  ──────────►   │  defineProps()        │      │
│  │ @eventName ◄──────────   │  defineEmits()        │      │
│  │ ref.child  ◄──────────   │  defineExpose()       │      │
│  │ <slot>     ──────────►   │  defineSlots()        │      │
│  │ v-model    ◄─────────►   │  defineModel()        │      │
│  └──────────────────────┘    └──────────────────────┘      │
│                                                             │
│  ───► 父→子 (Props, Slots)                                  │
│  ◄─── 子→父 (Emits, Expose)                                │
│  ◄──► 双向 (v-model)                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2.2 defineProps ​

支持泛型类型声明(Vue 3.3+,编译时擦除)和运行时声明(带校验器):

vue
<script setup lang="ts">
// 方式一:泛型声明(推荐)
interface Props {
  user: { id: string; name: string }
  selected?: boolean
  count: number
}
const props = withDefaults(defineProps<Props>(), {
  selected: false,
  count: 0,
})

// 方式二:运行时声明(需要校验器时)
const props = defineProps({
  user: { type: Object as PropType<User>, required: true },
  count: {
    type: Number,
    required: true,
    validator: (v: number) => v >= 0,
  },
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

关键原则:Props 只读,子组件永不直接修改。需要衍生状态用 computed。

2.3 defineEmits ​

事件命名描述"已发生的事情"而非"要执行的命令":

vue
<script setup lang="ts">
const emit = defineEmits<{
  select: [id: string]                    // ✅ 描述事件
  'update:modelValue': [value: string]   // ✅ v-model 约定
  close: []                               // ✅ 无参数事件
}>()

// ❌ 避免:saveData, doSubmit, clickItem(命令式命名)
</script>
1
2
3
4
5
6
7
8
9

2.4 defineExpose ​

<script setup> 默认封闭。defineExpose 显式暴露方法给父组件的 ref 访问:

vue
<!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const inputRef = ref<HTMLInputElement>()
function focus() { inputRef.value?.focus() }
defineExpose({ focus })  // 只暴露必要方法,不泄露内部状态
</script>
1
2
3
4
5
6
7
vue
<!-- Parent.vue -->
<script setup lang="ts">
const childRef = ref<InstanceType<typeof Child>>()
childRef.value?.focus()  // 类型安全
</script>
1
2
3
4
5

2.5 defineSlots(Vue 3.3+) ​

为插槽提供类型检查:

vue
<script setup lang="ts">
const slots = defineSlots<{
  default?: (props: Record<string, never>) => any
  header?: (props: Record<string, never>) => any
  item?: (props: { item: Item; index: number }) => any  // 作用域插槽
}>()
</script>
1
2
3
4
5
6
7

第3部分:v-model 全变体 ​

3.1 本质:语法糖 ​

v-model="text"
  ↕ 等价于
:modelValue="text" + @update:modelValue="text = $event"

v-model:title="text"
  ↕ 等价于
:title="text" + @update:title="text = $event"
1
2
3
4
5
6
7

3.2 全变体矩阵 ​

┌─────────────────────────────────────────────────────────────┐
│                    v-model 变体一览                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  变体                │  父组件写法            │  子组件接收   │
│  ───────────────────┼──────────────────────┼────────────── │
│  基础               │  v-model="v"          │  modelValue   │
│  具名               │  v-model:title="v"    │  title        │
│  修饰符 .trim       │  v-model.trim="v"     │  modelModifiers│
│  修饰符 .lazy       │  v-model.lazy="v"     │  modelModifiers│
│  自定义修饰符       │  v-model:title.cap="v"│  titleModifiers│
│  defineModel (3.4+) │  const m = defineModel│  一行搞定     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3.3 多重 v-model ​

vue
<!-- 父组件 -->
<UserForm v-model:name="userName" v-model:email="userEmail" v-model:bio="userBio" />

<!-- UserForm.vue -->
<script setup lang="ts">
const name = defineModel<string>('name', { required: true })
const email = defineModel<string>('email', { required: true })
const bio = defineModel<string>('bio', { default: '' })
</script>
1
2
3
4
5
6
7
8
9

3.4 修饰符 ​

内置修饰符(仅原生表单元素):.trim、.number、.lazy

自定义修饰符:子组件通过 modelModifiers prop 感知:

vue
<!-- 父:<CustomInput v-model.capitalize="text" /> -->
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
  modelModifiers?: { capitalize: boolean }
}>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

function handleInput(e: Event) {
  let value = (e.target as HTMLInputElement).value
  if (props.modelModifiers?.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

3.5 defineModel(Vue 3.4+ 推荐) ​

┌─────────────────────────────────────────────────────────────┐
│                 defineModel 简化对比                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  3.4 之前(每个 v-model 需要 ~5 行声明):                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ const props = defineProps<{ modelValue: string }>()  │   │
│  │ const emit = defineEmits<{                          │   │
│  │   'update:modelValue': [v: string]                  │   │
│  │ }>()                                                │   │
│  │ const local = computed({                            │   │
│  │   get: () => props.modelValue,                      │   │
│  │   set: (v) => emit('update:modelValue', v)          │   │
│  │ })                                                  │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  3.4+(一行):                                               │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ const model = defineModel<string>({ required: true })│   │
│  │ const [model, modifiers] = defineModel<string>()    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

3.6 v-model 数据流 ​

┌─────────────────────────────────────────────────────────────┐
│                    v-model 数据流                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  父组件                             子组件                   │
│  ┌──────────┐                    ┌──────────────┐          │
│  │  state   │─── :modelValue ──►│  props.modelValue        │
│  │  text    │                    │       │                  │
│  │          │◄── @update:mv ────│  emit('update:mv', v)   │
│  │          │      = $event     │       ▲                  │
│  └──────────┘                    │  ┌────┴─────┐           │
│                                  │  │ <input>  │           │
│                                  │  └──────────┘           │
│                                  └──────────────┘          │
│                                                             │
│  本质仍是单向数据流:Props 向下,Events 向上                  │
│  v-model 只把这个模式封装为更简洁的语法                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第4部分:v-for / v-if 优先级与 key ​

4.1 v-if 优先级高于 v-for ​

Vue 3 中 v-if 先执行,此时循环变量尚不可用,禁止在同一元素上同时使用:

vue
<!-- ❌ 错误:v-if 先执行,item 不存在 -->
<li v-for="item in items" v-if="item.active">{{ item.name }}</li>

<!-- ✅ 正确:computed 预过滤 -->
<script setup>
const activeItems = computed(() => items.value.filter(i => i.active))
</script>
<template>
  <li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>
</template>

<!-- ✅ 正确:template 包裹 v-for,内层 v-if -->
<template v-for="item in items" :key="item.id">
  <li v-if="item.active">{{ item.name }}</li>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

4.2 key 的本质:身份标识 ​

key 赋予每个节点稳定的身份标识,让 Vue 能正确追踪 DOM 与数据实体的对应关系,而非仅仅是性能优化参数:

┌─────────────────────────────────────────────────────────────┐
│                   key 的作用演示                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原数据: [{id:1,"A"},{id:2,"B"},{id:3,"C"}]                  │
│  → 头部插入 {id:4,"D"}                                       │
│                                                             │
│  无 key(就地复用):          有 key(按身份追踪):           │
│  ┌──────────────────────┐   ┌──────────────────────┐       │
│  │ 位置0: "A"→"D" 更新  │   │ key=4: 新建插入头部  │       │
│  │ 位置1: "B"→"A" 更新  │   │ key=1,2,3: 原地不动  │       │
│  │ 位置2: "C"→"B" 更新  │   │                      │       │
│  │ 位置3: 新建 "C"      │   │ 内部状态全部保持正确!│       │
│  │ → 3次更新+所有状态错乱│   └──────────────────────┘       │
│  └──────────────────────┘                                    │
│                                                             │
│  核心:key 保护组件内部状态(表单输入、动画、focus 等)       │
│  这些状态绑定在组件实例上,不是 DOM 上                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

使用原则:

  • 始终提供稳定的唯一标识(数据库 ID)
  • 不用数组索引(除非列表永不变序、永不增删)
  • 不用 Math.random() 或 Date.now()(每次渲染都销毁重建)
  • Template v-for 时,key 放在 <template> 上

4.3 Template v-for 与解构 ​

vue
<!-- 不引入额外包裹元素 -->
<template v-for="item in items" :key="item.id">
  <dt>{{ item.term }}</dt>
  <dd>{{ item.definition }}</dd>
</template>

<!-- 解构 -->
<li v-for="{ id, name, email } in users" :key="id">
  {{ name }} ({{ email }})
</li>

<!-- 带索引解构 -->
<li v-for="({ id, name }, index) in users" :key="id">
  {{ index + 1 }}. {{ name }}
</li>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第5部分:插槽全解 ​

5.1 三种插槽 ​

┌─────────────────────────────────────────────────────────────┐
│                    插槽类型全景                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  默认插槽:      <Child>content</Child> ↕ <slot />           │
│  具名插槽:      <template #header> ↕ <slot name="header">   │
│  作用域插槽:    <template #item="{ item }"> ↕ <slot :item="...">│
│  动态插槽名:    <template #[dynamicName]>                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10

5.2 具名插槽与动态插槽名 ​

vue
<!-- Layout.vue(子组件) -->
<template>
  <div class="layout">
    <header><slot name="header">默认头部</slot></header>
    <main><slot>默认内容</slot></main>  <!-- 默认插槽 -->
    <footer><slot name="footer">默认底部</slot></footer>
  </div>
</template>

<!-- 父组件使用 -->
<Layout>
  <template #header><h1>页面标题</h1></template>
  <p>主要内容</p>  <!-- 自动落入默认插槽 -->
  <template #footer><small>版权信息</small></template>
</Layout>

<!-- 动态插槽名(Vue 3) -->
<template #[dynamicSlotName]> 内容 </template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

5.3 作用域插槽 ​

子组件向父组件暴露数据,实现"数据反向传递":

vue
<!-- DataTable.vue -->
<template>
  <tr v-for="row in rows" :key="row.id">
    <slot name="row" :row="row" :index="index">
      <td>{{ row.name }}</td>  <!-- 默认渲染 -->
    </slot>
  </tr>
</template>

<!-- 父组件使用 -->
<DataTable :rows="users">
  <template #row="{ row: user, index = 0 }">  <!-- 解构+重命名+默认值 -->
    <td>{{ index + 1 }}</td>
    <td><UserAvatar :user="user" />{{ user.name }}</td>
    <td><StatusBadge :status="user.status" /></td>
  </template>
</DataTable>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

5.4 插槽的编译结果 ​

┌─────────────────────────────────────────────────────────────┐
│                    插槽的编译结果                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  源模板:                        编译后 render:              │
│  ┌──────────────────────┐     ┌────────────────────────┐   │
│  │ <slot name="header"/>│     │ renderSlot(             │   │
│  │ <slot :item="data"/> │ →  │   $slots, 'header')     │   │
│  └──────────────────────┘     │ renderSlot(             │   │
│                               │   $slots, 'default',   │   │
│                               │   { item: _ctx.data }) │   │
│                               └────────────────────────┘   │
│                                                             │
│  每个插槽 = $slots 中的一个函数,接收 props,返回 VNode[]    │
│  useSlots() 可在 <script setup> 中访问插槽函数               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

5.5 $attrs 透传 ​

底层 UI 组件应透传属性到原生元素:

vue
<!-- BaseInput.vue —— 单根节点自动透传 -->
<template>
  <input v-bind="$attrs" :value="modelValue" @input="..." />
</template>

<!-- 多根节点 —— 必须显式指定 -->
<template>
  <label>Label</label>
  <input v-model="model" v-bind="$attrs" />  <!-- 显式绑定 -->
</template>
1
2
3
4
5
6
7
8
9
10

第6部分:Teleport / Suspense / KeepAlive ​

6.1 Teleport —— DOM 传送 ​

<Teleport> 将组件的一部分模板渲染到 DOM 树其他位置,同时保持逻辑层级不变(props/emits/provide/inject 照常工作):

vue
<template>
  <Teleport to="body">
    <div class="modal-overlay" @click.self="emit('close')">
      <div class="modal-content" role="dialog" aria-modal="true">
        <slot />
      </div>
    </div>
  </Teleport>
</template>
1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────────────────────┐
│                    Teleport DOM 结构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  组件树(逻辑层级):            DOM 树(渲染结果):            │
│  ┌────────────────────┐       ┌────────────────────┐       │
│  │ <App>              │       │ <body>             │       │
│  │  ├── <Main>        │       │  ├── <div id="app">│       │
│  │  │   └── <Modal>   │       │  │   └── <Main>    │       │
│  │  │       └── <Teleport>    │  ├── <div.modals>  │       │
│  │  │           └── <Overlay> │  │   └── <Overlay> │←传送  │
│  └────────────────────┘       └────────────────────┘       │
│                                                             │
│  适用: 模态框、Toast 通知、下拉菜单、全屏 Loading            │
│  禁用: <Teleport to="body" :disabled="isMobile">            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

6.2 Suspense —— 异步依赖协调 ​

<Suspense> 等待异步依赖(异步组件、async setup())时展示降级内容:

vue
<template>
  <Suspense>
    <template #default>
      <AsyncDashboard />          <!-- 异步组件 -->
    </template>
    <template #fallback>
      <DashboardSkeleton />       <!-- 加载中展示 -->
    </template>
  </Suspense>
</template>
1
2
3
4
5
6
7
8
9
10

触发 Suspense 的依赖:

  1. defineAsyncComponent 定义的异步组件
  2. <script setup> 中使用顶层 await 的组件
vue
<script setup lang="ts">
// 顶层 await 让整个 setup 变为 async,触发父级 Suspense
const data = await fetch('/api/dashboard').then(r => r.json())
</script>
1
2
3
4

6.3 KeepAlive —— 组件实例缓存 ​

缓存不活跃组件实例,避免重复创建/销毁,典型场景是 Tab 切换保留滚动位置和表单状态:

┌─────────────────────────────────────────────────────────────┐
│                    KeepAlive 缓存机制                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  无 KeepAlive:     Tab A → Tab B → Tab A                    │
│                    创建A   销毁A   重建A(状态丢失)           │
│                                                             │
│  有 KeepAlive:     Tab A → Tab B → Tab A                    │
│                    创建A   缓存A   激活A(状态保留)           │
│                            创建B                              │
│                                                             │
│  内部: cache: Map<componentName, VNode>                     │
│  max=n: 最多缓存 n 个(LRU 淘汰)                            │
│  include/exclude: 按组件名精确控制缓存范围                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
vue
<template>
  <KeepAlive :include="['Dashboard', 'Settings']" :exclude="['Login']" :max="10">
    <component :is="currentTab" />
  </KeepAlive>
</template>

<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'

// KeepAlive 专属生命周期
onActivated(() => {
  // 从缓存激活 → 重新获取数据、恢复定时器
})
onDeactivated(() => {
  // 被缓存 → 清除定时器、保存草稿
})
// 注意:被缓存的组件不触发 onUnmounted
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第7部分:组件注册与异步组件 ​

7.1 全局注册 vs 局部注册 ​

┌─────────────────────────────────────────────────────────────┐
│               全局注册 vs 局部注册 对比                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  全局注册 (app.component):                                   │
│  ✅ 任何组件中直接使用,无需 import                          │
│  ❌ 即使不引用也会打包(Tree-shaking 失效)                  │
│  ❌ 全局命名空间污染,难以追踪依赖                           │
│                                                             │
│  局部注册 (import):                                          │
│  ✅ Tree-shaking 友好,显式依赖关系                          │
│  ❌ 每个文件需要 import                                     │
│                                                             │
│  策略:基础 UI 组件全局注册,业务组件局部注册                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts
// main.ts —— 全局注册通用组件
import { createApp } from 'vue'
import BaseButton from './components/BaseButton.vue'
const app = createApp(App)
app.component('BaseButton', BaseButton)
app.mount('#app')
1
2
3
4
5
6

7.2 defineAsyncComponent ​

创建仅在需要时才加载的异步组件,实现代码分割:

ts
import { defineAsyncComponent } from 'vue'

const AsyncDashboard = defineAsyncComponent({
  loader: () => import('./Dashboard.vue'),
  loadingComponent: DashboardSkeleton,  // 加载中组件
  errorComponent: ErrorDisplay,         // 加载失败组件
  delay: 200,                            // 展示 loading 前延迟(防止闪烁)
  timeout: 3000,                         // 超时时间
  onError(error, retry, fail, attempts) {
    if (attempts <= 3) { retry() }       // 最多重试 3 次
    else { fail() }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13

7.3 Suspense + 异步组件 ​

┌─────────────────────────────────────────────────────────────┐
│                 异步组件加载生命周期                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  pending ──delay(200ms)──► loadingComponent 显示            │
│     │                                                       │
│     ├── loader resolve ──► 渲染组件(成功)                  │
│     ├── timeout ──► errorComponent(超时)                   │
│     └── error ──► onError → retry 或 errorComponent         │
│                                                             │
│  Suspense 统一管理多个异步组件的加载状态                      │
│  defineAsyncComponent 提供单个组件的精细控制                  │
│  两者可组合使用                                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第8部分:自定义指令 ​

8.1 指令钩子与生命周期 ​

┌─────────────────────────────────────────────────────────────┐
│                   指令生命周期钩子                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  created → mounted → beforeUpdate → updated → beforeUnmount → unmounted
│  (绑定)    (插入DOM)  (数据变化前)  (数据变化后) (卸载前)     (已卸载)
│                                                             │
│  binding 对象:                                               │
│  • value:   指令值       v-dir="value"                       │
│  • arg:     参数         v-dir:arg                           │
│  • modifiers: 修饰符     v-dir.mod1.mod2                    │
│  • instance: 组件实例                                        │
│                                                             │
│  函数简写: 同时作为 mounted 和 updated 钩子                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

8.2 实用指令示例 ​

v-focus —— 自动聚焦:

ts
import type { Directive } from 'vue'
export const vFocus: Directive<HTMLInputElement> = {
  mounted(el) { el.focus() },
}
1
2
3
4

v-click-outside —— 点击外部关闭:

ts
import type { Directive, DirectiveBinding } from 'vue'

export const vClickOutside: Directive<HTMLElement> = {
  mounted(el: HTMLElement & { __handler?: Function }, binding: DirectiveBinding) {
    const handler = (event: MouseEvent) => {
      if (!el.contains(event.target as Node)) {
        binding.value?.(event)
      }
    }
    el.__handler = handler
    document.addEventListener('click', handler)
  },
  unmounted(el: HTMLElement & { __handler?: Function }) {
    if (el.__handler) {
      document.removeEventListener('click', el.__handler)
    }
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vue
<template>
  <div v-click-outside="() => (open = false)">
    <input @focus="open = true" />
    <ul v-if="open">...</ul>
  </div>
</template>
1
2
3
4
5
6

v-intersect —— 可见性检测:

ts
export const vIntersect: Directive<HTMLElement, () => void> = {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) binding.value?.()
    }, { threshold: binding.arg ? Number(binding.arg) : 0.1 })
    observer.observe(el)
    ;(el as any).__observer = observer
  },
  unmounted(el) {
    ;(el as any).__observer?.disconnect()
  },
}
1
2
3
4
5
6
7
8
9
10
11
12
vue
<img v-intersect:0.5="loadImage" :data-src="imageUrl" alt="Lazy" />
1

8.3 指令 vs 组件 ​

维度自定义指令组件
适用场景直接操作 DOM(focus, scroll, measure)渲染 UI 内容(新 DOM 子树)
DOM 影响附加行为到已有元素引入新 DOM 结构
状态管理无内部状态可管理内部状态
典型示例v-focus, v-click-outside, v-intersectModal, Dropdown, Tooltip

综合实战 —— 可复用的搜索选择器 ​

结合本章所有知识点实现一个带搜索、异步加载、点击外部关闭的选择器:

vue
<script setup lang="ts" generic="T extends { id: string; label: string }">
import { ref, computed } from 'vue'
import { useDebounceFn } from '@vueuse/core'

const props = withDefaults(defineProps<{
  items: T[]
  placeholder?: string
  loading?: boolean
}>(), {
  placeholder: '请选择...',
  loading: false,
})

const model = defineModel<T | null>({ default: null })
const open = ref(false)
const query = ref('')

const emit = defineEmits<{ search: [query: string] }>()
const debouncedSearch = useDebounceFn((q: string) => emit('search', q), 300)

const filteredItems = computed(() => {
  if (!query.value) return props.items
  const q = query.value.toLowerCase()
  return props.items.filter(i => i.label.toLowerCase().includes(q))
})

function select(item: T) {
  model.value = item
  open.value = false
}
</script>

<template>
  <div class="selector" v-click-outside="() => (open = false)">
    <input
      :value="model?.label ?? ''"
      :placeholder="placeholder"
      @focus="open = true"
      @input="query = ($event.target as HTMLInputElement).value; debouncedSearch(query)"
    />
    <div v-if="open" class="dropdown">
      <div v-if="loading" class="loading">加载中...</div>
      <ul v-else-if="filteredItems.length">
        <li
          v-for="item in filteredItems"
          :key="item.id"
          :class="{ active: model?.id === item.id }"
          @click="select(item)"
        >
          {{ item.label }}
        </li>
      </ul>
      <div v-else class="empty">无匹配结果</div>
    </div>
  </div>
</template>

<style scoped>
.selector { position: relative; }
.dropdown {
  position: absolute; top: 100%; left: 0; right: 0;
  background: white; border: 1px solid #ddd;
  border-radius: 4px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  z-index: 100; max-height: 240px; overflow-y: auto;
}
.active { background: #e8f4fd; }
</style>
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

核心总结 ​

总结1:SFC 编译原理 ​

.vue → @vue/compiler-sfc 三路解析
  ├── Template → Parse → Transform → Codegen → render()(带 PatchFlag 优化)
  ├── Script setup → 编译宏展开 → 标准 JS/TS
  └── Scoped Style → data-v-xxx 属性选择器隔离
1
2
3
4

总结2:组件契约四要素 ​

宏方向作用
defineProps父→子只读输入属性
defineEmits子→父领域事件契约(描述已发生的事)
defineExpose子→父(ref 访问)暴露方法/属性
defineSlots父→子(内容分发)类型安全的插槽定义

总结3:v-model 语法糖 ​

v-model="v" ⟺ :modelValue="v" + @update:modelValue="v = $event"
v-model:name="v" ⟺ :name="v" + @update:name="v = $event"
defineModel<T>() → 一站式,自动处理 props + emits(Vue 3.4+)
1
2
3

总结4:关键规则速查 ​

  • v-if > v-for 优先级:永不共存;用 computed 预过滤
  • key = 身份标识:保护组件内部状态,不是性能优化参数
  • 插槽 = 函数:$slots 中的函数,接收 props 返回 VNode[]
  • Teleport:DOM 传送但逻辑层级不变;Suspense:统一异步加载状态;KeepAlive:缓存实例保留状态

章节测试 ​

测试1:SFC 编译 ​

Vue SFC 的 <template> 块经过哪三个阶段变为 render 函数?

测试2:Scoped Style ​

<style scoped> 的隔离原理是什么?如何穿透子组件?

测试3:v-if 与 v-for ​

为什么 Vue 3 不允许同一元素上同时使用 v-if 和 v-for?正确做法是什么?

测试4:key 的作用 ​

key 最核心的作用是什么? A. 帮助 Virtual DOM diff 算法更快 B. 赋予节点稳定身份标识,让 Vue 正确追踪 DOM 与数据的对应关系 C. 标记唯一的 CSS 类名 D. Vue Router 识别路由的参数

测试5:defineModel ​

写出用 defineModel 定义一个具名 v-model title 的代码(Vue 3.4+)。

测试6:Teleport 场景 ​

哪些场景适合 <Teleport>?(多选) A. 模态框(需脱离 overflow:hidden) B. 全局通知 C. 兄弟组件共享数据 D. 全屏 Loading 遮罩

测试7:KeepAlive ​

被 <KeepAlive> 缓存的组件触发哪些特殊钩子?各适合做什么?

测试8:自定义指令 ​

写出 v-autofocus 指令:挂载后自动聚焦,100ms 后选中所有文本。


参考答案 ​

测试1答案 ​

Parse(HTML → AST)→ Transform(指令展开为 JS 表达式)→ Codegen(生成 render 函数)。编译产物使用 PatchFlag 标记动态内容以优化 diff。


测试2答案 ​

给组件内元素添加唯一 data-v-xxx 属性,CSS 选择器改写为属性选择器(.title → .title[data-v-xxx])。穿透使用 :deep(.child-class)。


测试3答案 ​

Vue 3 中 v-if 优先级高于 v-for,v-if 执行时循环变量未定义。正确做法:用 computed 预过滤,或将 v-for 放在 <template> 上、v-if 放内层元素。


测试4答案 ​

B。key 的核心是身份标识,保护组件内部状态在列表重排时不发生错乱。


测试5答案 ​

vue
<script setup lang="ts">
const title = defineModel<string>('title', { required: true })
</script>
1
2
3

测试6答案 ​

A、B、D。Teleport 解决 DOM 位置问题。C 是数据共享问题,应用 Pinia 或 provide/inject。


测试7答案 ​

onActivated(重新获取数据、恢复定时器)和 onDeactivated(清除定时器、保存草稿)。被缓存时不触发 onUnmounted。


测试8答案 ​

ts
import type { Directive } from 'vue'
export const vAutofocus: Directive<HTMLInputElement> = {
  mounted(el) { el.focus(); setTimeout(() => el.select(), 100) },
}
1
2
3
4

相关笔记 ​

  • [[02-reactivity-and-composition-api]] - 响应式系统与 Composition API
  • [[03-router-forms-and-component-architecture]] - 路由、表单与组件架构
  • [[04-pinia-state-management]] - Pinia 状态管理
  • [[../01-javascript-and-typescript/00-overview]] - JavaScript 与 TypeScript 基础

下一步学习 ​

  • [ ] 阅读 02 - 响应式系统与 Composition API
  • [ ] 阅读 03 - 路由、表单与组件架构

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇1. Vue 生态知识体系 / Vue Ecosystem Knowledge System
下一篇3. Vue 3 响应式系统与 Composition API / Vue 3 Reactivity System and Composition API

持续记录,持续成长

Copyright © Tidenflow