重新学习前端之Vue3

2 阅读25分钟

Vue3

一、Composition API 基础

1. 什么是 Composition API?为什么引入?

定义: Composition API(组合式 API)是 Vue3 引入的一组新的 API,允许开发者使用 setup 函数和一系列响应式 API 来组织组件逻辑。

引入原因:

  • Vue2 的 Options API 在处理大型组件时,逻辑关注点分散在不同选项(data、methods、computed、watch)中
  • 代码复用困难,mixins 存在命名冲突、来源不清晰等问题
  • TypeScript 支持不佳

原理: Composition API 基于函数式的思想,将相关逻辑组织在一起(函数),通过闭包和响应式系统实现数据绑定和更新。

示例:

// Options API - 逻辑分散
export default {
  data() {
    return { count: 0, name: 'vue' }
  },
  methods: {
    increment() { this.count++ },
    logName() { console.log(this.name) }
  }
}

// Composition API - 逻辑聚合
import { ref } from 'vue'

export default {
  setup() {
    // 计数逻辑
    const count = ref(0)
    const increment = () => count.value++

    // 名称逻辑
    const name = ref('vue')
    const logName = () => console.log(name.value)

    return { count, increment, name, logName }
  }
}

常见误区:

  • Composition API 不是完全替代 Options API,两者可以混合使用
  • setup 函数中不能使用 this(此时组件实例还未创建)
  • 不要在 setup 中使用解构响应式对象,会导致响应性丢失(需用 toRefs)

2. Composition API 与 Options API 的区别

Options API 定义: 通过 data、methods、computed、watch 等选项来组织组件逻辑。

Composition API 定义: 通过 setup 函数和响应式 API 在函数内组织逻辑。

对比表格:

维度Options APIComposition API
代码组织按选项类型(data/methods/computed)分散按逻辑功能聚合
逻辑复用Mixins(命名冲突、来源不清)Composables/自定义 Hooks
TypeScript 支持较差,需要装饰器或复杂类型推导原生支持,类型推导友好
this 指向依赖 this,容易混淆不依赖 this,纯函数式
打包体积全量引入Tree-shaking 友好
学习曲线低,结构清晰较陡,需要理解响应式原理
适用场景小型简单组件中大型复杂组件、组件库

选择策略:

  • 小项目/简单组件:Options API 更简洁
  • 大型项目/复杂逻辑:Composition API 更易维护
  • 需要 TypeScript:优先 Composition API
  • 需要逻辑复用:Composition API 的 Composables 更优

3. setup 函数

定义: setup 是 Composition API 的入口函数,在组件创建之前执行,用于创建响应式数据、计算属性、方法等。

执行时机:beforeCreate 之前执行,此时组件实例尚未创建,所以 setup 中不能使用 this

参数:

  1. props - 父组件传递的 props,是响应式的
  2. context - 上下文对象,包含 attrsslotsemit

返回值:

  • 返回的对象会在模板中可用
  • 返回渲染函数可完全手动控制渲染
  • 如果使用 <script setup> 则不需要显式返回

示例:

export default {
  props: { title: String },
  setup(props, context) {
    const { attrs, slots, emit } = context

    const count = ref(0)
    const handleClick = () => {
      emit('click', count.value)
    }

    return { count, handleClick }
  }
}

注意事项:

  • setup 是同步函数,不能使用 async
  • props 是响应式的,但不能解构(解构会失去响应性)
  • 生命周期钩子需要在 setup 中注册(如 onMounted)

4. ref

定义: ref 用于创建基本类型(或任意类型)的响应式数据。

原理: ref 接收一个值,返回一个响应式对象,该对象只有一个 .value 属性。在模板中使用时,Vue 会自动解包,不需要 .value

示例:

import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const name = ref('Vue3')
    const obj = ref({ age: 20 }) // 也可以包裹对象

    console.log(count.value) // 0

    const increment = () => {
      count.value++
    }

    return { count, name, increment }
  }
}

常见误区:

  • 在 JS 中访问必须用 .value,在模板中自动解包
  • ref 包裹对象时,内部会自动调用 reactive 转换
  • 不要用解构来获取 ref 的值,应直接操作 .value

5. reactive

定义: reactive 用于创建对象类型的响应式代理。

原理: 基于 ES6 Proxy 实现,返回一个深层响应式的代理对象。

示例:

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      name: 'Vue3',
      user: { age: 20 }
    })

    const increment = () => {
      state.count++
    }

    return { state, increment }
  }
}

常见误区:

  • reactive 只能用于对象类型(不能用于 string、number、boolean)
  • 不能直接替换 reactive 对象,会失去响应性
    // 错误
    state = reactive({ count: 1 }) // 失去响应性
    
    // 正确
    Object.assign(state, { count: 1 })
    
  • 解构 reactive 对象会失去响应性,需用 toRefs

6. ref 与 reactive 的区别

对比表格:

维度refreactive
支持类型任意类型仅对象类型
访问方式需要 .value(JS中)直接访问
替换整个值支持不支持
内部实现底层用 reactive(对象时)基于 Proxy
模板使用自动解包不需要解包
适用场景基本类型、需要替换整个值复杂对象结构

使用策略:

  • 基本类型用 ref
  • 需要整体替换值用 ref
  • 复杂对象用 reactive(语义更好)
  • 统一用 ref 也是常见做法(更一致)

7. toRef 与 toRefs

toRef 定义: 为 reactive 对象上的某个属性创建一个 ref,保持响应性连接。

toRefs 定义: 将 reactive 对象转换为普通对象,每个属性都是对应的 ref。

区别对比:

维度toReftoRefs
处理范围单个属性全部属性
返回值一个 ref包含多个 ref 的普通对象
使用场景解构单个属性时解构整个 reactive 对象时

示例:

