第七章 响应式的 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 的响应系统核心部分!接下来,我们将进入全新章节:渲染系统实现,敬请期待 🚀

相关推荐
Moment5 分钟前
面试官:一个接口使用postman这些测试很快,但是页面加载很慢怎么回事 😤😤😤
前端·后端·面试
诗书画唱9 分钟前
【前端面试题】JavaScript 核心知识点解析(第二十二题到第六十一题)
开发语言·前端·javascript
excel15 分钟前
前端必备:从能力检测到 UA-CH,浏览器客户端检测的完整指南
前端
前端小巷子22 分钟前
Vue 3全面提速剖析
前端·vue.js·面试
悟空聊架构29 分钟前
我的网站被攻击了,被干掉了 120G 流量,还在持续攻击中...
java·前端·架构
CodeSheep30 分钟前
国内 IT 公司时薪排行榜。
前端·后端·程序员
尖椒土豆sss34 分钟前
踩坑vue项目中使用 iframe 嵌套子系统无法登录,不报错问题!
前端·vue.js
遗悲风35 分钟前
html二次作业
前端·html
江城开朗的豌豆38 分钟前
React输入框优化:如何精准获取用户输入完成后的最终值?
前端·javascript·全栈
CF14年老兵38 分钟前
从卡顿到飞驰:我是如何用WebAssembly引爆React性能的
前端·react.js·trae