简介:本资源是一套面向前端开发者与Vue进阶学习者的Vue3+TypeScript源码级学习资料,聚焦Composition API实践、类型系统深度集成及主流生态工具链应用,帮助开发者突破语法表层,理解响应式原理、编译时行为与工程化落地细节。压缩包含2002个文件,主体为1042篇结构化Markdown学习笔记(覆盖API解析、对比分析、踩坑记录)、803个可运行JS示例(含vue.global.js等核心构建产物验证脚本)及147个配置类JSON文件,整体63MB,目录组织体现从基础语法→类型定义→状态管理(Pinia/Vuex)→路由集成→UI组件适配的渐进式学习路径。已有575人学习下载,内容预览显示包含多版本Vue构建产物(如vue.runtime.esm-browser.js)与微任务测试页,佐证其对底层机制与兼容性实践的重视,适合希望夯实Vue3原理、提升TS工程规范性及快速搭建高质量前端项目的中高级开发者。
1. Vue3+TypeScript源码学习:不是抄API文档,而是亲手拆开响应式引擎和编译器黑匣子
你有没有试过在setup()里写ref,却说不清它为什么能触发更新?或者调试v-model双向绑定时,在 DevTools 里看到一堆Proxy嵌套,但根本不知道哪个effect正在监听你的computed?这不是你基础不牢——是 Vue3 的响应式系统和编译流程本身就被设计成「可插拔、可追踪、可替换」的模块化架构。这份 Vue3+TypeScript 源码学习资源,不是教你背onMounted参数顺序,而是把reactivity包从reactive.ts拆到effect.ts,把compiler-core从baseParse跟到transformElement,再用一个最小可运行的mini-vue工程,让你亲手改出自己的ref实现、注入自定义transform插件、甚至替换掉默认的scheduler。适合正在用 Vue3 写中后台但卡在「知道怎么用、不知道为什么这么用」的中级开发者;也适合想跳过「魔改 Element Plus」阶段、直接理解 Composition API 底层契约的 TypeScript 实践者。它不承诺「三天读完源码」,但保证你跑通第一个createApp().mount()后,就能定位到patch函数里hostInsert的真实调用栈。
2. 项目结构与核心模块定位:从packages/目录开始,找到你真正该盯住的五个文件夹
Vue 官方 monorepo 的packages/目录不是平铺直叙的代码堆,而是一个按职责分层的精密齿轮箱。这份学习资源已帮你过滤掉test-utils、devtools等外围包,聚焦在五个核心模块上。我们不讲「每个包都重要」,只告诉你哪几个文件夹改一行代码,就能立刻看到控制台打印出trigger的依赖追踪路径。
2.1reactivity:响应式系统的「心脏起搏器」
这是你必须先啃下的第一块硬骨头。reactivity不是单个类,而是一组协同工作的函数集合:
reactive.ts:暴露reactive(),内部调用createReactiveObject(),关键在new Proxy(target, mutableHandlers)—— 注意mutableHandlers里的get和set并非直接操作target,而是通过track()和trigger()与effect系统联动;effect.ts:effect()是响应式副作用的注册入口,activeEffect全局变量存储当前正在执行的 effect,track()把它推入targetMap的依赖 Map 中;ref.ts:ref()返回的是一个带value属性的对象,其get value和set value内部调用trackRefValue和triggerRefValue,本质是reactive({ value: xxx })的语法糖封装;computed.ts:computed()创建的是ComputedRefImpl实例,它继承RefImpl,但get value会先检查dirty标志位,dirty === true时才执行runner()(即用户传入的 getter 函数)并重置dirty = false。
提示:不要一上来就看
baseHandlers.ts里几十个Reflect调用。先在effect.ts的effect()函数开头加console.log('effect created:', fn),再在reactive.ts的gethandler 里加console.log('get triggered on', key),跑一个最简ref示例,你立刻能看到「谁触发了谁」的因果链。
2.2runtime-core:虚拟 DOM 渲染的「中央调度室」
runtime-core是连接响应式数据与真实 DOM 的桥梁。它不直接操作 DOM,而是定义了一套平台无关的渲染接口(RendererOptions),让runtime-dom去实现具体插入、删除逻辑。
renderer.ts:createRenderer()是核心工厂函数,返回{ render, createApp }。render()接收vnode和container,内部调用patch()进行 diff;patch.ts:patch()是 diff 算法主入口,根据vnode.type分发到processElement()、processComponent()、processText()等函数。重点看processElement()里的patchElement()—— 它调用patchProps()更新属性,patchChildren()处理子节点;component.ts:setupComponent()是 Composition API 的执行起点。它先调用initProps()解析props,再执行setupStatefulComponent(),后者才是真正调用用户setup()函数的地方,并将返回值合并进组件实例的setupState。
2.3compiler-core:模板编译的「词法分析器」
Vue 的<template>不是运行时解析,而是在构建时(或开发时)被@vue/compiler-core编译为可执行的 JavaScript 渲染函数。学习它,是为了理解v-if为何比v-show多一次patch,<slot>如何被转换为renderSlot()调用。
parse.ts:baseParse()是入口,将字符串模板转为 AST(抽象语法树)。它调用parseChildren()递归解析子节点,parseElement()处理标签,parseAttribute()提取指令如v-if、v-for;transform.ts:baseTransform()遍历 AST,调用一系列transformXXX插件(如transformIf、transformFor、transformSlotOutlet)。每个插件修改 AST 节点的codegenNode属性,为后续生成代码做准备;codegen.ts:generate()将处理后的 AST 转为字符串形式的渲染函数代码,核心是genNode()递归生成,对JSChildNode调用genNodeListAsArray(),对JSExpressionNode直接输出node.content。
2.4runtime-dom:浏览器平台的「肌肉组织」
runtime-dom实现了runtime-core定义的RendererOptions,是 Vue 在浏览器中真正干活的模块。
nodeOps.ts:导出所有 DOM 操作函数,如insert()、remove()、createElement()、createText()。insert(child, parent, anchor)是patch流程中插入新节点的关键;modules/attrs.ts、modules/class.ts、modules/style.ts:每个模块负责一类属性的更新逻辑。例如patchClass()判断prevValue和nextValue类型(string / object / array),再调用add/removeclassList 方法;index.ts:createApp()的最终实现,调用createRenderer()传入nodeOps和modules,返回app实例。
2.5shared:跨包共享的「工具箱」
shared包含大量类型定义和纯工具函数,是理解整个代码库的「字典」。
shapeFlags.ts:ShapeFlags是位掩码枚举,用于快速判断 vnode 类型。ELEMENT | COMPONENT表示一个普通元素或组件,SUSPENSE表示 Suspense 组件。isElement()函数就是vnode.shapeFlag & ShapeFlags.ELEMENT;looseEqual.ts:looseEqual()实现深度相等比较,用于v-model的check逻辑,它会递归比较对象键值、数组长度和元素;warning.ts:warn()函数统一控制警告输出,开发模式下打印红色警告,生产模式下静默 —— 这是你调试时console.warn不见了的原因。
3. 快速启动与调试配置:用 VS Code + Debugger 跑通mini-vue并打断点看ref执行流
别急着npm run build。这份学习资源附带一个精简版mini-vue工程,它只包含reactivity和runtime-core的最小依赖,能在 30 秒内启动并让你看到ref的完整调用链。关键不是「跑起来」,而是「断点能打进去」。
3.1 初始化mini-vue并安装依赖
# 解压资源后进入 mini-vue 目录 cd mini-vue # 使用 pnpm(推荐,避免 node_modules 嵌套过深) pnpm install # 启动开发服务器(基于 Vite) pnpm dev注意:
mini-vue的vite.config.ts已配置resolve.alias,将vue指向本地packages/runtime-dom/index.ts,确保你修改源码后热更新生效。如果pnpm dev报错Cannot find module 'vue',检查vite.config.ts中resolve.alias是否指向正确路径。
3.2 配置 VS Code Debugger,精准命中ref创建过程
在mini-vue根目录创建.vscode/launch.json:
{ "version": "0.2.0", "configurations": [ { "type": "pwa-chrome", "request": "launch", "name": "Launch Chrome against localhost", "url": "http://localhost:5173", "webRoot": "${workspaceFolder}", "sourceMapPathOverrides": { "webpack:///./src/*": "${webRoot}/src/*", "webpack:///src/*": "${webRoot}/src/*", "webpack:///./*": "${webRoot}/*", "webpack:///./~/*": "${webRoot}/node_modules/*" } } ] }然后在src/main.ts的createApp()调用前加断点:
// src/main.ts import { createApp } from 'vue' import App from './App.vue' // 在这一行打上断点 const app = createApp(App) app.mount('#app')启动调试(F5),Chrome 自动打开。此时打开 DevTools → Sources →webpack://→src/main.ts,断点已生效。按 F10 单步进入,你会看到调用栈一路跳进runtime-dom/index.ts→runtime-core/index.ts→reactivity/index.ts。
3.3 在ref.ts中设置关键断点,观察依赖收集全过程
打开packages/reactivity/src/ref.ts,在ref()函数内部设置断点:
export function ref<T>(value: T): Ref<T> { // 在这一行打断点:ref() 被调用时,value 是什么? const wrapper = { value } as Ref<T> // 在这一行打断点:trackRefValue() 是如何把当前 effect 记录下来的? trackRefValue(wrapper) return wrapper }同时,在packages/reactivity/src/effect.ts的effect()函数开头也设断点:
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions): ReactiveEffect<T> { // 在这一行打断点:effect() 创建时,fn 是什么? if ((effect as ReactiveEffect).active) { // ... } const e = createReactiveEffect(fn, options) // ... }现在,在src/App.vue的setup()中写:
import { ref, effect } from 'vue' export default { setup() { const count = ref(0) // 断点1:这里触发 ref() 创建 // 断点2:这里触发 effect() 注册,count.value 的读取会触发 track effect(() => { console.log('count is', count.value) // 断点3:这里触发 get value -> trackRefValue }) return { count } } }启动调试,F5 运行,断点会依次命中:
- 先停在
effect()创建处,fn是() => console.log(...); - 再停在
ref()创建处,value是0; - 最后停在
console.log前,此时count.value的get触发trackRefValue(),你可以在 Debug Console 输入activeEffect查看当前 effect 对象。
3.4 修改reactive.ts的gethandler,实时验证响应式触发
想确认trigger()是否真的被调用?直接在packages/reactivity/src/reactive.ts的gethandler 里加日志:
const get = /*#__PURE__*/ createGetter() function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { // 在这里加日志,key 是访问的属性名 console.log('[GET] target:', target, 'key:', key) const res = Reflect.get(target, key, receiver) // 如果不是只读且不是浅响应,则进行依赖收集 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } return res } }保存后,Vite 会自动热更新。在浏览器控制台,你会看到每次state.count被读取时,都打印[GET] target: {count: 0} key: count。这就是响应式系统「被动收集」的实证。
4. 常见问题排查:5 个血泪经验总结,避免你在effect嵌套和scheduler上翻车
源码调试不是线性阅读,而是不断「现象→原因→解决」的循环。以下是我在mini-vue上踩过的 5 个典型坑,每个都附带复现步骤和修复方案。
4.1 现象:effect回调执行两次,控制台打印两行count is 0
原因:effect()默认是lazy: false,创建时立即执行一次;之后count.value变更又触发一次。你以为是 bug,其实是设计如此。effect()的语义是「创建一个响应式副作用,首次执行 + 后续依赖变更时执行」。
解决:如果你只想监听变更、不想首次执行,显式传入lazy: true:
effect( () => { console.log('count changed to', count.value) }, { lazy: true } // 关键:首次不执行 ) count.value++ // 此时才会打印提示:
watch()内部就是用effect({ lazy: true })实现的,所以watch默认不执行回调,除非你加{ immediate: true }。
4.2 现象:修改ref的value,effect不触发,控制台无输出
原因:ref的value属性是通过Object.defineProperty或Proxy的get/set控制的,但如果你直接给ref对象赋新值(如count = ref(1)),就丢失了响应式引用。count变量本身不是响应式,它只是指向一个响应式对象的指针。
解决:永远通过.value修改:
const count = ref(0) count.value = 1 // ✅ 正确:触发 trigger // count = ref(1) // ❌ 错误:count 变量被重新赋值,原 ref 失去引用4.3 现象:computed的 getter 执行了,但value没更新,console.log(computedRef.value)还是旧值
原因:computed是懒执行的,它的valuegetter 会先检查dirty标志位。如果dirty === false,直接返回缓存的value,不执行用户 getter。而dirty只在依赖的ref或reactive变更时由triggerRefValue()或trigger()设为true。
解决:确保computed依赖的响应式数据确实发生了变更。在computed.ts的ComputedRefImpl.get value里加断点,观察this._dirty的值变化:
get value() { console.log('computed get value, dirty:', this._dirty) // 加这行 if (this._dirty) { this._dirty = false this._value = this._getter() } return this._value }4.4 现象:v-model绑定的input输入后,ref值变了,但视图没更新
原因:v-model在<input>上编译为:value+@input,但@input的事件处理器可能没正确触发ref.value = $event.target.value。常见于手动写了@input但忘了e.preventDefault()或e.stopPropagation()干扰了默认行为。
解决:检查compiler-core生成的渲染函数。在mini-vue的dist/index.js(或 DevTools 的 Sources 中vue模块)里搜索vModelText,找到类似:
withDirectives(_openBlock(), [ _createBlock("input", { "onUpdate:modelValue": $event => (searchQuery.value = $event) }, null, 8 /* PROPS */, ["onUpdate:modelValue"]) ])确认onUpdate:modelValue的回调是否正确赋值。如果不是,说明transform阶段的transformModel插件没生效,检查compiler-core/src/transforms/vModel.ts是否被正确 import。
4.5 现象:setTimeout修改ref,视图更新延迟一帧,console.log在patch之后才打印
原因:Vue 的scheduler默认使用Promise.then()或queueMicrotask()进行异步批处理。setTimeout是宏任务,会等到 microtask 队列清空后才执行,导致ref.value++的trigger()调用被排在patch之后。
解决:强制同步更新(仅调试用):在reactivity/src/scheduler.ts的queueJob()函数开头加:
export function queueJob(job: Job) { // 临时注释掉异步队列,改为立即执行 // if (!queue.length) { // queueFlush() // } job() // ✅ 立即执行 job,即 patch }注意:这只是调试手段!生产环境必须用异步队列保证性能。真正的优化点在
scheduler.ts的nextTick实现,它用Promise.resolve().then()确保在下一个 microtask 执行。
5. 核心模块代码逐行解析:reactive.ts的createReactiveObject与effect.ts的track机制
现在我们沉到底层,把reactive()和effect()的协作关系,用代码块 + 逐行注释的方式彻底讲透。这不是 API 文档,而是你调试时真正要盯住的 20 行核心逻辑。
5.1createReactiveObject:如何用Proxy构建响应式代理
打开packages/reactivity/src/reactive.ts,找到createReactiveObject()函数(Vue 3.4+ 版本中此函数已重构,但逻辑不变):
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any> ) { // 1. 如果 target 已经是 Proxy,直接返回,避免重复代理 if (target[ReactiveFlags.RAW]) { return target } // 2. 检查 target 是否已被代理过,从 proxyMap 缓存中取 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // 3. 根据 target 类型(Object/Array/Set/Map)选择 handler const handlers = targetType(target) === TargetType.COLLECTION ? collectionHandlers // Set/Map 用 collectionHandlers : baseHandlers // Object/Array 用 baseHandlers // 4. 创建 Proxy,target 是原始对象,handlers 是拦截规则 const proxy = new Proxy(target, handlers) // 5. 将 proxy 缓存到 proxyMap,避免重复创建 proxyMap.set(target, proxy) return proxy }参数说明:
target:原始对象,如{ count: 0 };isReadonly:是否只读,影响sethandler 的行为(只读时set直接返回false);baseHandlers:mutableHandlers或readonlyHandlers,定义get/set/deleteProperty等拦截逻辑;collectionHandlers:针对Set/Map的特殊 handler,因为它们的size、has等方法需要单独处理;proxyMap:WeakMap<Target, Proxy>,缓存已代理的对象,避免内存泄漏。
关键点:proxyMap是全局唯一的,它确保同一个target只有一个proxy。这也是为什么toRaw(proxy)能拿到原始对象 ——proxy[ReactiveFlags.RAW]就是target。
5.2track:依赖收集的「登记簿」如何工作
track()是响应式系统的基石,它定义了「谁(effect)在什么时候(get 时)关注了谁(target)的哪个属性(key)」。打开packages/reactivity/src/effect.ts:
// 1. 全局 targetMap:Map<target, keyToDepMap> export const targetMap = new WeakMap<any, KeyToDepMap>() // 2. 当前正在执行的 effect,全局变量 let activeEffect: ReactiveEffect | undefined // 3. track 函数:在 get 时调用,记录依赖 export function track(target: object, type: TrackOpTypes, key: unknown) { // 3.1 如果没有 activeEffect,说明不是在 effect 内部读取,不收集 if (!isTracking()) { return } // 3.2 从 targetMap 获取 target 对应的 keyToDepMap // 如果不存在,新建一个 Map<key, Set<effect>> let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } // 3.3 从 depsMap 获取 key 对应的 dep(Set<effect>) // 如果不存在,新建一个 Set let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = createDep())) } // 3.4 将当前 activeEffect 添加到 dep 中 // createDep() 返回的是 Set,所以 add(activeEffect) 就是登记 if (!dep.has(activeEffect)) { dep.add(activeEffect) // 3.5 同时将 dep 添加到 activeEffect 的 deps 数组中,形成双向引用 // 这样 effect cleanup 时可以反向清除 activeEffect?.deps.push(dep) } } // 4. isTracking 判断是否在 effect 内部 export function isTracking() { return activeEffect !== undefined && !activeEffect?.dirty }逐行逻辑说明:
targetMap是一个WeakMap,键是原始对象target,值是keyToDepMap(Map<key, Set<effect>>)。WeakMap的好处是当target被 GC 时,targetMap中的条目自动消失,避免内存泄漏;activeEffect是全局变量,它在effect()执行时被赋值,在effect执行完毕后被重置为undefined。这就是为什么只有在effect函数体内读取ref.value才会触发track;track()的核心是三层嵌套:targetMap → keyToDepMap → dep(Set)。dep是一个Set,里面存的是所有依赖这个target.key的effect;activeEffect?.deps.push(dep)是关键的双向引用:effect记录自己依赖了哪些dep,这样当effect被 stop 时,可以遍历deps并从每个dep中删除自己,实现精准清理。
5.3trigger:状态变更时,如何精准通知所有依赖
trigger()是track()的镜像操作,它在set时被调用,根据target和key找到所有依赖的effect并执行。简化版如下:
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { // 1. 从 targetMap 获取 target 对应的 depsMap const depsMap = targetMap.get(target) if (!depsMap) return // 2. 创建 effects 数组,用于收集要执行的 effect let effects: ReactiveEffect[] = [] // 3. 根据 type(SET/ADD/DELETE)决定收集哪些 effect // 如果 key 存在,只收集 key 对应的 dep if (key !== void 0) { effects.push(...depsMap.get(key)) } // 4. 如果是数组的 length 变更,或 Map/Set 的 clear,需要收集所有 effect // (因为 length 变更会影响 for...of,clear 会影响所有 key) if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY effects.push(...depsMap.get(iterationKey)) } // 5. 执行所有收集到的 effect // 注意:这里会去重,避免同一个 effect 被执行多次 const effectsToRun = new Set<ReactiveEffect>() for (const effect of effects) { if (effect !== activeEffect || effect.allowRecurse) { effectsToRun.add(effect) } } // 6. 调用 scheduler,排队执行 for (const effect of effectsToRun) { if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() } } }参数说明:
type:TriggerOpTypes.SET(普通属性赋值)、ADD(数组 push、Map set)、DELETE(delete、Map delete);key:触发变更的属性名,如count;newValue/oldValue:新旧值,用于watch的回调参数;oldTarget:Map/Set的旧值,用于clear操作。
关键点:trigger()不是简单地遍历depsMap,而是根据type精准匹配。比如arr.push(1)会触发type=ADD和key='length',因此会收集depsMap.get('length')中的所有effect,这就是为什么v-for能响应数组长度变化。
6. 二次开发实战:如何为mini-vue添加一个useDebounce组合式函数,并接入runtime-core的scheduler
学源码的终极目的,不是为了「看懂」,而是为了「能改」。现在我们动手,基于reactivity和runtime-core的现有能力,封装一个生产可用的useDebounce,并让它与 Vue 的响应式调度深度集成,而不是用setTimeout简单包裹。
6.1 设计目标:一个真正 Vue 化的防抖 Hook
我们要的useDebounce不是:
// ❌ 简单 setTimeout,脱离 Vue 调度,无法被 `flush: 'post'` 控制 function useDebounce(fn, delay) { let timer return (...args) => { clearTimeout(timer) timer = setTimeout(() => fn(...args), delay) } }而是:
// ✅ Vue 原生风格:返回 ref,支持 `watch`,可被 `nextTick` 控制 const debouncedSearch = useDebounce(searchQuery, 300) watch(debouncedSearch, (val) => { // val 是防抖后的 searchQuery.value api.search(val) })6.2 实现原理:利用effect的scheduler和job队列
Vue 的scheduler本质是一个微任务队列。我们可以把防抖逻辑塞进effect的scheduler里,让debouncedRef.value的变更,和patch更新一样,走同一套调度系统。
// packages/runtime-core/src/apiUseDebounce.ts import { ref, Ref, watch, onBeforeUnmount, nextTick } from 'vue' import { queueJob, flushPostFlushCbs } from '../scheduler' export function useDebounce<T>(source: Ref<T>, delay: number): Ref<T> { // 1. 创建一个内部 ref,存储防抖后的值 const debounced = ref<T>(source.value) // 2. 创建一个 job,用于在 delay 后更新 debounced.value let job: (() => void) | null = null // 3. 创建一个 effect,监听 source 变更 // 注意:这里用 watch 而不是 effect,因为 watch 更易管理 const stop = watch( source, (newValue) => { // 4. 取消之前的 job if (job) { queueJob(() => {}) // 清空 job 队列中的旧 job job = null } // 5. 创建新 job,delay 后执行 job = () => { debounced.value = newValue job = null } // 6. 将 job 推入 Vue 的 job 队列,而不是 setTimeout // 这样它会和 patch 一起被 flushPostFlushCbs 执行 queueJob(job) }, { immediate: false } ) // 7. 组件卸载时清理 onBeforeUnmount(() => { stop() if (job) { queueJob(() => {}) // 清空残留 job job = null } }) return debounced }为什么用queueJob而不用setTimeout?
queueJob是 Vue 的微任务调度器,它保证job在patch之后、DOM 更新之前执行(flush: 'post');setTimeout是宏任务,会等到整个 JS 调用栈清空后才执行,可能错过nextTick的时机;queueJob支持flushPostFlushCbs,你可以用await nextTick()等待防抖完成。
6.3 集成到mini-vue并验证
在mini-vue/src/main.ts中测试:
import { createApp } from 'vue' import { useDebounce } from './apiUseDebounce' // 引入你写的 hook import App from './App.vue' const app = createApp(App) // 在 app 上挂载,方便全局使用 app.config.globalProperties.$useDebounce = useDebounce app.mount('#app')在App.vue中使用:
<script setup> import { ref, watch } from 'vue' import { useDebounce } from './apiUseDebounce' const searchQuery = ref('') const debouncedQuery = useDebounce(searchQuery, 500) // 这个 watch 会在 debouncedQuery.value 变更时触发,且只触发一次 watch(debouncedQuery, (val) => { console.log('Debounced search:', val) // 这里调用 API }) </script> <template> <input v-model="searchQuery" placeholder="Search..." /> <div>Current: {{ searchQuery }}</div> <div>Debounced: {{ debouncedQuery }}</div> </template>验证方法:
- 在输入框快速输入
abcde; - 观察控制台:
Debounced search: abcde只打印一次,且在输入停止 500ms 后; - 打开 DevTools → Performance,录制一次输入,查看
queueJob的调用栈,确认它出现在flushPostFlushCbs附近。
6.4 进阶改造:支持flush选项,对接不同调度时机
useDebounce可以更进一步,支持flush: 'pre' | 'post' | 'sync',就像watch一样:
export function useDebounce<T>( source: Ref<T>, delay: number, options: { flush?: 'pre' | 'post' | 'sync' } = {} ): Ref<T> { const { flush = 'post' } = options const debounced = ref<T>(source.value) let job: (() => void) | null = null const stop = watch( source, (newValue) => { if (job) { // 根据 flush 选项选择调度方式 if (flush === 'sync') { job() } else { queueJob(job) } job = null } job = () => { debounced.value = newValue job = null } // flush: 'pre' 时,job 在 patch 之前执行 if (flush === 'pre') { queueJob(job) } } ) return debounced }flush语义:
'sync':立即执行,不经过队列(慎用,可能破坏响应式链路);'pre':在patch之前执行,适用于需要在 DOM 更新前计算的场景;'post':在patch之后执行,是默认行为,保证 DOM 已更新。
从那以后我每次封装组合式
本文还有配套的精品资源,点击获取