import { reactive, toRef, toRefs } from 'vue'

const state = reactive({ foo: 1, bar: 2 })

// toRef - 单个属性
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2

// toRefs - 全部属性(常用于 return 时解构)
const { foo, bar } = toRefs(state)
// 现在 foo 和 bar 都是 ref,且与 state 保持连接

常见误区:

  • toRef 创建的 ref 会随源对象变化而变化
  • toRefs 只对 reactive 对象有意义
  • 在 setup return 时必须用 toRefs 才能安全解构

8. shallowRef 与 shallowReactive

shallowRef 定义: 只追踪 .value 的赋值操作,不会对值进行深度响应式转换。

shallowReactive 定义: 只代理对象的第一层属性,不会深层代理嵌套对象。

与 ref/reactive 的区别:

API响应式深度适用场景
ref深层(对象时)需要深度响应的任意类型
shallowRef仅 .value 赋值大型数据结构、不可变数据
reactive深层代理需要深度响应的对象
shallowReactive仅第一层仅需追踪顶层属性的对象

示例:

import { shallowRef, shallowReactive } from 'vue'

// shallowRef - 只有 .value 赋值才触发更新
const data = shallowRef({ a: 1, b: { c: 2 } })
data.value.a = 10       // 不会触发更新
data.value = { a: 2 }   // 会触发更新

// shallowReactive - 只有第一层响应
const state = shallowReactive({ a: 1, nested: { b: 2 } })
state.a = 10            // 会触发更新
state.nested.b = 20     // 不会触发更新

使用场景:

  • 处理大型列表数据时避免深层代理开销
  • 使用不可变数据模式时
  • 第三方库对象不需要响应式时

9. readonly 与 shallowReadonly

readonly 定义: 创建一个深层只读代理,任何层级都不能被修改。

shallowReadonly 定义: 创建一个浅层只读代理,只有第一层不可修改。

示例:

import { readonly, shallowReadonly, reactive } from 'vue'

const state = reactive({ a: 1, b: { c: 2 } })

const readOnlyState = readonly(state)
readOnlyState.a = 2        // 警告:Cannot set
readOnlyState.b.c = 3      // 警告:Cannot set(深层只读)

const shallowRO = shallowReadonly(state)
shallowRO.a = 2            // 警告
shallowRO.b.c = 3          // 可以修改(不警告)

使用场景:

  • 暴露给外部但不希望被修改的数据
  • 作为 props 传递给子组件时保护数据

10. 响应式判断 API

API作用示例
isRef(value)判断是否为 refisRef(ref(0)) → true
isReactive(value)判断是否为 reactive 代理isReactive(reactive({})) → true
isReadonly(value)判断是否为只读代理isReadonly(readonly({})) → true
isProxy(value)判断是否为 Proxy(reactive/readonly)isProxy(reactive({})) → true

示例:

import { ref, reactive, readonly, isRef, isReactive, isReadonly, isProxy } from 'vue'

const r = ref(0)
const re = reactive({ a: 1 })
const ro = readonly({ b: 2 })

isRef(r)        // true
isReactive(r)   // false(ref不是reactive)
isProxy(r)      // false

isReactive(re)  // true
isProxy(re)     // true

isReadonly(ro)  // true
isProxy(ro)     // true

11. unref 与 proxyRefs

unref 定义: 如果参数是 ref 则返回其 .value,否则返回参数本身。是 val = isRef(val) ? val.value : val 的语法糖。

proxyRefs 定义: 返回一个代理,访问时自动解包 ref,修改时自动设置 .value

示例:

import { ref, unref, proxyRefs } from 'vue'

// unref
const count = ref(0)
unref(count)  // 0
unref(10)     // 10

// proxyRefs - 常用于 return 对象时自动解包 ref
const user = {
  name: ref('vue'),
  age: ref(3)
}
const reactiveUser = proxyRefs(user)
console.log(reactiveUser.name) // 'vue'(自动解包)
reactiveUser.age = 4           // 自动设置 value

12. markRaw 与 toRaw

markRaw 定义: 标记一个对象,使其永远不会被转换为代理。

toRaw 定义: 获取 reactive 或 readonly 代理的原始对象。

示例:

import { reactive, markRaw, toRaw } from 'vue'

// markRaw - 阻止响应式转换
const rawObj = markRaw({ a: 1 })
const state = reactive({ obj: rawObj })
// state.obj 不是代理

// toRaw - 获取原始对象
const state2 = reactive({ a: 1 })
const raw = toRaw(state2)
// raw === 原始对象,修改 raw 不会触发视图更新

使用场景:

  • markRaw:第三方实例(如图表实例)、大型不可变列表
  • toRaw:需要直接操作底层数据而不触发更新时

二、响应式原理

13. Vue3 响应式原理

原理概述: Vue3 基于 ES6 Proxy 实现响应式系统。

核心流程:

  1. 创建代理:使用 reactive()ref() 创建 Proxy 对象
  2. 依赖收集:访问属性时,通过 get 拦截器收集当前活跃的 effect(依赖)
  3. 触发更新:修改属性时,通过 set 拦截器触发收集的 effect 执行
  4. 深度响应:在 get 中递归将嵌套对象也转为响应式代理

源码核心逻辑:

function createReactiveObject(target, shallow = false) {
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, TrackOpTypes.GET, key)
      
      const res = Reflect.get(target, key, receiver)
      
      // 深度响应式转换
      if (!shallow && isObject(res)) {
        return reactive(res)
      }
      return res
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      
      // 触发更新
      if (oldValue !== value) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
      return result
    }
  })
  return proxy
}

核心模块:

  • reactive / ref:创建响应式数据
  • track:依赖收集,建立 target-key-effect 的映射
  • trigger:触发更新,执行收集的 effect
  • effect:副作用函数,负责创建和响应更新

