第10章 响应式 API 进阶

第十章 响应式 API 进阶

ref 和 reactive 只是 Vue 响应式系统的"入门 API"。这一章我们来探索更深层的东西——shallowRef、shallowReactive、markRaw、toRaw、readonly、customRef……以及 watch 的各种高级用法、nextTick 的原理。读完这一章,你对 Vue 响应式系统的理解会从"会用"升级到"理解原理"。

10.1 响应式 API 全解

10.1.1 ref 与 reactive 深度对比

前面已经讲过 refreactive,这里做一下深度对比,帮你彻底理解两者的异同:

refreactive
适用类型基本类型 + 对象/数组仅对象/数组
访问方式.value(script)/ 自动解包(template)直接属性访问
可以整体替换ref.value = newValue❌ 需要 Object.assign
解构后响应式❌(除非用 toRefs)❌(除非用 toRefs)
类型标注泛型参数:ref<T>()泛型参数:reactive<T>()
内部实现对象包装 + getter/setterProxy
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref, reactive } from 'vue'

// ref 适合基本类型
const count = ref(0)
const name = ref('小明')

// ref 也适合对象(会自动用 reactive 包装)
const user = ref({ name: '小明', age: 25 })
// 访问时:user.value.name
// 模板里:user.name(自动解包)

// reactive 只适合对象/数组
const state = reactive({
  count: 0,
  user: { name: '小明' }
})
// 访问时:state.count, state.user.name

// 整体替换:ref 可以,reactive 不行
count.value = 10  // ✅ OK
// reactive 不支持:state = reactive({ new: 'state' }) —— 会丢失响应式

// 如果真的要替换 reactive 的整体,用 Object.assign
Object.assign(state, { count: 10 })  // ✅ OK

10.1.2 shallowRef 与 triggerRef

ref 会递归地把所有嵌套属性都变成响应式的——如果你的数据嵌套很深(比如一个巨大的对象),ref 可能会带来性能问题。

shallowRef 是"浅层响应式"的 ref——它只把第一层变成响应式的,深层属性的变化不会触发视图更新,除非你手动强制触发

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { shallowRef, triggerRef } from 'vue'

// shallowRef:只追踪 .value 的变化,不追踪内部属性的变化
const state = shallowRef({
  count: 0,
  nested: {
    value: 100
  }
})

state.value.count++         // ⚠️ 不会触发视图更新!因为只追踪 .value
state.value = { count: 1, nested: { value: 100 } }  // ✅ 整体替换会触发

// 如果只想更新深层属性,需要手动调用 triggerRef
state.value.nested.value = 200
triggerRef(state)  // 强制触发更新 —— 但这基本等于放弃了响应式的自动追踪

什么时候用 shallowRef

  • 数据结构非常庞大,深层变化频繁,但视图不需要每次都更新(比如大数据量的表格、游戏引擎数据)
  • 性能优化场景:如果你明确知道只在特定时机更新视图,可以用 shallowRef 减少不必要的追踪
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { shallowRef } from 'vue'

// 场景:大数据量列表,只在翻页时更新
const largeDataTable = shallowRef({
  rows: [],       // 第一次填充
  page: 1,
  totalPages: 100
})

// 翻页时整体替换数据
async function loadPage(page: number) {
  const data = await fetchPageData(page)
  largeDataTable.value = { rows: data.rows, page, totalPages: data.totalPages }
  // shallowRef 只追踪 .value 的变化,所以整体替换才会触发更新
}

10.1.3 shallowReactive 与 readonly

shallowReactive:和 shallowRef 类似,shallowReactive 只让对象的第一层属性响应式,深层不会。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { shallowReactive, readonly } from 'vue'

// shallowReactive:只有第一层是响应式的
const state = shallowReactive({
  count: 0,
  nested: {
    value: 100
  }
})

state.count++      // ✅ 响应式更新
state.nested.value = 200  // ⚠️ 不会触发更新!

readonly:把一个响应式对象变成"只读"——任何修改操作都会报警告。常用于只读的常量或配置数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import { readonly, reactive } from 'vue'

// readonly:只读副本
const original = reactive({
  apiBase: 'https://api.example.com',
  maxRetries: 3,
  config: { timeout: 5000 }
})

