第七章 响应式的 watch 实现【手摸手带你实现一个vue3】

大家好,我是作曲家种太阳,今天我们来手把手去看看 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 的实现逻辑

  1. 基于 ReactiveEffect 创建 watcher 任务
  2. 通过 getter 收集依赖(ref.value、reactive 属性)
  3. 用 scheduler 包裹更新逻辑,防抖调度
  4. 用 traverse 实现深度监听
  5. 支持 immediate、deep 等选项

✅ 响应系统回顾

模块 功能描述 特点说明
reactive 为对象创建响应式代理 Proxy 劫持 getter/setter
ref 为基本类型/对象包裹响应性 .value 存取值
computed 创建惰性缓存的计算属性 调度器 + 脏值判断
watch 监听响应式数据变化触发回调 traverse + scheduler

至此,我们完整实现了 Vue3 的响应系统核心部分!接下来,我们将进入全新章节:渲染系统实现,敬请期待 🚀

相关推荐
IT_陈寒6 分钟前
SpringBoot自动配置坑了我一周,原来问题这么蠢!
前端·人工智能·后端
kyriewen22 分钟前
我review了一份Vibe Coding写的前端代码——能跑,但5个地方迟早要命
前端·javascript·ai编程
REDcker1 小时前
Cesium三维WebGIS入门详解
前端·gis·web·cesium·webgis
0暗影流光01 小时前
十倍效能提升——Web 基础研发体系的建立
前端
前端一课1 小时前
用Agent Skill重构代码:300行缩至30行的实战教程
前端
腻害兔1 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:IM 即时通讯模块,一个被低估的「全功能聊天系统」
java·前端·vue.js·产品经理·ai编程
奎叔2 小时前
Flutter 异常处理体系:从捕获、转换到恢复
前端
心念枕惊2 小时前
.NET CORE 授权进阶-角色、策略与动态权限实现
java·前端·.netcore
小徐_23332 小时前
寓言故事一则:狗猛酒酸
前端·ai编程
xiaobaoyu2 小时前
vue2+element开发中问题和解决方式
前端