14. Proxy 与 Object.defineProperty 的区别

Object.defineProperty 定义: Vue2 使用的响应式方案,通过劫持对象的 getter/setter 实现。

Proxy 定义: Vue3 使用的响应式方案,是 ES6 提供的代理对象,可以拦截多种操作。

对比表格:

维度Object.definePropertyProxy
支持监听只能监听已知属性可监听任意属性(包括新增/删除)
数组支持需要重写数组方法原生支持数组索引和 length
Map/Set不支持原生支持
性能初始化时需递归遍历所有属性懒代理,访问时才转换
新属性添加需要 Vue.set直接支持
删除属性需要 Vue.delete直接支持
兼容性支持 IE不支持 IE

为什么 Vue3 选择 Proxy:

  • 彻底解决 Vue2 响应式系统的局限性
  • 性能更好,按需代理而非全量递归
  • API 更丰富,支持 13 种拦截操作
  • 更好地支持 Map、Set 等新数据结构

15. Proxy 的拦截操作

Proxy 可以拦截以下操作:

const handler = {
  get(target, prop, receiver) {},        // 读取属性
  set(target, prop, value, receiver) {}, // 设置属性
  has(target, prop) {},                  // in 操作符
  deleteProperty(target, prop) {},       // delete 操作符
  ownKeys(target) {},                    // Object.keys 等
  getOwnPropertyDescriptor(target, prop) {},
  defineProperty(target, prop, desc) {},
  preventExtensions(target) {},
  getPrototypeOf(target) {},
  setPrototypeOf(target, proto) {},
  isExtensible(target) {},
  apply(target, thisArg, args) {},       // 函数调用
  construct(target, args) {}             // new 操作符
}

16. Reflect 与 Proxy 配合

Reflect 定义: ES6 提供的内置对象,提供拦截 JavaScript 操作的方法,与 Proxy 方法一一对应。

配合使用原因:

  1. 保证 this 指向正确
  2. 提供默认行为(保证原有功能正常)
  3. 函数式 API,更易于组合

示例:

const proxy = new Proxy(target, {
  get(target, key, receiver) {
    // 依赖收集
    track(target, key)
    // 使用 Reflect 保证 this 指向和默认行为
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    // 触发更新
    trigger(target, key)
    return result
  }
})

17. 如何停止响应式对象的响应性?

// 方法1: 使用 markRaw 标记(防止转换)
const obj = markRaw({ a: 1 })

// 方法2: 使用 toRaw 获取原始对象(不触发更新)
const raw = toRaw(reactiveObj)

// 方法3: 替换为非响应式对象
state = { ...toRaw(state) }

// 方法4: 使用 shallowRef(仅 .value 改变才响应)
const data = shallowRef(largeObject)

三、生命周期

18. Vue3 生命周期钩子

生命周期概览: Vue 组件从创建到销毁经历的一系列阶段。

Vue3 中的生命周期钩子(Composition API):

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered
} from 'vue'

执行顺序与说明:

创建阶段:
  setup() → onBeforeMount() → onMounted()

更新阶段:
  onBeforeUpdate() → onUpdated()

销毁阶段:
  onBeforeUnmount() → onUnmounted()

Keep-alive:
  onActivated() → onDeactivated()

调试钩子:
  onRenderTracked() → onRenderTriggered()

错误捕获:
  onErrorCaptured()

示例:

import { ref, onMounted, onBeforeUnmount } from 'vue'

export default {
  setup() {
    const count = ref(0)

    onMounted(() => {
      console.log('组件已挂载')
    })

    onBeforeUnmount(() => {
      console.log('组件即将卸载')
    })

    return { count }
  }
}

19. Vue3 与 Vue2 生命周期的区别

对应关系表:

Vue2 Options APIVue3 Options APIVue3 Composition API说明
beforeCreatebeforeCreatesetup组件初始化前
createdcreatedsetup组件创建完成
beforeMountbeforeMountonBeforeMountDOM 挂载前
mountedmountedonMountedDOM 挂载后
beforeUpdatebeforeUpdateonBeforeUpdate数据变化,DOM更新前
updatedupdatedonUpdatedDOM 更新后
beforeDestroybeforeUnmountonBeforeUnmount组件销毁前
destroyedunmountedonUnmounted组件销毁后
activatedactivatedonActivatedkeep-alive 激活
deactivateddeactivatedonDeactivatedkeep-alive 停用
errorCapturederrorCapturedonErrorCaptured捕获子组件错误
--onRenderTracked调试:追踪依赖
--onRenderTriggered调试:触发重新渲染

关键变化:

  1. beforeDestroybeforeUnmountdestroyedunmounted
  2. beforeCreatecreatedsetup 替代
  3. 新增了调试钩子 onRenderTrackedonRenderTriggered

20. 各生命周期钩子详解

onBeforeMount / onMounted:

onBeforeMount(() => {
  // DOM 还未挂载,不能操作 DOM
})

onMounted(async () => {
  // DOM 已挂载,可以操作 DOM
  // 适合:发起请求、初始化第三方库
  const el = document.getElementById('app')
  const data = await fetchData()
})

onBeforeUpdate / onUpdated:

onBeforeUpdate(() => {
  // DOM 更新前,可以访问旧 DOM
})

onUpdated(() => {
  // DOM 更新后,可以访问新 DOM
  // 注意:避免在此修改状态,可能导致无限循环
})

onBeforeUnmount / onUnmounted:

onBeforeUnmount(() => {
  // 清理即将被销毁的资源
})

onUnmounted(() => {
  // 组件已销毁
  // 适合:清理事件监听器、定时器、取消请求
  clearInterval(timer)
  window.removeEventListener('resize', handler)
})

onActivated / onDeactivated(keep-alive):