const config = readonly(original)

// 读取没问题
console.log(config.apiBase)  // https://api.example.com

// 修改会报警告
config.apiBase = 'http://localhost'  // ⚠️ Vue 会报警告:Vue received a Received-a-reactive-proxy-of...
config.maxRetries = 10              // ⚠️ 同样报警告

// 修改 original 也会影响 config(因为是同一份数据)
original.apiBase = 'http://changed.com'
console.log(config.apiBase)  // http://changed.com —— readonly 不创造副本,只是禁止修改

readablereadonly 是"只读"版本,如果你的数据一开始就是普通数据(非响应式),想变成只读但不转 Proxy,可以用 readable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { readonly, ref } from 'vue'

const count = ref(0)

// readonly 创建只读代理
const readOnlyCount = readonly(count)

// 读取 OK
console.log(readOnlyCount.value)  // 0

// 写入会报错
count.value++  // ⚠️ 报错(如果直接赋值给 readonly 的响应式数据)

10.1.4 markRaw(绕过响应式)

markRaw 告诉 Vue:“这个对象不需要响应式处理”,可以提升性能。常用于不渲染在模板里、只是当作配置或工具函数使用的对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { reactive, markRaw } from 'vue'

// 如果一个对象不需要响应式,用 markRaw 可以避免不必要的 Proxy 包装开销
const utils = markRaw({
  formatDate: (date: Date) => date.toISOString(),
  deepClone: <T>(obj: T): T => JSON.parse(JSON.stringify(obj)),
  someLargeLib: new LargeLibrary()  // 大型库实例,不需要响应式追踪
})

const state = reactive({
  data: { message: 'hello' },
  utils  // utils 里的内容不会被 Proxy 包装
})

常见的使用场景:

  • 第三方库的实例(如 axios、lodash)
  • 不需要在模板中渲染的工具对象
  • 组件实例的引用(如 new Chart()

10.1.5 toRaw(获取原始对象)

toRaw 可以从 Proxy 响应式对象中提取出"原始对象"——也就是被 Vue 包装之前的那个原始对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { reactive, toRaw } from 'vue'

const state = reactive({
  count: 0,
  name: '小明'
})

const raw = toRaw(state)
console.log(raw === state)  // false —— 是两个不同的引用
console.log(raw.count)     // 0 —— 可以正常访问

// 修改 raw 不会触发响应式更新(绕过了 Proxy)
raw.count = 999
console.log(state.count)  // 0 —— 响应式对象没有变

toRaw 的使用场景不多,但在某些需要绕过响应式直接操作对象的场景下会用到(比如你需要直接修改某个属性但不触发更新,或者需要把对象传给不支持 Proxy 的第三方库)。

10.2 Template Refs(获取 DOM)

10.2.1 传统 ref 绑定 DOM

ref 除了创建响应式数据,还能用来获取 DOM 元素或子组件实例——这就是 Template Ref。

 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
<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref,类型是 HTMLDivElement
const containerRef = ref<HTMLDivElement | null>(null)
const inputRef = ref<HTMLInputElement | null>(null)

onMounted(() => {
  // DOM 元素在 onMounted 之后才能访问
  console.log(containerRef.value)  // <div> 元素
  console.log(inputRef.value)     // <input> 元素

  // 获取焦点
  inputRef.value?.focus()
  // 获取尺寸
  const rect = containerRef.value?.getBoundingClientRect()
  console.log('container 宽度:', rect?.width)
})
</script>

<template>
  <div ref="containerRef" class="container">
    <input ref="inputRef" type="text" placeholder="自动聚焦" />
  </div>
</template>

10.2.2 useTemplateRef(Vue 3.5+ 新 API)

Vue 3.5 引入了 useTemplateRef,简化了模板 ref 的获取——不需要在 <script setup> 里声明 ref 变量,只需要指定 ref 名称即可。

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

// 不需要 ref()!直接用 useTemplateRef
// 'myDiv' 是 ref 的名字,要和模板里 ref 属性绑定的变量名一致
const myDiv = useTemplateRef<HTMLDivElement>('myDiv')

onMounted(() => {
  console.log(myDiv.value)  // DOM 元素
})
</script>

<template>
  <!-- ref="myDiv" 对应 useTemplateRef('myDiv') -->
  <!-- 注意在模板里用 camelCase 命名 myDiv),Vue 会自动处理 -->
  <div ref="myDiv">Hello</div>
