大家好,我是作曲家种太阳,今天我们来手把手去看看 Vue 3 响应式系统中非常核心的一部分:watch 是怎么个执行流程和触发流程,深入了解watch的原理和设计理念,watch 是 Vue 响应系统中非常重要的一环,理解其实现原理有助于深入掌握 Vue 的响应机制。
✅ 1. 创建 watch
函数主结构
文件路径:packages/runtime-core/src/apiWatch.ts
我们先实现 watch
函数的主结构及其核心逻辑:
ts
/**
* watch 配置项属性
*/
export interface WatchOptions<Immediate = boolean> {
immediate?: Immediate
deep?: boolean
}
/**
* 指定的 watch 函数
* @param source 监听的响应性数据
* @param cb 回调函数
* @param options 配置对象
* @returns
*/
export function watch(source, cb: Function, options?: WatchOptions) {
return doWatch(source as any, cb, options)
}
function doWatch(
source,
cb: Function,
{ immediate, deep }: WatchOptions = EMPTY_OBJ
) {
// 触发 getter 的指定函数
let getter: () => any
// 判断 source 的数据类型
if (isReactive(source)) {
// 指定 getter
getter = () => source
// 深度
deep = true
} else {
getter = () => {}
}
// 存在回调函数和deep
if (cb && deep) {
// TODO
const baseGetter = getter
getter = () => baseGetter()
}
// 旧值
let oldValue = {}
// job 执行方法
const job = () => {
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (deep || hasChanged(newValue, oldValue)) {
cb(newValue, oldValue)
oldValue = newValue
}
}
}
// 调度器
let scheduler = () => queuePreFlushCb(job)
const effect = new ReactiveEffect(getter, scheduler)
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else {
effect.run()
}
return () => {
effect.stop()
}
}
这一部分构建了基本的
watch
行为逻辑,包括:数据源识别、依赖收集逻辑包裹、job 执行器、scheduler 调度机制。
✅ 2. 为 reactive 类型数据添加标识
文件路径:packages/reactivity/src/reactive.ts
我们需要为 reactive 数据打上标记,以便 watch
能判断 source 类型:
ts
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isReactive'
}
function createReactiveObject(
...
) {
...
// 未被代理则生成 proxy 实例
const proxy = new Proxy(target, baseHandlers)
// 为 Reactive 增加标记
proxy[ReactiveFlags.IS_REACTIVE] = true
...
}
✅ 3. scheduler.ts的实现
文件路径:packages/reactivity/src/scheduler.ts
js
// 对应 promise 的 pending 状态
let isFlushPending = false
/**
* promise.resolve()
*/
const resolvedPromise = Promise.resolve() as Promise<any>
/**
* 当前的执行任务
*/
let currentFlushPromise: Promise<void> | null = null
/**
* 待执行的任务队列
*/
const pendingPreFlushCbs: Function[] = []
/**
* 队列预处理函数
*/
export function queuePreFlushCb(cb: Function) {
queueCb(cb, pendingPreFlushCbs)
}
/**
* 队列处理函数
*/
function queueCb(cb: Function, pendingQueue: Function[]) {
// 将所有的回调函数,放入队列中
pendingQueue.push(cb)
queueFlush()
}
/**
* 依次处理队列中执行函数
*/
function queueFlush() {
if (!isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
/**
* 处理队列
*/
function flushJobs() {
isFlushPending = false
flushPreFlushCbs()
}
/**
* 依次处理队列中的任务
*/
export function flushPreFlushCbs() {
if (pendingPreFlushCbs.length) {
// 去重
let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
// 清空就数据
pendingPreFlushCbs.length = 0
// 循环处理
for (let i = 0; i < activePreFlushCbs.length; i++) {
activePreFlushCbs[i]()
}
}
}
🌟 scheduler 功能概述
这段调度器代码实现了一个 微任务级别的异步执行队列,其主要职责包括:
- 任务收集:接收多个异步副作用回调函数,加入到一个待处理队列中。
- 防抖处理:即使多次触发收集,也只会创建一个微任务,避免重复执行。
- 去重优化:相同的任务只会执行一次,避免重复调用。
- 异步批处理:通过 Promise.resolve().then(...) 创建微任务,批量执行所有收集的任务,确保副作用更新在 DOM 渲染前完成(pre flush)。
🔧 scheduler 模块说明
函数名 | 作用说明 |
---|---|
isFlushPending | 是否已经安排过一次微任务执行,防止重复调度 |
resolvedPromise | 一个已 resolve 的 Promise,用来创建微任务 |
pendingPreFlushCbs | 一个数组,用来临时缓存所有待处理的回调函数 |
queuePreFlushCb(cb) | 外部 API,用于将回调加入任务队列,并安排微任务调度 |
queueCb(cb, queue) | 将回调添加进任务队列,并触发 queueFlush() |
queueFlush() | 利用微任务机制调度 flushJobs() |
flushJobs() | 真正开始处理任务队列的执行函数(只负责调用) |
flushPreFlushCbs() | 遍历任务队列,去重执行所有回调 |
✅ 3. 创建 EMPTY_OBJ 占位对象
文件路径:packages/shared/src/index.ts
ts
export const EMPTY_OBJ: { readonly [key: string]: any } = {}
用于 doWatch
默认参数避免 undefined 判断。
✅ 4. 导出 watch 函数
分别在以下文件中导出 watch
:
packages/runtime-core/src/index.ts
packages/vue/src/index.ts
ts
export { watch } from '@vue/runtime-core'
✅ 5. 测试验证
文件路径:packages/vue/examples/reactivity/watch.html
html
<script>
const { reactive, watch } = Vue
const obj = reactive({ name: '张三' })
watch(obj, (val, oldVal) => {
console.log('watch 被触发')
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
</script>
此时运行却发现,watch 监听不到响应式数据的变化。
watch 下的依赖收集机制
测试的时候,为什么监听不到?
答案是:我们没有进行依赖收集。
✅ 解决方案:实现 traverse
收集 getter 行为
文件路径:packages/runtime-core/src/apiWatch.ts
ts
function traverse(value: unknown, seen: Set<unknown> = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) return value
seen.add(value)
for (const key in value) {
traverse((value as any)[key], seen)
}
return value
}
在 doWatch
中调整 getter 实现:
ts
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
✅ 验证:再次运行测试
watch 成功响应 reactive
的属性变化。
watch 的扩展与支持
- 支持
immediate: true
✅ - 支持
ref.value
✅
新增测试:packages/vue/examples/reactivity/watch-2.html
html
<script>
const { ref, watch } = Vue
const obj = ref({ name: '张三' })
watch(obj.value, (val, oldVal) => {
console.log('watch 被触发')
}, { immediate: true })
setTimeout(() => {
obj.value.name = '李四'
}, 2000)
</script>
✅ watch 的实现逻辑
- 基于 ReactiveEffect 创建 watcher 任务
- 通过 getter 收集依赖(ref.value、reactive 属性)
- 用 scheduler 包裹更新逻辑,防抖调度
- 用 traverse 实现深度监听
- 支持 immediate、deep 等选项
✅ 响应系统回顾
模块 | 功能描述 | 特点说明 |
---|---|---|
reactive | 为对象创建响应式代理 | Proxy 劫持 getter/setter |
ref | 为基本类型/对象包裹响应性 | .value 存取值 |
computed | 创建惰性缓存的计算属性 | 调度器 + 脏值判断 |
watch | 监听响应式数据变化触发回调 | traverse + scheduler |
至此,我们完整实现了 Vue3 的响应系统核心部分!接下来,我们将进入全新章节:渲染系统实现,敬请期待 🚀