2025.09.21
watch:核心实现
watch 允许在 响应式数据发生变化时,执行特定的副作用(Side Effect)。核心思想是基于 effect 的 调度机制(
scheduler)。
watch 允许在 响应式数据发生变化时,执行特定的副作用(Side Effect)。核心思想是基于 effect 的 调度机制(
scheduler)。
⚙️ watch 参数回顾
| 参数 | 描述 |
|---|---|
source | 监听的数据源, 可以是 ref reactive对象,或是一个返回监听值的 getter 函数 |
cb | 回调函数, 接受(newValue, oldValue)作为参数 |
options | 配置项,如(immediate(立即执行)、dep(深度监听), once(只执行一次)) |
🛠️ 基础实现
测试代码:
<script type="module">
import {ref, watch} from '../dist/reactivity.esm.js'
const count = ref(0)
watch(count, (newV, oldV) => {
console.log('--- Watch Fired ---')
console.log('newV:', newV, 'oldV:', oldV)
})
// 模拟响应式数据更新
setTimeout(() => {
count.value = 1
// 理想输出: newV: 1, oldV: 0
}, 100)
</script>
- 实现
watch时, 直接使用reactiveEffect类,而不是effect函数
effect 函数返回的是一个 runner。要获取 effect 内部 fn 的返回值(即监听数据的当前值),我们必须通过
ReactiveEffect 实例调用 effect.run()。
// 原始的 effect 实现示例
export function effect(fn, options = {}) {
const e = new ReactiveEffect(fn)
Object.assign(e, options)
e.run() // 首次执行并收集依赖
const runner = e.run.bind(e)
runner.effect = e
return runner // runner 本身不返回 fn 的值
}
- 包装
source为getter函数ReactiveEffect构造函数需要传入一个函数 (getter)。因为source可能是一个ref对象(而不是函数),我们需要对其进行统一的包装。import type { RefImpl } from './ref' import { EMPTY_OBJ } from '@vue/shared' import { ReactiveEffect } from './effect' import { isRef } from './ref' // --- 类型定义 --- export interface WatchOptions { immediate?: boolean deep?: boolean | number once?: boolean } // ----------------- export function watch( source, cb, options: WatchOptions = EMPTY_OBJ ) { let getter: () => any // 1. 统一 source 为 getter 函数 if (isRef(source)) { // 如果是 ref,getter 就是返回其 .value 的函数 getter = () => source.value } else { // 假设是函数(针对 reactive 或 computed,此处简化) getter = source as () => any } // 2. 创建 ReactiveEffect 实例 const effect = new ReactiveEffect(getter) } - ⚙️ 核心:实现
job调度器
job 函数是作为 effect.scheduler 传入的,它定义了当依赖数据更新时需要执行的操作流程。
| 步骤 | 目的 | 代码实现 |
|---|---|---|
| 1. 获取新值 | 调用 effect.run() 获取最新的值。重新收集依赖 | const newValue = effect.run() |
| 2. 执行回调 | 调用提供的 cb, 传入 newValue 和之前保存的 oldValue | cb(newValue,oldValue) |
| 3. 更新旧值 | 将当前的 newValue 保存为下一次变化的 oldValue | oldValue = newValue |
import { ReactiveEffect } from './effect'
// ... (其他导入)
export function watch(
source: WatchSource,
cb,
options: WatchOptions = EMPTY_OBJ
) {
let getter: () => any
let oldValue: any // 用于存储上一次的值
if (isRef(source)) {
getter = () => source.value
}
else {
getter = source as () => any
}
// --- 定义核心调度器 ---
function job() {
// 1. 运行 effect 拿到 newValue (同时重新收集依赖)
const newValue = effect.run()
// 2. 执行用户的回调
cb(newValue, oldValue)
// 3. 更新旧值,为下一次变化做准备
oldValue = newValue
}
// -----------------------
// 1. 创建 effect 实例
const effect = new ReactiveEffect(getter)
// 2. 设置调度器:当依赖变化时,执行 job 而不是 effect.run()
effect.scheduler = job
// 3. 首次执行:收集依赖,并获取初始的 oldValue
// effect.run() 的返回值就是 getter 的返回值
oldValue = effect.run()
return () => {
}
}
🛑 停止监听 (stop)
为了实现停止监听的功能,核心在于增强 ReactiveEffect 类,赋予它管理依赖和控制激活状态的能力。当调用 stop() 时,需要清除当前
effect 收集到的所有依赖,并将其标记为非激活状态。
<script type="module">
import {ref, watch} from '../dist/reactivity.esm.js'
const count = ref(0)
// 接收 stop 函数
const stop = watch(count, (newV, oldV) => {
console.log('--- Watch Fired ---')
console.log('newV:', newV, 'oldV:', oldV)
})
setTimeout(() => {
count.value = 1 // 第一次触发: Fired (newV: 1, oldV: 0)
setTimeout(() => {
stop() // 停止监听!
count.value = 2 // 第二次更新:不会触发 watch 回调
}, 100)
}, 100)
</script>
- 增强
ReactiveEffect类 (支持active和stop)export class ReactiveEffect implements ReactiveNode { // ... (deps, depsTail, tracking, dirty 属性省略) // 🔴 核心属性:用于控制是否激活 active = true constructor(public fn: Function) { /* ... */ } // ... (run, notify, scheduler 方法省略) /** * 🛑 停止依赖追踪和响应 */ stop() { if (this.active) { // 1. 清理依赖:通过 startTrack/endTack 机制清除当前 effect 上收集的所有依赖。 // (这里的 startTrack(this) / endTack(this) 内部负责遍历并从 dep 中移除 effect) startTrack(this) endTack(this) // 2. 标记为非激活状态,阻止未来的 run() 执行依赖收集 this.active = false } } } - 返回
stop函数
export function watch(
source,
cb,
options: WatchOptions = EMPTY_OBJ
) {
// ...
/**
* 🛑 返回给用户的停止监听函数
*/
function stop() {
effect.stop() // 调用 ReactiveEffect 实例的 stop 方法
}
return stop // 将停止函数返回
}
🔧 options 参数处理
- 从 options 中解构出所需的配置项:
export function watch( source, cb, options: WatchOptions = EMPTY_OBJ ) { const { immediate, once, deep } = options // ... (其他 watch 逻辑) }
🚀 immediate (立即执行)
// ... (定义 job() 和 effect 实例后)
if (immediate) {
// 立即执行 job,cb(newValue, undefined)
job()
}
else {
// 默认行为:首次执行只收集依赖并获取 oldValue
oldValue = effect.run()
}
// ...
♻️ once (只执行一次)
// ... (options 解构后)
if (once) {
const _cb = cb // 存储原始回调
// 重新赋值 cb 为包装后的函数
cb = (...args: any[]) => {
_cb(...args) // 1. 执行原始回调
stop() // 2. 自动调用 stop 停止监听
}
}
// ... (watch 逻辑继续,确保 stop 函数在 watch 作用域内可用)
🧭 deep (深度监听)
在 effect 首次运行时,递归地访问被监听对象的所有嵌套属性,从而触发所有深层属性的 getter,将它们全部收集为依赖。
import { isObject } from '@vue/shared'
/**
* 递归遍历对象,触发所有属性的 getter
* @param value 待遍历的值
* @param depth 剩余递归深度 (Infinity 表示无限制)
* @param seen 用于处理循环引用的 Set
*/
function traverse(value, depth: number = Infinity, seen = new Set()) {
// 边界条件 1: 不是对象,停止
if (!isObject(value))
return value
// 边界条件 2: 已经访问过,停止 (防止循环引用)
if (seen.has(value))
return value
// 边界条件 3: 深度用尽,停止
if (depth <= 0)
return value
seen.add(value)
depth-- // 消耗一层深度
// 递归遍历所有 key
for (const key in value) {
// 访问 value[key],触发 getter 并收集依赖
traverse(value[key], depth, seen)
}
return value // 返回原始值
}
export function watch(
source,
cb,
options: WatchOptions = EMPTY_OBJ
) {
const { immediate, once, deep } = options
// ... (处理 source 转换为基础 getter 的逻辑)
// 深度处理
if (deep) {
const baseGetter = getter // 存储原始 getter
// 确定递归深度
// true 表示无限深度,数字表示指定层级
const depth = deep === true ? Infinity : deep
// 重新定义 getter:执行原始 getter 并对返回值进行深度遍历
getter = () => traverse(baseGetter(), depth)
}
// ... (创建 effect, job, stop, immediate 逻辑)
}
💡 Vue 3.5 层级控制: 通过将
deep设置为数字(如 { deep: 2 }),可以精细控制traverse的递归深度,例如只监听两层嵌套属性的变化,以优化性能。