</template>

useTemplateRef vs 传统 ref() 的区别:

传统 ref()useTemplateRef()
需要先声明变量const el = ref(null)无需声明
ref 名和变量名的对应容易拼写不一致声明和使用在一起,不易出错
适用场景所有场景Vue 3.5+ 推荐

useTemplateRef 的优势是不需要提前声明 ref 变量——传统写法容易出现"模板里写了 ref="xxx" 但 JS 里变量名拼错了"的 bug,useTemplateRef 把声明和使用绑定在一起,减少了出错的可能。

10.2.3 绑定组件实例

模板 ref 还可以用来获取子组件实例,访问子组件暴露的属性和方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- 子组件Counter.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)

function reset() {
  count.value = 0
}

// 通过 defineExpose 暴露给父组件
defineExpose({ count, reset })
</script>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'

const counterRef = ref<InstanceType<typeof Counter> | null>(null)

function handleReset() {
  counterRef.value?.reset()  // 调用子组件的方法
  console.log(counterRef.value?.count)  // 访问子组件的 count
}
</script>

<template>
  <Counter ref="counterRef" />
  <button @click="handleReset">重置</button>
</template>

InstanceType<typeof Counter> 是 TypeScript 的语法,用来获取 Counter 组件实例的类型。这样 counterRef.value 就有正确的类型提示,可以访问 reset() 方法和 count 属性。

10.2.4 常见错误与注意事项

错误一:在 setup 里访问 ref

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
setup() {
  const el = ref<HTMLDivElement | null>(null)
  console.log(el.value)  // null —— setup 执行时 DOM 还不存在!

  // 正确做法:在 onMounted 里访问
  onMounted(() => {
    console.log(el.value)  // DOM 元素
  })

  return { el }
}

错误二:ref 名称拼写错误

1
2
<!-- 模板里ref="inputRef" -->
<!-- script const inputReff = ref(...) -->  <!-- 拼写不一致-->

useTemplateRef(Vue 3.5+)可以避免这个问题,因为它把名称绑定在一起,拼写错误在编译时就能发现。

10.3 reactive 解构与 Vue 3.5 改进

10.3.1 reactive 直接解构(Vue 3.5 支持,保持响应式)

在 Vue 3.5 之前,从 reactive 对象解构出来的属性会丢失响应式。从 Vue 3.5 开始,defineProps 返回的对象解构后保持响应式(前面讲过),但普通 reactive 对象的解构仍然有问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  name: '小明'
})

// ❌ Vue 3.5 之前:解构会丢失响应式
// const { count, name } = state
// count++ 不会影响 state.count

// ✅ 正确做法:保持对原始对象的引用
function increment() {
  state.count++  // 直接修改原对象
}

// 或者用 toRefs 转换成多个 ref
import { toRefs } from 'vue'
const { count, name } = toRefs(state)
count.value++  // ✅ 响应式更新

10.3.2 toRef / toRefs 的使用场景变化

toRefs 的核心价值是把 reactive 对象的所有属性变成 ref,从而支持解构后仍然保持响应式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { reactive, toRefs } from 'vue'

const state = reactive({
  count: 0,
  name: '小明'
})

// toRefs:解构后每个属性都是 ref,和原始对象保持响应式连接
const { count, name } = toRefs(state)

// count.value++ 会更新 state.count
// state.count++ 也会更新 count.value

10.3.3 Props 解构与 withDefaults 配合(重点)

Props 的解构是 Vue 3.5 的重点改进之一。从 Vue 3.5 开始,defineProps 返回的对象可以被安全地解构,且解构后的值保持响应式。

1
2
3
4
5
6
7
8
9
// Vue 3.5+:解构 props 保持响应式
const { title, count, items } = defineProps<{
  title: string
  count?: number
  items?: string[]
}>()

// title, count, items 都是响应式的
// 在模板里可以直接用,不需要 props.title

