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

相关推荐
장숙혜7 分钟前
ElementUi的Dropdown下拉菜单的详细介绍及使用
前端·javascript·vue.js
火柴盒zhang10 分钟前
websheet之 编辑器
开发语言·前端·javascript·编辑器·spreadsheet·websheet
某公司摸鱼前端13 分钟前
uniapp 仿企微左边公司切换页
前端·uni-app·企业微信
WKK_16 分钟前
uniapp自定义封装tabbar
前端·javascript·小程序·uni-app
莫问alicia16 分钟前
react 常用钩子 hooks 总结
前端·javascript·react.js
Mintopia25 分钟前
图形学中的数学基础与 JavaScript 实践
前端·javascript·计算机图形学
Mintopia32 分钟前
Three.js 制作飘摇的草:从基础到进阶的全流程教学
前端·javascript·three.js
BillKu32 分钟前
Vue3父子组件数据双向同步实现方法
前端·javascript·vue.js
红尘散仙1 小时前
七、WebGPU 基础入门——Texture 纹理
前端·rust·gpu
jaywongX1 小时前
Base64编码原理:二进制数据与文本的转换技术
前端·javascript·vue