onActivated(() => {
  // 组件被 keep-alive 激活时调用
})

onDeactivated(() => {
  // 组件被 keep-alive 缓存时调用
})

onErrorCaptured:

onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err)
  console.error('错误组件:', instance)
  console.error('错误信息:', info)
  return false // 阻止错误继续向上传播
})

四、watch、computed 与 effectScope

21. watch

定义: 侦听一个或多个响应式数据源,数据变化时执行回调。

使用方式:

import { ref, reactive, watch } from 'vue'

// 侦听单个 ref
const count = ref(0)
watch(count, (newVal, oldVal) => {
  console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})

// 侦听 reactive 对象的某个属性
const state = reactive({ name: 'vue' })
watch(() => state.name, (newVal, oldVal) => {
  console.log(newVal)
})

// 侦听多个数据源
const name = ref('')
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('多个数据变化了')
})

// 侦听整个 reactive 对象(需要函数返回)
watch(
  () => ({ ...state }),
  (newState, oldState) => {
    console.log('整个对象变化')
  },
  { deep: true }
)

配置选项:

watch(source, callback, {
  immediate: true,      // 立即执行一次
  deep: true,           // 深度侦听
  flush: 'pre',         // 'pre' | 'post' | 'sync'
  onTrack(e) {},        // 调试:依赖追踪时调用
  onTrigger(e) {}       // 调试:依赖触发时调用
})

22. watchEffect

定义: 立即执行一个函数,自动追踪其响应式依赖,依赖变化时重新执行。

与 watch 的区别:

维度watchwatchEffect
立即执行否(除非 immediate: true)
手动指定依赖需要自动追踪
访问旧值可以(回调参数)不可以
适用场景明确知道侦听什么多个依赖联动执行

示例:

import { ref, watchEffect } from 'vue'

const id = ref(1)
const data = ref(null)

// 自动追踪 id 和 data
watchEffect(() => {
  console.log(`Fetching data for id: ${id.value}`)
  // 自动收集 id.value 作为依赖
  fetchData(id.value).then(res => {
    data.value = res
  })
})

// 停止侦听
const stop = watchEffect(() => {
  console.log(count.value)
})
stop() // 停止

23. watch 与 watchEffect 的区别(详细对比)

维度watchwatchEffect
语法需要指定数据源和回调只需要一个回调
懒执行默认懒执行立即执行
依赖追踪手动指定自动收集
旧值访问支持不支持
性能更精确,只侦听指定数据可能收集多余依赖
使用场景需要旧值、明确数据源简单场景、多依赖联动

选择策略:

  • 需要访问旧值 → watch
  • 明确知道侦听目标 → watch
  • 自动追踪多个依赖 → watchEffect
  • 需要副作用立即执行 → watchEffect

24. computed

定义: 计算属性,基于响应式数据计算得出新值,具有缓存特性。

原理: 只有在依赖的响应式数据变化时才重新计算,否则返回缓存值。

使用方式:

import { ref, computed } from 'vue'

const count = ref(0)

// 只读计算属性
const doubleCount = computed(() => count.value * 2)

// 可读写计算属性
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => { count.value = val - 1 }
})

console.log(doubleCount.value) // 0
count.value = 5
console.log(doubleCount.value) // 10(重新计算)

25. computed 与 watch 的区别

维度computedwatch
返回值有(计算结果)无(执行副作用)
缓存有(依赖不变不重新计算)无(每次变化都执行)
用途计算派生数据执行异步操作、副作用
同步性必须同步可以异步
性能更高效(惰性求值+缓存)较低(每次变化都执行)

26. effectScope 与 onScopeDispose

effectScope 定义: 创建一个效应作用域,统一管理其中的响应式效应(computed、watch、watchEffect)。

onScopeDispose 定义: 在当前 effectScope 被销毁时执行的回调。

使用场景: 在组合函数(Composables)中统一清理副作用。

import { effectScope, ref, watch, onScopeDispose } from 'vue'

function useFeature() {
  const count = ref(0)
  
  // 在同一个 scope 内创建的所有效应
  const scope = effectScope()
  scope.run(() => {
    watch(count, () => console.log('count changed'))
    // 其他 watch、computed 等
  })
  
  // 组件卸载时统一清理
  onScopeDispose(() => {
    console.log('scope disposed')
  })
  
  return {
    count,
    dispose: () => scope.stop()
  }
}

五、Teleport、Suspense 与 Fragments

27. Teleport

定义: Teleport(传送门)组件允许将组件的 HTML 渲染到 DOM 树中的其他位置,而非组件自身的 DOM 层级。

原理: 在 vnode 渲染时,将内容挂载到目标 DOM 节点下,但逻辑上仍属于当前组件。

属性:

  • to - 目标位置(CSS 选择器或 DOM 元素)
  • disabled - 禁用传送功能

使用场景:

  • 模态框(Modal)渲染到 body
  • 全局通知提示
  • 弹出菜单、Tooltip

示例:

<template>
  <button @click="showModal = true">打开模态框</button>

  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <p>这是模态框内容</p>
      <button @click="showModal = false">关闭</button>
    </div>
  </Teleport>
</template>

<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>

注意事项:

  • to 必须在 Teleport 挂载前存在于 DOM 中
  • 可以配合 disabled 条件控制是否传送
  • 逻辑仍在父组件中,props 和 events 正常传递

28. Suspense

定义: Suspense 用于处理异步组件的加载状态,显示加载中/加载失败的 UI。

原理: 等待其树中的异步依赖(如 async setup)完成后,才渲染 #default 插槽。等待期间显示 #fallback 插槽。

插槽:

  • #default - 异步加载完成后的内容
  • #fallback - 等待加载时显示的内容

示例:

<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import AsyncComponent from './AsyncComponent.vue'
</script>