配合 withDefaults 设置默认值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const { title, count = 0, items = [] } = withDefaults(defineProps<{
  title: string
  count?: number
  items?: string[]
}>(), {
  count: 0,
  items: () => []  // 引用类型必须用函数返回
})

// 在模板里直接用 title, count, items

10.4 watch 进阶

10.4.1 flush 选项(pre / post / sync)

watch 默认在组件更新前触发(flush: 'pre')。你可以通过 flush 选项调整触发时机:

flush 值触发时机适用场景
'pre'(默认)组件更新前触发,组件更新多个时只触发一次常规场景
'post'DOM 更新后触发需要访问更新后的 DOM
'sync'数据变化时同步触发(不推荐日常使用)需要即时响应
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { ref, watch } from 'vue'

const inputValue = ref('')

// 默认(pre):输入时不会立即触发,等输入稳定后才触发
watch(inputValue, (newVal) => {
  console.log('输入值变了:', newVal)
})

// post:DOM 更新后触发,可以访问最新 DOM
const domRef = ref<HTMLDivElement | null>(null)
watch(inputValue, () => {
  // DOM 已经更新完成
  console.log('DOM 内容:', domRef.value?.innerText)
}, { flush: 'post' })

10.4.2 once 修饰符(Vue 3.5+)

Vue 3.5 引入了 watch.once 修饰符,让监听器只触发一次后自动停止:

1
2
3
4
5
6
7
8
import { ref, watch } from 'vue'

const userId = ref(1)

watch(userId, (newId) => {
  console.log('用户 ID 变了:', newId)
  // fetchUser(newId)
}, { once: true })  // 只触发一次,之后自动停止(相当于手动调用了 stop())

10.4.3 watch 参数详解(source / callback / options)

watch 的完整签名:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
watch<T>(
  source: () => T,          // getter 函数或 ref/reactive
  callback: (newValue: T, oldValue: T) => void,  // 回调函数
  options?: {                // 可选配置
    immediate?: boolean,     // 是否立即执行
    deep?: boolean,         // 是否深度监听
    flush?: 'pre' | 'post' | 'sync',
    once?: boolean          // Vue 3.5+,只触发一次
  }
)

监听 getter 函数:

1
2
3
4
5
6
7
8
9
// 监听计算属性(getter)
const user = reactive({ name: '小明', age: 25 })

watch(
  () => user.name,  // getter 函数,只监听 user.name 的变化
  (newName, oldName) => {
    console.log(`名字从 ${oldName} 变成了 ${newName}`)
  }
)

10.4.4 监听多个数据源

watch 支持同时监听多个数据源:

1
2
3
4
5
6
7
8
9
import { ref, watch } from 'vue'

const firstName = ref('')
const lastName = ref('')

// 监听多个 ref
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`姓名从 ${oldFirst} ${oldLast} 变成了 ${newFirst} ${newLast}`)
})

10.4.5 Vue 3.5 onWatcherCleanup 清理(重点)

Vue 3.5 的 onWatcherCleanup API 让 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
import { ref, watch, onWatcherCleanup } from 'vue'

const searchQuery = ref('')

watch(searchQuery, async (query) => {
  // 创建取消控制器
  const controller = new AbortController()

  // 注册清理函数 —— watch 重新触发或停止时调用
  onWatcherCleanup(() => {
    controller.abort()  // 取消还在进行的请求
  })

  try {
    const data = await fetch(`/api/search?q=${query}`, {
      signal: controller.signal
    }).then(r => r.json())

    console.log('搜索结果:', data)
  } catch (err) {
    if ((err as Error).name !== 'AbortError') {
      console.error('请求失败:', err)
    }
  }
})

10.5 nextTick 与 DOM 更新

10.5.1 DOM 异步更新机制

Vue 的 DOM 更新是异步的——当响应式数据变化时,Vue 不会立即更新 DOM,而是把更新操作放到一个队列里,然后在下一个微任务(microtask)中批量执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { ref } from 'vue'

const count = ref(0)

// 连续修改数据
count.value++
count.value++
count.value++
// 此时 DOM 还没有更新!

// 访问 DOM 的最新值,需要等 nextTick
console.log(document.querySelector('p')?.textContent)  // 旧值

