云计算百科
云计算领域专业知识百科平台

Vue 3 知识点总结

Vue 3 知识点总结

最后更新:2026-07-29


目录

  • Vue 3 核心变化概览
  • 组合式 API(Composition API)
  • 响应式系统
  • 生命周期钩子
  • 计算属性与侦听器
  • 组件系统
  • 模板语法与指令
  • 内置组件
  • 路由(Vue Router 4)
  • 状态管理(Pinia)
  • TypeScript 集成
  • 性能优化
  • 编译优化
  • Teleport / Suspense / 异步组件
  • 自定义指令
  • 插件系统
  • 渲染函数与 JSX
  • 服务端渲染(SSR)
  • 测试
  • 常见设计模式与最佳实践

  • 1. Vue 3 核心变化概览

    1.1 与 Vue 2 的主要区别

    特性Vue 2Vue 3
    API 风格 Options API Options API + Composition API
    响应式原理 Object.defineProperty Proxy
    多根节点 不支持(需唯一根节点) 支持 Fragment
    全局 API Vue.xxx 挂载 createApp() 应用实例
    状态管理 Vuex Pinia(官方推荐)
    路由 Vue Router 3 Vue Router 4
    TypeScript 支持较弱 原生 TS 编写,一等支持
    包体积 ~23KB ~10KB(tree-shaking)
    渲染性能 基线 约 1.3~2 倍提升
    v-model 仅一个 多个 v-model
    过渡类名 v-enter v-enter-from
    过滤器 支持 已移除
    $on/$off/$once 支持 已移除(用 mitt 等替代)
    函数式组件 支持 仅保留函数式写法,性能优势消失

    1.2 创建应用

    // Vue 3
    import { createApp } from 'vue'
    import App from './App.vue'

    const app = createApp(App)
    app.use(router)
    app.use(pinia)
    app.mount('#app')

    1.3 全局 API 变更

    // Vue 2: Vue.component / Vue.directive / Vue.mixin
    // Vue 3: 挂在 app 实例上
    app.component('MyComp', MyComp)
    app.directive('focus', { mounted(el) { el.focus() } })
    app.mixin({ /* … */ })
    app.config.globalProperties.$http = axios
    app.config.errorHandler = (err) => { /* … */ }


    2. 组合式 API(Composition API)

    2.1 setup() 函数

    <script setup>
    // <script setup> 是语法糖,无需 return,顶层变量/函数自动暴露给模板
    import { ref, computed, onMounted } from 'vue'

    const count = ref(0)
    const double = computed(() => count.value * 2)

    function increment() {
    count.value++
    }

    onMounted(() => {
    console.log('mounted')
    })
    </script>

    普通 setup() 写法:

    export default {
    setup(props, { emit, expose, slots, attrs }) {
    const count = ref(0)
    // 必须 return 暴露给模板
    return { count }
    }
    }

    2.2 组合式函数(Composables)

    将可复用逻辑抽取为函数,以 use 开头命名:

    // useCounter.js
    import { ref } from 'vue'

    export function useCounter(initial = 0) {
    const count = ref(initial)
    const increment = () => count.value++
    const decrement = () => count.value
    const reset = () => (count.value = initial)
    return { count, increment, decrement, reset }
    }

    <script setup>
    import { useCounter } from './useCounter'
    const { count, increment } = useCounter(10)
    </script>

    常用组合式函数示例:

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

    // useFetch.js – 数据请求
    import { ref, watchEffect, toValue } from 'vue'

    export function useFetch(url) {
    const data = ref(null)
    const error = ref(null)
    const isPending = ref(true)

    watchEffect(async () => {
    isPending.value = true
    data.value = null
    error.value = null
    try {
    const res = await fetch(toValue(url))
    if (!res.ok) throw new Error(res.statusText)
    data.value = await res.json()
    } catch (e) {
    error.value = e
    } finally {
    isPending.value = false
    }
    })

    return { data, error, isPending }
    }

    2.3 provide / inject(依赖注入)

    // 父组件
    import { provide, ref, readonly } from 'vue'

    const count = ref(0)
    provide('count', readonly(count)) // 只读注入
    provide('increment', () => count.value++) // 注入方法

    // 也可用 Symbol / InjectionKey 做类型安全
    export const CountKey = Symbol('count')
    provide(CountKey, count)

    // 子组件(任意深度)
    import { inject } from 'vue'
    const count = inject('count', 0) // 第二个参数为默认值


    3. 响应式系统

    3.1 核心 API

    ref()

    import { ref } from 'vue'

    const count = ref(0) // 基本类型
    count.value++ // 通过 .value 访问/修改

    const obj = ref({ a: 1 }) // 对象也可以,内部用 reactive 包装
    obj.value.a = 2

    // 模板中自动解包,无需 .value
    // <p>{{ count }}</p>

    reactive()

    import { reactive } from 'vue'

    const state = reactive({
    count: 0,
    nested: { deep: true }
    })

    state.count++ // 直接修改,无需 .value
    state.nested.deep // 深层也是响应式的

    // ⚠️ 不能解构,否则丢失响应性
    // const { count } = state // ❌ count 不再是响应式

    ref vs reactive 选择
    场景推荐
    基本类型(number/string/boolean) ref
    需要整体替换的对象 ref
    表单等固定结构的对象 reactive
    通用 / 不确定 ref(更安全)

    3.2 解构保持响应性

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

    const state = reactive({ name: 'Vue', version: 3 })

    // toRefs:整体解构
    const { name, version } = toRefs(state) // 每个都是 ref

    // toRef:单个属性
    const nameRef = toRef(state, 'name')

    3.3 只读与浅层

    import { readonly, shallowRef, shallowReactive, shallowReadonly, triggerRef } from 'vue'

    const original = reactive({ count: 0 })
    const copy = readonly(original)
    // copy.count++ // ❌ 警告,不可修改

    const shallow = shallowRef({ nested: { count: 0 } })
    // shallow.value.nested.count++ // ❌ 不触发更新
    shallow.value = { nested: { count: 1 } } // ✅ 替换整个 .value 才触发

    // 手动触发 shallowRef 更新
    triggerRef(shallow)

    3.4 响应式原理(Proxy)

    reactive(obj)
    → new Proxy(obj, {
    get(target, key, receiver) {
    track(target, key) // 收集依赖
    const res = Reflect.get(target, key, receiver)
    return isObject(res) ? reactive(res) : res // 惰性深层代理
    },
    set(target, key, value, receiver) {
    const oldVal = target[key]
    const res = Reflect.set(target, key, value, receiver)
    if (oldVal !== value) trigger(target, key) // 触发更新
    return res
    },
    deleteProperty(target, key) { … }
    })

    优势:

    • 可检测属性新增/删除
    • 可检测数组索引/length 变化
    • 惰性深层代理(访问时才代理子对象),性能更好

    3.5 响应式工具函数

    import { isRef, isReactive, isReadonly, isProxy, toRaw, markRaw, customRef } from 'vue'

    isRef(ref(0)) // true
    isReactive(reactive({})) // true
    isProxy(reactive({})) // true

    // toRaw:获取原始对象(跳过代理)
    const raw = toRaw(state)

    // markRaw:标记对象永不转为响应式(适合第三方库实例)
    const chart = markRaw(new ECharts())

    // customRef:自定义 ref(如防抖)
    function useDebouncedRef(value, delay = 200) {
    let timeout
    return customRef((track, trigger) => ({
    get() {
    track()
    return value
    },
    set(newValue) {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
    value = newValue
    trigger()
    }, delay)
    }
    }))
    }


    4. 生命周期钩子

    4.1 选项式 vs 组合式对照

    Options APIComposition API说明
    beforeCreate setup() 本身 setup 在此阶段执行
    created setup() 本身
    beforeMount onBeforeMount
    mounted onMounted DOM 已挂载
    beforeUpdate onBeforeUpdate
    updated onUpdated
    beforeUnmount onBeforeUnmount
    unmounted onUnmounted 清理副作用
    errorCaptured onErrorCaptured
    renderTracked onRenderTracked 调试:依赖追踪
    renderTriggered onRenderTriggered 调试:触发更新
    activated onActivated KeepAlive 激活
    deactivated onDeactivated KeepAlive 停用
    serverPrefetch onServerPrefetch SSR 专用

    4.2 使用示例

    <script setup>
    import { onMounted, onUnmounted, onUpdated } from 'vue'

    onMounted(() => {
    console.log('DOM ready')
    window.addEventListener('resize', handler)
    })

    onUnmounted(() => {
    window.removeEventListener('resize', handler)
    })

    // 可多次调用,按注册顺序执行
    onMounted(() => { /* 第二个回调 */ })
    </script>

    4.3 生命周期图示

    setup()

    ├─ onBeforeMount
    │ │
    │ 挂载 DOM
    │ │
    ├─ onMounted ◄── 可访问 DOM / 发起请求
    │ │
    │ 数据变化 → 重新渲染
    │ │
    ├─ onBeforeUpdate
    ├─ onUpdated
    │ │
    │ 卸载组件
    │ │
    ├─ onBeforeUnmount ◄── 清理定时器/事件
    └─ onUnmounted


    5. 计算属性与侦听器

    5.1 computed

    import { ref, computed } from 'vue'

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

    // 只读计算属性
    const fullName = computed(() => `${firstName.value} ${lastName.value}`)

    // 可写计算属性
    const fullNameWritable = computed({
    get: () => `${firstName.value} ${lastName.value}`,
    set: (val) => {
    const [first, last] = val.split(' ')
    firstName.value = first
    lastName.value = last
    }
    })

    特点: 基于依赖缓存,依赖不变则不重新计算。

    5.2 watch

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

    const count = ref(0)
    const state = reactive({ name: '', age: 0 })

    // 1. 侦听 ref
    watch(count, (newVal, oldVal) => {
    console.log(`${oldVal}${newVal}`)
    })

    // 2. 侦听 reactive 对象的某个属性(必须用 getter)
    watch(
    () => state.name,
    (newName) => console.log(newName)
    )

    // 3. 侦听多个源
    watch([count, () => state.name], ([newCount, newName]) => {
    // …
    })

    // 4. 深度侦听
    watch(state, (newState) => {
    // 任何嵌套属性变化都触发
    }, { deep: true })

    // 5. 立即执行
    watch(count, (val) => {
    // 立即执行一次
    }, { immediate: true })

    // 6. 回调刷新时机
    watch(count, cb, { flush: 'post' }) // DOM 更新后执行
    watch(count, cb, { flush: 'sync' }) // 同步执行(谨慎使用)

    // 7. 一次性侦听(Vue 3.4+)
    watch(count, cb, { once: true })

    // 8. 停止侦听
    const stop = watch(count, cb)
    stop() // 手动停止

    5.3 watchEffect

    import { watchEffect } from 'vue'

    // 自动追踪回调中使用的所有响应式依赖
    const stop = watchEffect((onCleanup) => {
    console.log(count.value) // 自动依赖 count

    // 清理函数:下次执行前或组件卸载时调用
    const timer = setInterval(() => {}, 1000)
    onCleanup(() => clearInterval(timer))
    })

    // 停止
    stop()

    5.4 watch vs watchEffect

    特性watchwatchEffect
    指定侦听源 ✅ 显式指定 ❌ 自动追踪
    访问旧值 ✅ (new, old)
    惰性执行 ✅ 默认惰性 ❌ 立即执行
    深度选项 ✅ { deep: true } 自动深度追踪

    6. 组件系统

    6.1 组件定义

    <!– 单文件组件 SFC –>
    <script setup lang="ts">
    // 组件逻辑
    </script>

    <template>
    <!– 模板 –>
    </template>

    <style scoped>
    /* 局部样式 */
    </style>

    6.2 Props

    <script setup lang="ts">
    // 方式一:defineProps 宏(编译时,无需 import)
    const props = defineProps<{
    title: string
    count?: number
    }>()

    // 带默认值
    const props = withDefaults(defineProps<{
    title: string
    count?: number
    }>(), {
    count: 0
    })

    // 运行时声明
    const props = defineProps({
    title: { type: String, required: true },
    count: { type: Number, default: 0 },
    list: { type: Array as PropType<string[]>, default: () => [] }
    })
    </script>

    Props 注意事项:

    • Props 是只读的,不要直接修改
    • 对象/数组类型的 default 必须用工厂函数
    • 未声明的 attribute 会落入 $attrs

    6.3 Emits

    <script setup lang="ts">
    const emit = defineEmits<{
    change: [value: string]
    update: [id: number, data: object]
    }>()

    // 运行时声明
    const emit = defineEmits(['change', 'update'])

    // 触发
    emit('change', 'hello')
    emit('update', 1, { name: 'test' })
    </script>

    6.4 v-model(组件双向绑定)

    <!– 父组件:多个 v-model –>
    <UserForm
    v-model:firstName="first"
    v-model:lastName="last"
    />

    <!– UserForm.vue –>
    <script setup>
    const firstName = defineModel('firstName') // Vue 3.4+ 语法糖
    const lastName = defineModel('lastName')

    // 等价于:
    // const props = defineProps(['firstName', 'lastName'])
    // const emit = defineEmits(['update:firstName', 'update:lastName'])
    </script>

    修饰符:

    <UserForm v-model:firstName.trim="first" />

    <!– 子组件中 –>
    <script setup>
    const [model, modifiers] = defineModel('firstName')
    if (modifiers.trim) { /* … */ }
    </script>

    6.5 Slots 插槽

    <!– 父组件 –>
    <Card>
    <template #header>标题</template>
    <template #default="{ item }">
    {{ item.name }}
    </template>
    <template #footer>底部</template>
    </Card>

    <!– Card.vue –>
    <template>
    <div class="card">
    <header><slot name="header" /></header>
    <main><slot :item="currentItem" /></main>
    <footer><slot name="footer">默认底部</slot></footer>
    </div>
    </template>

    // 在 setup 中访问插槽
    import { useSlots } from 'vue'
    const slots = useSlots()
    // slots.default?.()
    // slots.header?.()

    6.6 expose / 组件实例

    <!– 子组件 –>
    <script setup>
    import { ref } from 'vue'
    const count = ref(0)
    const reset = () => (count.value = 0)

    // 只暴露 reset,不暴露 count
    defineExpose({ reset })
    </script>

    <!– 父组件 –>
    <script setup>
    import { ref, onMounted } from 'vue'
    import Child from './Child.vue'

    const childRef = ref(null)
    onMounted(() => {
    childRef.value.reset() // ✅
    // childRef.value.count // ❌ 未暴露
    })
    </script>

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

    6.7 attrs 透传

    <!– 自动继承:未声明的 props/emits 自动绑定到根元素 –>
    <!– 禁用继承 –>
    <script setup>
    defineOptions({ inheritAttrs: false })
    </script>

    <template>
    <input v-bind="$attrs" />
    </template>

    6.8 动态组件

    <script setup>
    import { shallowRef } from 'vue'
    import TabA from './TabA.vue'
    import TabB from './TabB.vue'

    const current = shallowRef(TabA)
    </script>

    <template>
    <component :is="current" />
    </template>


    7. 模板语法与指令

    7.1 插值与绑定

    <!– 文本插值 –>
    <p>{{ message }}</p>

    <!– 原始 HTML –>
    <div v-html="rawHtml"></div>

    <!– 属性绑定 –>
    <img :src="imageUrl" :alt="title" />
    <div :class="{ active: isActive, 'text-danger': hasError }"></div>
    <div :class="[activeClass, errorClass]"></div>
    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

    <!– 动态参数 –>
    <a :[attrName]="url">Link</a>
    <button @[eventName]="handler">Click</button>

    7.2 条件渲染

    <div v-if="type === 'A'">A</div>
    <div v-else-if="type === 'B'">B</div>
    <div v-else>C</div>

    <!– v-show:切换 display,频繁切换时性能更好 –>
    <div v-show="isVisible">Toggle me</div>

    <!– template 上使用 v-if(不渲染额外元素) –>
    <template v-if="showGroup">
    <h1>Title</h1>
    <p>Content</p>
    </template>

    7.3 列表渲染

    <li v-for="(item, index) in items" :key="item.id">
    {{ index }} – {{ item.name }}
    </li>

    <!– 对象遍历 –>
    <div v-for="(value, key, index) in obj" :key="key">
    {{ key }}: {{ value }}
    </div>

    <!– 范围 –>
    <span v-for="n in 10" :key="n">{{ n }}</span>

    ⚠️ key 的重要性: 必须用唯一标识(如 id),不要用 index。

    7.4 事件处理

    <button @click="handler">Click</button>
    <button @click="handler($event, 'arg')">With args</button>

    <!– 修饰符 –>
    <form @submit.prevent="onSubmit"> <!– 阻止默认 –>
    <a @click.stop="doThis"> <!– 阻止冒泡 –>
    <div @click.self="onlySelf"> <!– 仅自身触发 –>
    <input @keyup.enter="submit"> <!– 按键修饰符 –>
    <button @click.once="doOnce"> <!– 仅触发一次 –>
    <div @scroll.passive="onScroll"> <!– passive 监听 –>

    7.5 表单绑定

    <input v-model="text" />
    <textarea v-model="desc"></textarea>
    <input type="checkbox" v-model="checked" />
    <input type="checkbox" v-model="picked" value="a" />
    <input type="radio" v-model="picked" value="b" />
    <select v-model="selected">
    <option value="1">One</option>
    </select>

    <!– 修饰符 –>
    <input v-model.trim="msg" /> <!– 自动 trim –>
    <input v-model.number="age" /> <!– 转 number –>
    <input v-model.lazy="msg" /> <!– change 而非 input 事件 –>


    8. 内置组件

    8.1 Transition

    <template>
    <Transition name="fade" mode="out-in">
    <p v-if="show">Hello</p>
    </Transition>
    </template>

    <style>
    .fade-enter-active,
    .fade-leave-active {
    transition: opacity 0.3s ease;
    }
    .fade-enter-from,
    .fade-leave-to {
    opacity: 0;
    }
    </style>

    过渡类名(Vue 3):

    • v-enter-from → v-enter-active → v-enter-to
    • v-leave-from → v-leave-active → v-leave-to

    8.2 TransitionGroup

    <TransitionGroup name="list" tag="ul">
    <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </TransitionGroup>

    <style>
    .list-enter-active, .list-leave-active {
    transition: all 0.5s ease;
    }
    .list-enter-from, .list-leave-to {
    opacity: 0;
    transform: translateX(30px);
    }
    .list-move {
    transition: transform 0.5s ease;
    }
    </style>

    8.3 KeepAlive

    <KeepAlive :include="['TabA', 'TabB']" :max="10">
    <component :is="currentTab" />
    </KeepAlive>

    <!– 配合生命周期 –>
    <script setup>
    import { onActivated, onDeactivated } from 'vue'

    onActivated(() => { /* 从缓存恢复 */ })
    onDeactivated(() => { /* 进入缓存 */ })
    </script>


    9. 路由(Vue Router 4)

    9.1 基本配置

    // router/index.js
    import { createRouter, createWebHistory } from 'vue-router'

    const routes = [
    {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue') // 懒加载
    },
    {
    path: '/user/:id',
    name: 'User',
    component: () => import('../views/User.vue'),
    props: true, // 将 params 作为 props 传入
    children: [
    { path: 'profile', component: () => import('../views/Profile.vue') }
    ]
    },
    {
    path: '/:pathMatch(.*)*', // 404 兜底
    name: 'NotFound',
    component: () => import('../views/404.vue')
    }
    ]

    const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
    scrollBehavior(to, from, savedPosition) {
    return savedPosition || { top: 0 }
    }
    })

    export default router

    9.2 组合式 API 中使用

    <script setup>
    import { useRouter, useRoute } from 'vue-router'

    const router = useRouter()
    const route = useRoute()

    // 编程式导航
    router.push('/user/1')
    router.push({ name: 'User', params: { id: 1 } })
    router.replace('/login')
    router.back()
    router.go(-2)

    // 当前路由信息
    console.log(route.params.id)
    console.log(route.query.search)
    console.log(route.hash)
    </script>

    9.3 导航守卫

    // 全局前置守卫
    router.beforeEach((to, from) => {
    const auth = useAuthStore()
    if (to.meta.requiresAuth && !auth.isLoggedIn) {
    return { name: 'Login', query: { redirect: to.fullPath } }
    }
    })

    // 全局后置钩子
    router.afterEach((to, from) => {
    document.title = to.meta.title || 'App'
    })

    // 路由独享守卫
    {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from) => {
    if (!isAdmin()) return false
    }
    }

    <!– 组件内守卫 –>
    <script setup>
    import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

    onBeforeRouteLeave((to, from) => {
    if (hasUnsavedChanges.value) {
    return window.confirm('确定离开?未保存的更改将丢失。')
    }
    })

    onBeforeRouteUpdate((to, from) => {
    // 同组件复用,参数变化时(如 /user/1 → /user/2)
    fetchData(to.params.id)
    })
    </script>

    9.4 路由元信息

    {
    path: '/dashboard',
    component: Dashboard,
    meta: {
    requiresAuth: true,
    title: '控制台',
    roles: ['admin', 'editor']
    }
    }


    10. 状态管理(Pinia)

    10.1 基本使用

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

    // 组合式写法(推荐)
    export const useCounterStore = defineStore('counter', () => {
    // state
    const count = ref(0)
    const name = ref('Vue')

    // getters
    const doubleCount = computed(() => count.value * 2)

    // actions
    function increment() {
    count.value++
    }

    async function fetchCount() {
    const res = await api.getCount()
    count.value = res.data
    }

    return { count, name, doubleCount, increment, fetchCount }
    })

    // 选项式写法
    export const useCounterStore = defineStore('counter', {
    state: () => ({ count: 0, name: 'Vue' }),
    getters: {
    doubleCount: (state) => state.count * 2,
    doublePlusOne() { return this.doubleCount + 1 }
    },
    actions: {
    increment() { this.count++ },
    async fetchCount() {
    this.count = await api.getCount()
    }
    }
    })

    10.2 在组件中使用

    <script setup>
    import { useCounterStore } from '@/stores/counter'
    import { storeToRefs } from 'pinia'

    const store = useCounterStore()

    // ⚠️ 解构会丢失响应性,用 storeToRefs
    const { count, doubleCount } = storeToRefs(store)
    // actions 可以直接解构
    const { increment } = store

    // 修改 state
    store.count++
    store.$patch({ count: 10, name: 'Pinia' })
    store.$patch((state) => {
    state.count++
    state.name = 'batch'
    })
    store.$reset() // 重置(仅选项式写法支持)
    </script>

    10.3 Store 组合

    // stores/user.js
    export const useUserStore = defineStore('user', () => {
    const token = ref('')
    const isLoggedIn = computed(() => !!token.value)
    return { token, isLoggedIn }
    })

    // stores/cart.js
    export const useCartStore = defineStore('cart', () => {
    const user = useUserStore() // 组合其他 store
    const items = ref([])

    const canCheckout = computed(() => user.isLoggedIn && items.value.length > 0)

    return { items, canCheckout }
    })

    10.4 插件与持久化

    // pinia 插件
    function piniaLogger({ store }) {
    store.$subscribe((mutation, state) => {
    console.log(`[${store.$id}] ${mutation.type}`, state)
    })
    }

    const pinia = createPinia()
    pinia.use(piniaLogger)

    // 持久化(pinia-plugin-persistedstate)
    import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
    pinia.use(piniaPluginPersistedstate)

    // store 中启用
    export const useUserStore = defineStore('user', () => {
    // …
    }, {
    persist: true // 或 { key: 'user', storage: localStorage }
    })


    11. TypeScript 集成

    11.1 项目配置

    npm create vue@latest # 选择 TypeScript

    // tsconfig.json 关键配置
    {
    "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "paths": { "@/*": ["./src/*"] }
    }
    }

    11.2 组件中的 TS

    <script setup lang="ts">
    import { ref, computed } from 'vue'
    import type { Ref } from 'vue'

    // 自动推断
    const count = ref(0) // Ref<number>

    // 显式泛型
    const list = ref<string[]>([])
    const user = ref<{ name: string; age: number } | null>(null)

    // Props 类型
    interface Props {
    title: string
    items?: string[]
    }
    const props = withDefaults(defineProps<Props>(), {
    items: () => []
    })

    // Emits 类型
    const emit = defineEmits<{
    change: [value: string]
    delete: [id: number]
    }>()

    // Ref 类型
    const inputRef = ref<HTMLInputElement | null>(null)

    // 组件 ref 类型
    import Child from './Child.vue'
    const childRef = ref<InstanceType<typeof Child> | null>(null)
    </script>

    11.3 注入类型安全

    import type { InjectionKey, Ref } from 'vue'

    export const CountKey: InjectionKey<Ref<number>> = Symbol('count')

    // provide
    provide(CountKey, ref(0))

    // inject – 自动推断为 Ref<number>
    const count = inject(CountKey)!

    11.4 模板 ref 类型

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

    // DOM 元素
    const el = ref<HTMLDivElement | null>(null)

    // 组件实例
    import MyComp from './MyComp.vue'
    const comp = ref<InstanceType<typeof MyComp> | null>(null)

    onMounted(() => {
    el.value?.focus()
    comp.value?.reset()
    })
    </script>

    <template>
    <div ref="el" tabindex="0">Focusable</div>
    <MyComp ref="comp" />
    </template>


    12. 性能优化

    12.1 编译层优化

    • Tree-shaking:未使用的 API 不会打包
    • 静态提升(hoistStatic):静态节点提升到渲染函数外
    • Patch Flags:编译时标记动态绑定类型,运行时精准 diff
    • Block Tree:将模板分块,只追踪动态节点
    • 缓存事件处理函数:cacheHandlers

    12.2 运行时优化

    <script setup>
    import { shallowRef, vModelText } from 'vue'

    // 1. 大列表用 shallowRef / shallowReactive
    const bigList = shallowRef(hugeArray)

    // 2. v-once:只渲染一次
    // <div v-once>{{ staticContent }}</div>

    // 3. v-memo:条件记忆化
    // <div v-memo="[item.id === selected]">…</div>

    // 4. 虚拟列表(大量数据)
    // 使用 vue-virtual-scroller / @tanstack/vue-virtual
    </script>

    12.3 组件级优化

    // 1. 异步组件 / 代码分割
    import { defineAsyncComponent } from 'vue'
    const HeavyChart = defineAsyncComponent(() =>
    import('./HeavyChart.vue')
    )

    // 2. KeepAlive 缓存
    // <KeepAlive :max="5">
    // <component :is="currentView" />
    // </KeepAlive>

    // 3. 避免不必要的响应式
    import { markRaw } from 'vue'
    const chartInstance = markRaw(new ECharts()) // 不需要响应式

    // 4. 函数式组件(纯展示)
    const FuncComp = (props) => h('div', props.msg)

    12.4 列表优化

    <!– ✅ 正确:唯一 key –>
    <li v-for="item in list" :key="item.id">{{ item.name }}</li>

    <!– ❌ 错误:用 index –>
    <li v-for="(item, i) in list" :key="i">{{ item.name }}</li>

    <!– 大列表:分页 / 虚拟滚动 –>
    <VirtualList :items="items" :item-height="50" v-slot="{ item }">
    <div>{{ item.name }}</div>
    </VirtualList>

    12.5 其他技巧

    // 防抖/节流搜索
    import { useDebouncedRef } from './useDebouncedRef'
    const search = useDebouncedRef('', 300)
    watch(search, (val) => fetchResults(val))

    // 图片懒加载
    // <img v-lazy="imageUrl" /> (自定义指令或 vue-lazyload)

    // 路由懒加载
    component: () => import('./views/About.vue')

    // 第三方库按需引入
    // import { debounce } from 'lodash-es' // ✅
    // import _ from 'lodash' // ❌


    13. 编译优化

    13.1 Patch Flags 详解

    // 编译前
    // <div><span>static</span><span>{{ dynamic }}</span></div>

    // 编译后(简化)
    import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

    const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "static", 1 /* HOISTED */)

    export function render(_ctx, _cache) {
    return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("span", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
    ]))
    }

    常见 PatchFlag:

    Flag值含义
    TEXT 1 动态文本
    CLASS 2 动态 class
    STYLE 4 动态 style
    PROPS 8 动态非 class/style props
    FULL_PROPS 16 动态 key 不确定的 props
    NEED_HYDRATION 32 SSR 需要水合
    STABLE_FRAGMENT 64 子节点顺序不变
    KEYED_FRAGMENT 128 有 key 的 fragment
    UNKEYED_FRAGMENT 256 无 key 的 fragment
    HOISTED -1 静态提升
    BAIL -2 退出优化模式

    13.2 Block Tree

    <template>
    <div> <!– Block root –>
    <p>static</p> <!– 不追踪 –>
    <p>{{ a }}</p> <!– 追踪 –>
    <p v-if="ok">{{ b }}</p> <!– 条件块 –>
    <p>{{ c }}</p> <!– 追踪 –>
    </div>
    </template>

    运行时 diff 只比较 Block 收集的动态节点数组,跳过所有静态内容。


    14. Teleport / Suspense / 异步组件

    14.1 Teleport

    将内容渲染到 DOM 树的其他位置:

    <template>
    <button @click="showModal = true">打开弹窗</button>

    <Teleport to="body">
    <div v-if="showModal" class="modal-overlay">
    <div class="modal">
    <p>弹窗内容</p>
    <button @click="showModal = false">关闭</button>
    </div>
    </div>
    </Teleport>
    </template>

    <!– 禁用 teleport –>
    <Teleport to="body" :disabled="isMobile">
    <div class="tooltip">…</div>
    </Teleport>

    14.2 Suspense(实验性)

    <template>
    <Suspense>
    <!– 默认插槽:异步内容 –>
    <template #default>
    <AsyncDashboard /> <!– setup() 中有 await 的组件 –>
    </template>

    <!– 加载状态 –>
    <template #fallback>
    <LoadingSpinner />
    </template>
    </Suspense>
    </template>

    <!– AsyncDashboard.vue –>
    <script setup>
    // 顶层 await → 组件变为异步依赖
    const data = await fetchDashboardData()
    </script>

    Suspense 事件:

    <Suspense
    @resolve="onResolve"
    @pending="onPending"
    @fallback="onFallback"
    >

    14.3 异步组件

    import { defineAsyncComponent } from 'vue'

    // 简单用法
    const AsyncComp = defineAsyncComponent(() => import('./Comp.vue'))

    // 完整配置
    const AsyncCompWithOptions = defineAsyncComponent({
    loader: () => import('./Comp.vue'),
    loadingComponent: LoadingSpinner,
    errorComponent: ErrorDisplay,
    delay: 200, // 显示 loading 前的延迟
    timeout: 10000, // 超时
    suspensible: false, // 是否被 Suspense 接管
    onError(error, retry, fail, attempts) {
    if (attempts <= 3) retry()
    else fail()
    }
    })


    15. 自定义指令

    15.1 定义

    // Vue 3 钩子名称与生命周期对齐
    const myDirective = {
    created(el, binding, vnode) {}, // 元素创建后
    beforeMount(el, binding, vnode) {}, // 挂载前
    mounted(el, binding, vnode) {}, // 挂载后
    beforeUpdate(el, binding, vnode, prevVnode) {},
    updated(el, binding, vnode, prevVnode) {},
    beforeUnmount(el, binding, vnode) {},
    unmounted(el, binding, vnode) {}
    }

    15.2 使用

    <script setup>
    // 局部注册(script setup 中以 v 开头的变量自动注册)
    const vFocus = {
    mounted: (el) => el.focus()
    }

    const vHighlight = {
    mounted(el, binding) {
    el.style.backgroundColor = binding.value || 'yellow'
    },
    updated(el, binding) {
    el.style.backgroundColor = binding.value || 'yellow'
    }
    }
    </script>

    <template>
    <input v-focus />
    <p v-highlight="'lightblue'">高亮文本</p>
    <p v-highlight>默认黄色</p>
    </template>

    // 全局注册
    app.directive('focus', {
    mounted(el) { el.focus() }
    })

    // 简写(mounted + updated 相同逻辑)
    app.directive('color', (el, binding) => {
    el.style.color = binding.value
    })

    15.3 binding 对象

    {
    value: '绑定值', // v-xxx:value
    oldValue: '旧值', // 仅 updated
    arg: '参数', // v-xxx:arg
    modifiers: { mod: true }, // v-xxx.mod
    instance: null, // 组件实例
    dir: { /* 指令定义 */ }
    }


    16. 插件系统

    16.1 编写插件

    // myPlugin.js
    export default {
    install(app, options) {
    // 全局组件
    app.component('MyIcon', MyIcon)

    // 全局指令
    app.directive('tooltip', tooltipDirective)

    // 全局属性
    app.config.globalProperties.$format = (val) => { /* … */ }

    // provide 注入
    app.provide('pluginOptions', options)

    // 全局 mixin(谨慎使用)
    app.mixin({
    created() { /* … */ }
    })
    }
    }

    16.2 使用插件

    import myPlugin from './myPlugin'

    const app = createApp(App)
    app.use(myPlugin, { theme: 'dark' })
    app.mount('#app')


    17. 渲染函数与 JSX

    17.1 h() 函数

    import { h, ref } from 'vue'

    // 基本用法
    const vnode = h('div', { class: 'container' }, [
    h('h1', 'Title'),
    h('p', { style: 'color: red' }, 'Content')
    ])

    // 组件
    import MyComp from './MyComp.vue'
    const compVnode = h(MyComp, {
    title: 'Hello',
    onChange: (val) => console.log(val)
    }, {
    default: () => 'Slot content',
    header: () => h('h2', 'Header')
    })

    // 条件渲染
    const render = () => {
    return show.value
    ? h('div', 'Visible')
    : h('span', 'Hidden')
    }

    // 列表渲染
    const render = () => {
    return h('ul', items.value.map(item =>
    h('li', { key: item.id }, item.name)
    ))
    }

    17.2 JSX(需 @vitejs/plugin-vue-jsx)

    import { defineComponent, ref } from 'vue'

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

    return () => (
    <div class="container">
    <h1>Count: {count.value}</h1>
    <button onClick={() => count.value++}>+1</button>
    {count.value > 5 && <p>Big number!</p>}
    <ul>
    {items.value.map(item => (
    <li key={item.id}>{item.name}</li>
    ))}
    </ul>
    </div>
    )
    }
    })

    17.3 渲染函数 vs 模板

    场景推荐
    常规 UI 模板(可读性 + 编译优化)
    高度动态/递归组件 渲染函数
    组件库/抽象组件 渲染函数 / JSX
    需要完整 JS 表达力 JSX

    18. 服务端渲染(SSR)

    18.1 基本流程

    客户端请求


    服务器执行 Vue 组件 → 生成 HTML 字符串


    返回完整 HTML(首屏可见)


    客户端加载 JS → 水合(Hydration)→ 交互可用

    18.2 核心 API

    // 服务器端
    import { renderToString } from 'vue/server-renderer'
    import { createSSRApp } from 'vue'

    const app = createSSRApp(App)
    const html = await renderToString(app)

    // 客户端
    import { createSSRApp } from 'vue'
    const app = createSSRApp(App)
    app.mount('#app') // 水合而非重新渲染

    18.3 数据预取

    <script setup>
    import { useAsyncData } from '#app' // Nuxt 3

    const { data, pending, error } = await useAsyncData('posts', () =>
    $fetch('/api/posts')
    )
    </script>

    18.4 流式渲染(Vue 3.5+)

    import { renderToStream } from 'vue/server-renderer'

    const stream = renderToStream(app)
    stream.pipe(res) // Node.js response

    18.5 Nuxt 3 框架

    npx nuxi@latest init my-app

    nuxt.config.ts # 配置
    pages/ # 文件系统路由
    components/ # 自动导入组件
    composables/ # 自动导入组合式函数
    server/api/ # API 路由
    middleware/ # 路由中间件
    plugins/ # 插件


    19. 测试

    19.1 单元测试(Vitest)

    // vitest.config.ts
    import { defineConfig } from 'vitest/config'
    import vue from '@vitejs/plugin-vue'

    export default defineConfig({
    plugins: [vue()],
    test: {
    environment: 'jsdom', // 或 happy-dom
    globals: true
    }
    })

    // Counter.test.ts
    import { describe, it, expect } from 'vitest'
    import { ref, computed, nextTick } from 'vue'
    import { useCounter } from './useCounter'

    describe('useCounter', () => {
    it('increments', () => {
    const { count, increment } = useCounter(0)
    increment()
    expect(count.value).toBe(1)
    })

    it('computes double', async () => {
    const { count, double } = useCounter(5)
    expect(double.value).toBe(10)
    count.value = 10
    await nextTick()
    expect(double.value).toBe(20)
    })
    })

    19.2 组件测试(Vue Test Utils)

    // npm i -D @vue/test-utils
    import { mount } from '@vue/test-utils'
    import { describe, it, expect } from 'vitest'
    import Counter from './Counter.vue'

    describe('Counter', () => {
    it('renders count', () => {
    const wrapper = mount(Counter, {
    props: { initial: 5 }
    })
    expect(wrapper.text()).toContain('5')
    })

    it('increments on click', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.find('.count').text()).toBe('1')
    })

    it('emits change event', async () => {
    const wrapper = mount(Counter)
    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('change')).toHaveLength(1)
    expect(wrapper.emitted('change')[0]).toEqual([1])
    })

    it('renders slot', () => {
    const wrapper = mount(Counter, {
    slots: { default: '<span class="custom">Slot</span>' }
    })
    expect(wrapper.find('.custom').exists()).toBe(true)
    })
    })

    19.3 E2E 测试(Playwright / Cypress)

    // playwright
    import { test, expect } from '@playwright/test'

    test('counter interaction', async ({ page }) => {
    await page.goto('/')
    await page.click('button.increment')
    await expect(page.locator('.count')).toHaveText('1')
    })


    20. 常见设计模式与最佳实践

    20.1 组件设计原则

    ✅ 单一职责:一个组件只做一件事
    ✅ Props 向下,Events 向上(单向数据流)
    ✅ 组合优于继承
    ✅ 合理拆分:展示组件 vs 容器组件
    ✅ 命名:多单词 PascalCase(MyComponent)
    ✅ 避免 v-if 和 v-for 同时使用
    ✅ 大列表必须用 key,且不用 index

    20.2 常见模式

    受控组件模式

    <!– 父组件完全控制 –>
    <Input :value="text" @update:value="text = $event" />
    <!– 或 –>
    <Input v-model="text" />

    渲染代理 / 无渲染组件

    <!– RenderlessList.vue –>
    <script setup>
    defineProps({ items: Array })
    </script>

    <template>
    <slot :items="items" :isEmpty="!items.length" />
    </template>

    <!– 使用 –>
    <RenderlessList :items="todos" v-slot="{ items, isEmpty }">
    <p v-if="isEmpty">暂无数据</p>
    <ul v-else>
    <li v-for="item in items" :key="item.id">{{ item.text }}</li>
    </ul>
    </RenderlessList>

    递归组件

    <!– TreeNode.vue –>
    <script setup>
    defineProps({ node: Object })
    </script>

    <template>
    <li>
    {{ node.label }}
    <ul v-if="node.children">
    <TreeNode
    v-for="child in node.children"
    :key="child.id"
    :node="child"
    />
    </ul>
    </li>
    </template>

    20.3 错误处理

    <!– ErrorBoundary 组件 –>
    <script setup>
    import { ref, onErrorCaptured } from 'vue'

    const error = ref(null)

    onErrorCaptured((err, instance, info) => {
    error.value = err
    return false // 阻止继续向上传播
    })
    </script>

    <template>
    <div v-if="error" class="error">
    <p>出错了:{{ error.message }}</p>
    <button @click="error = null">重试</button>
    </div>
    <slot v-else />
    </template>

    20.4 项目结构推荐

    src/
    ├── api/ # API 请求封装
    │ ├── index.ts # axios 实例
    │ ├── user.ts
    │ └── post.ts
    ├── assets/ # 静态资源
    ├── components/ # 通用组件
    │ ├── ui/ # 基础 UI 组件
    │ └── business/ # 业务组件
    ├── composables/ # 组合式函数
    │ ├── useAuth.ts
    │ ├── useFetch.ts
    │ └── usePagination.ts
    ├── directives/ # 自定义指令
    ├── layouts/ # 布局组件
    ├── pages/ (views/) # 页面组件
    ├── router/ # 路由配置
    │ └── index.ts
    ├── stores/ # Pinia stores
    │ ├── user.ts
    │ └── app.ts
    ├── styles/ # 全局样式
    ├── types/ # TS 类型定义
    ├── utils/ # 工具函数
    ├── App.vue
    └── main.ts

    20.5 常用工具库生态

    类别推荐
    构建工具 Vite(官方)/ Nuxt
    路由 Vue Router 4
    状态管理 Pinia
    HTTP axios / ky / ofetch
    工具函数 VueUse(强烈推荐)
    UI 组件库 Element Plus / Ant Design Vue / Naive UI / Vuetify 3 / PrimeVue
    CSS Tailwind CSS / UnoCSS
    图标 @iconify/vue / lucide-vue-next
    表单验证 vee-validate + zod / valibot
    图表 ECharts / Chart.js
    动画 GSAP / @vueuse/motion
    国际化 vue-i18n
    测试 Vitest + Vue Test Utils + Playwright
    代码规范 ESLint + Prettier + husky

    20.6 VueUse 常用函数

    import {
    useStorage, // 响应式 localStorage
    useDark, // 暗黑模式
    useToggle, // 切换布尔值
    useMouse, // 鼠标位置
    useWindowSize, // 窗口尺寸
    useIntersectionObserver, // 交叉观察
    useEventListener, // 自动清理事件监听
    useDebounceFn, // 防抖函数
    useThrottleFn, // 节流函数
    useClipboard, // 剪贴板
    useMediaQuery, // 媒体查询
    useFetch, // 数据请求
    useVModel, // 组件 v-model 辅助
    } from '@vueuse/core'


    附录:Vue 3.4 / 3.5+ 新特性速览

    Vue 3.4

    • defineModel() 稳定版
    • watch 支持 { once: true }
    • 改进的响应式系统(更精确的依赖追踪)
    • 解析器性能提升 2 倍

    Vue 3.5

    • 响应式系统重写:内存占用降低 56%,使用双向链表替代 Set
    • useTemplateRef() 替代 ref + 模板 ref 字符串
    • useId() 生成 SSR 安全的唯一 ID
    • onWatcherCleanup() 清理函数
    • Lazy hydration(异步组件按需水合)
    • app.onUnmount() 清理应用级副作用

    Vue 3.6+(Vapor Mode,开发中)

    • Vapor Mode:无虚拟 DOM 的编译模式
    • 直接编译为命令式 DOM 操作
    • 性能接近原生 JS,内存占用大幅降低
    • 可与现有虚拟 DOM 组件混用

    快速参考卡片

    // 响应式
    ref(0) // 基本类型
    reactive({}) // 对象
    computed(() => {}) // 计算属性
    watch(source, cb, opts) // 侦听器
    watchEffect(fn) // 自动追踪

    // 生命周期
    onMounted / onUnmounted / onUpdated / onActivated

    // 组件通信
    defineProps / defineEmits / defineModel / defineExpose
    provide / inject
    useSlots / useAttrs

    // 工具
    toRef / toRefs / toRaw / markRaw
    readonly / shallowRef / shallowReactive
    nextTick / defineAsyncComponent

    // 路由
    useRouter / useRoute
    router.push / replace / back / go

    // Pinia
    defineStore / storeToRefs / $patch / $reset


    📚 参考资源:

    • Vue 3 官方文档
    • Vue Router 文档
    • Pinia 文档
    • VueUse 文档
    • Vue 3 RFC
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Vue 3 知识点总结
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!