异步组件示例:

// AsyncComponent.vue
export default {
  async setup() {
    const data = await fetch('/api/data').then(res => res.json())
    return { data }
  }
}

注意事项:

  • Suspense 在 Vue3 中仍是实验性功能
  • 可以嵌套使用
  • 支持 @resolve@pending 事件

29. Fragments(片段)与多根节点

定义: Fragments 允许组件拥有多个根节点,不需要额外的包裹元素。

Vue2 的限制: 组件模板必须有且仅有一个根节点。

Vue3 的改进: 支持多个根节点,自动用 Fragment 包裹。

示例:

<!-- Vue3 多根节点组件 -->
<template>
  <header>头部</header>
  <main>内容</main>
  <footer>底部</footer>
</template>

注意事项:

  • 多根节点组件需要显式声明 inheritAttrs 的行为
  • 如果有多个根节点,attrs 不会自动继承,需要通过 $attrs 手动绑定
  • 可以用 v-bind="$attrs" 指定继承位置
<template>
  <header>头部</header>
  <main v-bind="$attrs">内容</main>
</template>

六、script setup 与 define 宏

30. script setup 语法糖

定义: <script setup> 是 Composition API 的语法糖,在编译时自动处理 setup 函数的内容。

作用:

  • 更简洁的语法,不需要返回
  • 更好的 TypeScript 支持
  • 更好的运行时性能(编译为 render 函数)
  • 可以使用 define 宏(defineProps、defineEmits 等)

与 setup 函数的区别:

维度setup() 函数<script setup>
返回值需要显式 return自动暴露顶层绑定
组件注册需要手动注册自动注册导入的组件
宏函数不可用可用(defineProps 等)
性能需要运行时处理编译时优化
TS 支持需要 defineComponent自动推导

示例:

<script setup>
import { ref, computed } from 'vue'
import ChildComponent from './ChildComponent.vue'

const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>

<template>
  <ChildComponent :count="count" @click="increment" />
</template>

31. defineProps

定义:<script setup> 中声明组件 props 的编译器宏。

<script setup>
// 方式1:运行时声明
const props = defineProps({
  title: String,
  count: { type: Number, default: 0 }
})

// 方式2:纯类型声明(推荐,更好的 TS 支持)
const props = defineProps<{
  title: string
  count?: number
}>()

// 方式3:使用 withDefaults 设置默认值
const props = withDefaults(defineProps<{
  title: string
  count?: number
}>(), {
  count: 0
})

console.log(props.title)
</script>

注意事项:

  • props 是响应式的,但不能直接解构
  • 使用 toRefs 解构保持响应性
<script setup>
import { toRefs } from 'vue'
const { title } = toRefs(defineProps<{ title: string }>())
</script>

32. defineEmits

定义:<script setup> 中声明组件事件的编译器宏。

<script setup>
// 方式1:运行时声明
const emit = defineEmits(['update', 'delete'])

// 方式2:类型声明
const emit = defineEmits<{
  (e: 'update', id: number): void
  (e: 'delete', id: number, name: string): void
}>()

emit('update', 1)
emit('delete', 1, 'item')
</script>

33. defineExpose

定义: 显式暴露组件内部的属性或方法,供父组件通过模板引用访问。

注意: <script setup> 中组件默认关闭,不暴露任何内容。

<!-- Child.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
const increment = () => count.value++

defineExpose({
  count,
  increment
})
</script>

<!-- Parent.vue -->
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

onMounted(() => {
  childRef.value.count      // 0
  childRef.value.increment() // 1
})
</script>

<template>
  <Child ref="childRef" />
</template>

34. useSlots 与 useAttrs

useSlots 定义:<script setup> 中获取插槽对象。

useAttrs 定义:<script setup> 中获取 attrs 对象。

与 props 的区别:

  • props 是显式声明的属性
  • attrs 是未声明的属性(如 class、style、自定义事件等)
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

console.log(slots.default)  // 默认插槽
console.log(attrs.class)    // 传入的 class
console.log(attrs.onClick)  // 传入的事件
</script>

七、Vue3 新特性与变化

35. v-model 的变化

Vue2 的 v-model:

  • 默认使用 value prop 和 input 事件
  • 一个组件只能有一个 v-model
  • 修饰符:.lazy.number.trim

Vue3 的 v-model:

  • 默认使用 modelValue prop 和 update:modelValue 事件
  • 支持多个 v-model 绑定
  • 自定义参数名和修饰符

示例:

<!-- 多个 v-model -->
<ChildComponent v-model:title="title" v-model:content="content" />

<!-- ChildComponent.vue -->
<script setup>
defineProps(['modelValue', 'title', 'content'])
defineEmits(['update:modelValue', 'update:title', 'update:content'])
</script>

<!-- 自定义修饰符 -->
<ChildComponent v-model.capitalize="name" />

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})
// props.modelModifiers = { capitalize: true }
</script>

36. 自定义指令

Vue3 的变化: 生命周期钩子与组件生命周期一致。

钩子函数对比:

Vue2Vue3触发时机
bindbeforeMount指令绑定到元素前
insertedmounted元素插入父节点
updateupdated组件更新后
componentUpdatedupdated组件及子组件更新
unbindunmounted指令解绑

示例:

const vFocus = {
  mounted: (el) => el.focus()
}

// 全局注册
app.directive('focus', vFocus)

// 局部注册
<script setup>
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

指令钩子参数:

const directive = {
  mounted(el, binding, vnode, prevVnode) {
    // el: 绑定元素
    // binding: 包含 name、value、oldValue、modifiers、arg
    // vnode: 虚拟节点
    // prevVnode: 上一个虚拟节点
  }
}

37. provide 与 inject

定义: 依赖注入机制,父组件提供数据,后代组件注入使用,跨层级传递数据。