// 正确做法:等 nextTick
import { nextTick } from 'vue'
count.value++
count.value++
count.value++

nextTick(() => {
  console.log(document.querySelector('p')?.textContent)  // 最新值
})

10.5.2 nextTick 原理(微任务队列)

nextTick 的实现依赖于 JavaScript 的微任务队列。它把回调函数包装成一个 Promise(或者用 queueMicrotask),在当前宏任务结束后、下一个宏任务开始前执行。

1
2
3
4
5
6
7
// nextTick 的等价实现
function nextTick(callback?: () => void) {
  return Promise.resolve().then(callback)
}

// 或者更精确的实现(Vue 实际用的是 MessageChannel / queueMicrotask)
queueMicrotask(callback)

这就是为什么 nextTick 总是在 DOM 更新后执行——因为 Vue 把 DOM 更新操作也放到了微任务队列里,而 nextTick 的回调会在所有 DOM 更新任务之后执行。

10.5.3 实际场景(聚焦、滚动、获取最新 DOM)

nextTick 最常用的三个场景:

 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 { ref, nextTick } from 'vue'

// 场景一:表单验证后聚焦到第一个错误字段
async function validateForm() {
  errors.value = await runValidation()
  if (errors.value.length > 0) {
    await nextTick()
    // DOM 已经更新,可以聚焦
    const firstErrorInput = document.querySelector('.input-error')
    ;(firstErrorInput as HTMLInputElement)?.focus()
  }
}

// 场景二:数据更新后滚动到列表底部
async function loadMoreComments() {
  comments.value.push(...newComments)
  await nextTick()
  // DOM 更新后,滚动到底部
  scrollContainer.value?.scrollTo({
    top: scrollContainer.value.scrollHeight,
    behavior: 'smooth'
  })
}

// 场景三:获取动态生成的元素的尺寸
async function measureElement() {
  show.value = true  // 显示一个之前隐藏的元素
  await nextTick()
  const height = containerRef.value?.offsetHeight
  console.log('元素高度:', height)
}

10.6 辅助 API

10.6.1 isRef / unref / isProxy / isReactive / isReadonly

Vue 提供了一系列类型判断工具函数,帮助你检查一个值是什么类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { ref, reactive, readonly, isRef, isProxy, isReactive, isReadonly, unref } from 'vue'

const count = ref(0)
const state = reactive({ name: '小明' })
const ro = readonly(state)

isRef(count)      // true —— count 是 ref
isRef(state)      // false —— state 是 reactive,不是 ref
isProxy(state)     // true —— state 是 Proxy(reactive 返回的就是 Proxy)
isReactive(state) // true —— 是 reactive 的 Proxy
isReadonly(ro)    // true —— 是 readonly 的 Proxy

// unref:如果值是 ref,返回 .value;否则直接返回值
const a = ref(10)
const b = 20
console.log(unref(a))  // 10
console.log(unref(b))  // 20

10.6.2 toRef / toRefs / toRaw

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { reactive, toRef, toRefs, toRaw } from 'vue'

const state = reactive({ count: 0, name: '小明' })

// toRef:从 reactive 对象取单个属性变成 ref
const countRef = toRef(state, 'count')
countRef.value++  // state.count 变成 1

// toRefs:把整个对象变成多个 ref
const { count, name } = toRefs(state)
count.value++  // state.count 变成 2

// toRaw:获取原始对象(绕过 Proxy)
const raw = toRaw(state)
raw.count = 999  // 不会触发响应式更新

10.6.3 customRef(自定义响应式)

customRef 允许你自定义响应式的追踪和触发逻辑——这在实现一些特殊的响应式行为时非常有用,比如防抖 ref。

 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
import { customRef } from 'vue'

// 创建一个防抖的 ref:只在停止输入 500ms 后才触发更新
function useDebouncedRef<T>(initialValue: T, delay = 500) {
  let timeout: ReturnType<typeof setTimeout>

  return customRef((track, trigger) => {
    return {
      get() {
        track()  // 告诉 Vue:这个值需要被追踪
        return initialValue
      },
      set(value: T) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          initialValue = value
          trigger()  // 告诉 Vue:触发更新
        }, delay)
      }
    }
  })
}

// 使用这个自定义 ref
const searchQuery = useDebouncedRef('', 300)

track() 必须在 get 里调用,告诉 Vue “这个值在 get 里被读取了,需要追踪它的变化”。trigger()set 里调用,告诉 Vue “值变了,需要重新渲染”。

10.7 应用级 API(createApp)

10.7.1 app.use(插件注册)

app.use() 用来注册 Vue 插件。插件是一个包含 install 方法的对象(或者是一个返回 Promise 的函数),它可以在应用实例上注册全局功能。

 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
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

const app = createApp(App)

// 注册路由插件
app.use(router)

// 注册 Pinia 状态管理插件
app.use(createPinia())

// 注册自定义插件
app.use({
  install(app) {
    // 全局组件注册
    app.component('MyGlobalButton', MyButton)
    // 全局指令注册
    app.directive('focus', FocusDirective)
    // 全局 mixin(不推荐)
    app.mixin({ /* ... */ })
    // 应用级别的配置
    app.config.globalProperties.$myUtils = { /* ... */ }
  }
})

app.mount('#app')

10.7.2 app.component / app.directive(全局组件/指令注册)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components/MyButton.vue'
import myFocus from './directives/myFocus'

const app = createApp(App)

// 全局注册组件(不需要 import 就能在模板里用)
app.component('MyButton', MyButton)

// 全局注册自定义指令
app.directive('focus', myFocus)
app.directive('click-outside', ClickOutsideDirective)

// 一次性注册多个
app
  .component('HeaderNav', HeaderNav)
  .component('FooterNav', FooterNav)
  .component('SideBar', SideBar)
  .directive('resize', ResizeDirective)

app.mount('#app')

10.7.3 app.mount / app.unmount(挂载与卸载)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 挂载到 #app
app.mount('#app')

// 卸载(从 DOM 中移除应用实例)
// app.unmount()

10.7.4 app.config(全局配置)

 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
const app = createApp(App)

// 全局错误处理器
app.config.errorHandler = (err, instance, info) => {
  console.error('全局错误:', err)
  console.error('组件实例:', instance)
  console.error('错误信息:', info)
}

// 警告处理器(开发环境)
app.config.warnHandler = (msg, instance, trace) => {
  // 可以压制某些警告
  if (msg.includes('Avoided redundant Vue')) return
  console.warn(msg)
}

// 是否启用性能分析
app.config.performance = true  // 需要 Vue 3.2+ 并开启 devtools

// 全局属性(类似 Vue 2 的 Vue.prototype)
app.config.globalProperties.$myUtils = {
  formatDate: (date: Date) => date.toLocaleDateString()
}

// 是否允许开发者工具
app.config.devtools = true

app.mount('#app')

10.7.5 app.provide / app.version

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { createApp, inject } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 在应用级别 provide 数据(所有后代组件都能 inject)
app.provide('user', reactive({ name: '小明', role: 'admin' }))
app.provide('appVersion', '1.0.0')

// 访问 Vue 版本号
console.log(app.version)  // 3.4.x

app.mount('#app')

// 在组件里使用
// const user = inject('user')

本章小结

本章我们深入了 Vue 3 响应式系统的"高级 API":

  • shallowRef / shallowReactive:浅层响应式,只追踪第一层变化,用于优化大数据量场景。
  • readonly / markRaw:只读和绕过响应式,用于不可变数据和第三方库实例。
  • toRaw:获取 Proxy 背后的原始对象,绕过响应式直接操作。
  • 模板 ref:用 ref 获取 DOM 元素和子组件实例,useTemplateRef(Vue 3.5+)简化了这个过程。
  • watch 进阶flush 控制触发时机,once 只触发一次,onWatcherCleanup 处理清理逻辑。
  • nextTick:等待 DOM 异步更新完成后执行回调,原理是基于微任务队列。
  • 辅助 API:isRef / isProxy / isReactive / isReadonly / toRaw / toRef / toRefs / customRef。
  • 应用级 API:createApp 的插件注册、全局组件/指令注册、全局配置、app.provide。

下一章我们会学习 Composables(组合式函数)——这是 Vue 3 最核心的代码复用模式,也是 Vue 社区最活跃的领域之一!