Composition API 中的用法:

<!-- 父组件 -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const toggleTheme = () => {
  theme.value = theme.value === 'dark' ? 'light' : 'dark'
}

provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>

<!-- 子/孙组件 -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme', 'light') // 'light' 是默认值
const toggleTheme = inject('toggleTheme')
</script>

注意事项:

  • provide 的值是响应式的,后代组件会响应变化
  • 只能注入祖先组件提供的值
  • 可以注入函数来实现子组件向父组件通信

38. 过滤器(Filter)的移除

Vue2:

<template>
  <p>{{ message | capitalize }}</p>
</template>

Vue3: 过滤器已移除,推荐用 computed 或方法替代。

<template>
  <p>{{ capitalizedMessage }}</p>
</template>

<script setup>
import { computed } from 'vue'

const message = ref('hello')
const capitalizedMessage = computed(() =>
  message.value.charAt(0).toUpperCase() + message.value.slice(1)
)
</script>

39. 全局 API 的变化

Vue2 全局 API:

Vue.component()
Vue.directive()
Vue.use()
Vue.mixin()
Vue.filter()
Vue.prototype.$http = axios

Vue3 的变化: 改为通过 createApp 返回的 app 实例调用。

import { createApp } from 'vue'

const app = createApp(App)

app.component('MyComp', MyComp)
app.directive('focus', focusDirective)
app.use(router)
app.use(store)
app.mixin(myMixin)
app.provide('key', value)

// 全局属性
app.config.globalProperties.$http = axios

app.mount('#app')

app.config 配置:

app.config.globalProperties.$http = axios
app.config.errorHandler = (err) => { console.error(err) }
app.config.warnHandler = (msg) => { console.warn(msg) }

40. h 函数与 render 函数

h 函数定义: 用于创建虚拟节点(vnode)的函数。

render 函数: 返回 vnode 的函数,用于手动控制渲染逻辑。

示例:

import { h, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    return () => h('div', {
      id: 'app',
      onClick: () => count.value++
    }, `Count: ${count.value}`)
  }
}

// 更复杂的示例
import { h, resolveComponent } from 'vue'

export default {
  render() {
    return h('div', [
      h('h1', this.title),
      h(resolveComponent('my-button'), { onClick: this.handleClick })
    ])
  }
}

41. defineComponent

定义: 用于定义组件的辅助函数,提供完整的 TypeScript 类型推导。

作用:

  • 提供类型推断(props、emits、slots)
  • 支持 IDE 代码补全
  • 在 Options API 中保持类型安全
import { defineComponent, ref } from 'vue'

export default defineComponent({
  props: {
    title: { type: String, required: true }
  },
  emits: ['click'],
  setup(props, { emit }) {
    const count = ref(0)
    return { count }
  }
})

注意:<script setup> 中不需要 defineComponent。


八、Vue3 与 Vue2 对比

42. Vue3 相比 Vue2 的主要改进

1. 性能提升

  • 打包体积减少 41%(119KB → 70KB)
  • 初次渲染快 55%,更新快 133%
  • 内存使用减少 54%

2. Composition API

  • 更好的逻辑复用和组织
  • 替代 mixins 的方案
  • 更好的 TypeScript 支持

3. 响应式系统重构

  • 从 Object.defineProperty 改为 Proxy
  • 支持 Map、Set、数组索引
  • 懒代理,性能更好

4. 编译优化

  • 静态提升(Static Hoisting)
  • 预字符串化(Static Stringification)
  • PatchFlag 静态标记
  • Tree-shaking 支持

5. 新特性

  • Teleport(传送门)
  • Suspense(异步加载)
  • Fragments(多根节点)
  • <script setup> 语法糖

6. TypeScript 重写

  • 完整的类型定义
  • 更好的 IDE 支持

43. Vue 与 React 的异同点

相同点:

  • 都是组件化框架
  • 都使用虚拟 DOM
  • 都支持响应式/状态驱动视图
  • 都有组件生命周期

对比表格:

维度VueReact
模板模板语法(HTML-like)JSX(JavaScript-like)
数据流双向绑定(v-model)单向数据流
响应式自动追踪依赖手动 setState/useState
状态管理Pinia/VuexRedux/MobX
路由Vue RouterReact Router
学习曲线平缓较陡
灵活性适中更高
性能优化更好(编译时)依赖开发者优化
生态官方维护社区驱动

选择策略:

  • 快速开发、中小企业项目 → Vue
  • 大型项目、高度灵活需求 → React
  • 已有团队技术栈 → 优先考虑

44. Template 与 JSX 的性能对比

结论: Vue 的 Template 通常比 JSX 性能更好。

原因:

  1. 编译时优化:Template 可以在编译阶段进行静态分析
  2. 静态标记(PatchFlag):编译器标记动态内容,diff 时跳过静态部分
  3. 静态提升:静态节点只创建一次
  4. 预字符串化:连续静态节点转为字符串

JSX 的劣势: 运行时处理,无法编译时优化。


45. Vue 的优点与缺点

优点:

  • 渐进式框架,可逐步引入
  • 模板语法简单直观
  • 双向数据绑定简化开发
  • 官方工具链完善(Vite、Router、Pinia)
  • 文档优秀,学习曲线低
  • 社区活跃

缺点:

  • 生态不如 React 丰富
  • 大厂使用率相对低
  • 小版本更新频繁,可能不稳定
  • TypeScript 支持早期不如 React

46. 为什么 Vue 是渐进式框架

定义: 渐进式意味着可以按需使用,逐步深入。

渐进式体现:

  1. 核心库:只使用视图层(模板 + 数据绑定)
  2. 加路由:引入 Vue Router
  3. 加状态管理:引入 Pinia
  4. 加工具链:使用 Vite + CLI
  5. 加服务端渲染:使用 Nuxt

每个阶段可以独立使用,不需要全部引入。


九、性能优化

47. Vue3 编译优化

静态提升(Static Hoisting):

// Vue2 - 每次都重新创建
render() {
  return h('div', [
    h('span', 'static'),   // 静态节点
    h('span', this.msg)    // 动态节点
  ])
}

// Vue3 - 静态节点提升到渲染函数外部
const _hoisted_1 = h('span', 'static')

render() {
  return h('div', [
    _hoisted_1,             // 复用
    h('span', this.msg, 1)  // PatchFlag = 1(TEXT)
  ])
}

预字符串化(Static Stringification): 大量连续静态节点直接转为字符串,跳过虚拟 DOM 创建。

缓存事件处理函数:

// Vue2 - 每次渲染创建新函数
onClick: () => this.handleClick()

// Vue3 - 缓存函数,避免子组件不必要的更新
onClick: cache[0] || (cache[0] = () => this.handleClick())

48. PatchFlag

定义: 编译时在动态节点上标记的数字,标识节点哪些部分是动态的。

常见 PatchFlag 值:

1 (TEXT)        - 动态文本节点
2 (CLASS)       - 动态 class
4 (STYLE)       - 动态 style
8 (PROPS)       - 动态属性(非 class/style)
16 (FULL_PROPS) - 动态 key 属性
32 (HYDRATE_EVENTS) - 事件监听
64 (STABLE_FRAGMENT) - 稳定片段

原理: diff 时根据 PatchFlag 只对比动态部分,跳过静态部分。


49. v-if 与 v-for 的优先级

Vue2: v-for 优先于 v-if

Vue3: v-if 优先于 v-for

最佳实践: 永远不要在同一元素上同时使用 v-if 和 v-for。

<!-- 不推荐 -->
<li v-for="item in items" v-if="item.isActive">{{ item.name }}</li>

<!-- 推荐:用计算属性过滤 -->
<li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>

<script setup>
const activeItems = computed(() => items.value.filter(i => i.isActive))
</script>

<!-- 推荐:嵌套使用 -->
<template v-for="item in items" :key="item.id">
  <li v-if="item.isActive">{{ item.name }}</li>
</template>

50. v-memo

定义: Vue3.2 引入的性能优化指令,缓存子树,依赖不变时跳过渲染。

原理: 缓存 vnode 树,当依赖值不变时直接复用缓存。

示例:

<!-- 只有 item.id 变化时才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.id]">
  <p>{{ item.name }}</p>
  <p>{{ item.desc }}</p>
</div>

<!-- 依赖空数组 = 永远不更新 -->
<div v-memo="[]">不会更新的内容</div>

使用场景: 长列表渲染、复杂组件的条件渲染


51. Tree-shaking

定义: 打包工具移除未使用代码的优化技术。

Vue3 的支持: 将 API 导出为独立函数,打包工具可以移除未使用的 API。

// Vue2 - 全局 API,无法 Tree-shaking
Vue.nextTick()
Vue.observable()

// Vue3 - 按需导入,支持 Tree-shaking
import { nextTick, reactive } from 'vue'
// 如果不用 ref,就不会打包 ref 的代码

52. 按需加载与组件库优化

实现方式:

// 1. 手动按需导入
import { Button, Input } from 'element-plus'
app.use(Button)
app.use(Input)

// 2. 使用 unplugin-vue-components 自动导入
// vite.config.js
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default {
  plugins: [
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ]
}

53. 虚拟 DOM 与 Diff 算法优化

Vue3 的 Diff 优化:

  1. 双端 Diff:首尾同时对比
  2. 最长递增子序列:最少移动节点
  3. 静态标记跳过:PatchFlag 标识动态部分

流程:

  1. 对比新旧 vnode 树
  2. 标记不同部分
  3. 最少操作更新 DOM

十、Pinia 与 Vue 3 生态

54. Pinia 概述

定义: Pinia 是 Vue3 官方推荐的状态管理库,替代 Vuex。

核心概念:

  • Store:状态仓库
  • State:响应式状态
  • Getters:计算属性
  • Actions:方法(支持同步/异步)

55. Pinia 相比 Vuex 的优势

维度VuexPinia
Mutation需要 mutation 修改 state直接修改 state,无 mutation
TypeScript支持较差完整的类型推导
模块化modules 配置复杂每个 store 独立定义
体积较大更小(~1KB)
DevTools支持完整支持
SSR需要额外配置原生支持
代码分割困难天然支持
学习曲线较陡平缓

56. Pinia 的使用

定义 Store:

// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 方式1:Composition API 风格
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  const increment = () => count.value++

  return { count, doubleCount, increment }
})

// 方式2:Options 风格
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    async fetchData() {
      const res = await fetch('/api/data')
      this.count = await res.json()
    }
  }
})

使用 Store:

<script setup>
import { useCounterStore } from './stores/counter'

const counter = useCounterStore()
counter.increment()
console.log(counter.count)
console.log(counter.doubleCount)
</script>

57. Vue Router 在 Vue3 中的变化

// Vue2
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({ routes })

// Vue3
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
  routes
})

const app = createApp(App)
app.use(router)

十一、组件通信与高级话题

58. Vue3 组件通信方式

方式适用场景示例
props / emits父子组件props.title, emit('click')
v-model父子双向绑定v-model="value"
provide / inject跨层级组件provide('key', val)
ref / expose父访问子childRef.value.method()
attrs / slots透传属性/内容v-bind="$attrs"
Pinia/全局状态全局共享useStore()
事件总线(mitt)任意组件emitter.emit()
自定义 Hooks逻辑复用useMouse()

59. 自定义 Hooks(Composables)

定义: 封装和复用逻辑的函数,使用 Composition API。

示例 - useMouse:

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)

  function update(e) {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  return { x, y }
}

// 使用
<script setup>
import { useMouse } from './composables/useMouse'
const { x, y } = useMouse()
</script>

Hooks 与 Mixins 的区别:

维度MixinsHooks
来源清晰不清楚(多个 mixin 合并)清楚(显式导入调用)
命名冲突可能发生不会(独立变量)
类型推导不支持完整支持
灵活性

60. 响应式丢失问题与 toRefs 解决

问题场景:

function useFeature() {
  const state = reactive({ count: 0, name: 'vue' })
  return state  // 直接返回
}

// 使用时解构会丢失响应性
const { count, name } = useFeature() // count 不是 ref
count++ // 不会触发更新

解决方案:

function useFeature() {
  const state = reactive({ count: 0, name: 'vue' })
  return toRefs(state)  // 转换为 ref
}

// 使用时保持响应性
const { count, name } = useFeature()
count.value++ // 正常触发更新

61. 动态组件

实现: 使用 <component :is="componentName">

<script setup>
import { ref, shallowRef } from 'vue'
import Home from './Home.vue'
import About from './About.vue'

const currentTab = shallowRef(Home)
const tabs = { Home, About }
</script>

<template>
  <button v-for="(_, tab) in tabs" @click="currentTab = tabs[tab]">
    {{ tab }}
  </button>
  <component :is="currentTab" />
</template>

62. keep-alive

定义: 缓存组件实例,避免重复渲染。

<keep-alive :include="['Home', 'About']" :max="10">
  <component :is="currentTab" />
</keep-alive>

属性:

  • include:缓存的组件名
  • exclude:不缓存的组件名
  • max:最大缓存数量

63. props 解构问题

问题: Vue3.3 之前,解构 props 会丢失响应性。

// Vue3.3 之前
const props = defineProps(['title'])
const { title } = props  // title 不是响应式

解决方案:

// 方式1:使用 toRefs
import { toRefs } from 'vue'
const props = defineProps<{ title: string }>()
const { title } = toRefs(props)

// 方式2:Vue3.4+ 响应式解构(实验性)
const { title } = defineProps<{ title: string }>()

// 方式3:Vue3.5+ 使用 defineModel
const model = defineModel()

十二、TypeScript 支持

64. Vue3 对 TypeScript 的改进

Vue2 的问题:

  • Options API 类型推导困难
  • 需要装饰器(@Component)或复杂配置
  • this 类型不明确

Vue3 的改进:

  • 完整的 TypeScript 重写
  • Composition API 天然支持 TS
  • 编译器宏自动类型推导
  • <script setup> 类型自动推断

示例:

<script setup lang="ts">
import { ref } from 'vue'

// 类型自动推导
const count = ref(0)           // Ref<number>
const name = ref<string | null>(null)

// Props 类型
const props = defineProps<{
  title: string
  count?: number
}>()

// Emits 类型
const emit = defineEmits<{
  (e: 'update', value: string): void
}>()

// 泛型组件
const items = ref<string[]>([])
</script>

十三、自定义渲染器

65. createRenderer 与自定义渲染器

定义: 创建自定义渲染器,将 Vue 组件渲染到非 DOM 环境(如 Canvas、小程序、Native)。

示例:

import { createRenderer } from 'vue'

const renderer = createRenderer({
  createElement(type) {
    // 创建元素
    return { type, children: [] }
  },
  setElementText(node, text) {
    // 设置文本
    node.text = text
  },
  insert(child, parent, anchor) {
    // 插入元素
    parent.children.push(child)
  },
  // ... 其他必要方法
})

const app = renderer.createApp(App)

十四、项目迁移与创建

66. 创建 Vue3 项目

# 使用 Vite(推荐)
npm create vite@latest my-app -- --template vue

# 使用 TypeScript
npm create vite@latest my-app -- --template vue-ts

# 进入项目
cd my-app
npm install
npm run dev

67. Vue2 迁移到 Vue3

步骤:

  1. 安装迁移工具
npm install @vue/compat
  1. 配置别名
// vite.config.js
resolve: {
  alias: {
    vue: '@vue/compat'
  }
}
  1. 逐步迁移
  • 替换 beforeDestroybeforeUnmount
  • 替换 destroyedunmounted
  • 替换 Vue.useapp.use
  • 替换全局过滤器为 computed/方法
  • 替换 .sync 修饰符为 v-model:prop
  1. 使用官方迁移构建
configureCompat({
  MODE: 3, // 或 2
  GLOBAL_MOUNT: false,
  GLOBAL_EXTEND: false,
})

十五、响应式数据判断方法总结

68. 响应式判断 API 汇总

API作用返回值
isRef(val)判断是否为 refboolean
isReactive(val)判断是否为 reactiveboolean
isReadonly(val)判断是否为 readonlyboolean
isProxy(val)判断是否为 Proxyboolean
toRaw(val)获取原始对象object
markRaw(val)标记为永不转换object
unref(val)解包 refvalue

十六、总结速查表

Composition API 核心 API

API用途返回值
ref(value)基本类型响应式Ref<T>
reactive(obj)对象响应式Proxy
computed(fn)计算属性Ref<T>
watch(src, cb)侦听数据StopHandle
watchEffect(fn)自动追踪副作用StopHandle
provide(key, val)提供依赖void
inject(key)注入依赖any
onMounted(fn)挂载钩子void
toRefs(obj)转 ref 对象Object
toRef(obj, key)转单个 refRef

生命周期钩子

Composition API时机
setupbeforeCreate/created 之前
onBeforeMountDOM 挂载前
onMountedDOM 挂载后
onBeforeUpdateDOM 更新前
onUpdatedDOM 更新后
onBeforeUnmount组件销毁前
onUnmounted组件销毁后