1. 为什么需要自己实现 Vue 的 watch 功能
在 Vue 3.4 的响应式系统中,watch 是一个核心功能,它允许我们观察响应式数据的变化并执行相应的回调函数。理解其内部实现原理对于深入掌握 Vue 的响应式机制至关重要。
1.1 watch 的核心作用
watch 的主要功能是监听响应式数据的变化,并在变化发生时执行用户定义的回调函数。与 computed 不同,watch 更适合执行有副作用的操作,比如异步请求、DOM 操作等。在实际开发中,我们经常使用 watch 来:
- 监听表单输入的变化
- 在路由参数变化时重新获取数据
- 响应复杂对象内部属性的变化
- 执行一些需要在特定状态变化时触发的操作
1.2 响应式系统基础
要实现 watch,首先需要理解 Vue 3 响应式系统的几个核心概念:
-
Reactive 和 Ref:Vue 3 使用 Proxy 实现了响应式系统。reactive() 用于创建响应式对象,ref() 用于创建包含 value 属性的响应式引用。
-
依赖收集:当访问响应式数据时,Vue 会记录当前正在运行的 effect(副作用函数)。
-
触发更新:当响应式数据变化时,Vue 会通知所有依赖该数据的 effect 重新执行。
1.3 watch 与 watchEffect 的区别
虽然 watch 和 watchEffect 都是用来监听响应式数据变化的,但它们有几个关键区别:
-
惰性执行:watch 默认是惰性的,只有在监听的数据变化时才会执行回调;而 watchEffect 会立即执行一次。
-
访问旧值:watch 可以获取变化前的旧值和新值;watchEffect 只能获取当前值。
-
监听方式:watch 需要明确指定要监听的数据源;watchEffect 会自动收集依赖。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现基础的响应式系统
在实现 watch 之前,我们需要先构建一个简单的响应式系统基础。这个基础将包括 reactive、effect 等核心功能。
2.1 实现 reactive 函数
javascript复制const reactiveMap = new WeakMap()
function reactive(target) {
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
reactiveMap.set(target, proxy)
return proxy
}
这个简单的 reactive 实现使用了 Proxy 来拦截对象的 get 和 set 操作。当属性被访问时,我们会调用 track 函数记录依赖;当属性被修改时,我们会调用 trigger 函数触发更新。
2.2 实现 effect 和依赖收集
effect 是响应式系统的核心,它代表了一个副作用函数,当依赖的响应式数据变化时,这个函数会被重新执行。
javascript复制let activeEffect = null
const targetMap = new WeakMap()
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
这个实现中:
- track 函数用于在属性被访问时收集当前正在运行的 effect
- trigger 函数用于在属性变化时触发所有依赖该属性的 effect
- effect 函数用于创建一个副作用函数,并立即执行一次以收集依赖
3. 实现基础的 watch 功能
有了响应式系统的基础,我们现在可以实现一个简单的 watch 函数了。
3.1 基本 watch 实现
javascript复制function watch(source, callback) {
let getter
if (typeof source === 'function') {
getter = source
} else if ('value' in source) { // ref
getter = () => source.value
} else { // reactive object
getter = () => traverse(source)
}
let oldValue
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: () => {
const newValue = effectFn()
callback(newValue, oldValue)
oldValue = newValue
}
})
oldValue = effectFn()
}
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return value
}
seen.add(value)
for (const key in value) {
traverse(value[key], seen)
}
return value
}
这个 watch 实现支持三种数据源:
- getter 函数:返回一个值的函数
- ref 对象:包含 value 属性的响应式引用
- reactive 对象:通过 reactive() 创建的响应式对象
对于 reactive 对象,我们使用 traverse 函数递归地访问所有属性,以确保所有嵌套属性都被追踪。
3.2 处理旧值和新值
watch 的一个重要特性是能够访问变化前的旧值和变化后的新值。在我们的实现中,这是通过保存上一次执行 effectFn 的结果来实现的:
- 第一次执行 effectFn 时,我们保存结果作为初始的 oldValue
- 当数据变化触发 scheduler 时,我们再次执行 effectFn 获取 newValue
- 调用用户提供的 callback,传入 newValue 和 oldValue
- 将 oldValue 更新为当前的 newValue,为下一次变化做准备
4. 实现高级 watch 功能
现在我们已经有了一个基本的 watch 实现,接下来可以添加更多高级功能,如 immediate、deep 等选项。
4.1 实现 immediate 选项
immediate 选项允许 watch 在创建时立即执行一次回调函数。
javascript复制function watch(source, callback, options = {}) {
// ...之前的代码...
const job = () => {
const newValue = effectFn()
callback(newValue, oldValue)
oldValue = newValue
}
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
当 immediate 为 true 时,我们立即执行 job 函数,此时 oldValue 为 undefined(因为这是第一次执行)。
4.2 实现 deep 选项
deep 选项用于深度监听对象内部的变化。对于 reactive 对象,Vue 3 默认会深度监听,但对于 ref 或 getter 返回的对象,我们需要显式启用 deep。
javascript复制function watch(source, callback, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else if ('value' in source) {
getter = () => source.value
} else {
getter = () => source
}
if (options.deep) {
getter = () => traverse(getter())
}
// ...其余代码...
}
当 deep 为 true 时,我们会对 getter 返回的值进行深度遍历,确保所有嵌套属性都被追踪。
4.3 实现 flush 选项
flush 选项控制回调函数的执行时机,可以是 'pre'(默认,在组件更新前)、'post'(在组件更新后)或 'sync'(同步)。
javascript复制function watch(source, callback, options = {}) {
// ...之前的代码...
const job = () => {
const newValue = effectFn()
callback(newValue, oldValue)
oldValue = newValue
}
const scheduler = () => {
if (options.flush === 'sync') {
job()
} else if (options.flush === 'post') {
Promise.resolve().then(job)
} else { // 'pre'
// 这里简化处理,实际Vue中会与组件更新队列结合
job()
}
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler
})
// ...其余代码...
}
4.4 实现 onCleanup 功能
onCleanup 允许用户在回调中注册清理函数,这些函数会在下一次回调执行前或 watch 停止时被调用。
javascript复制function watch(source, callback, options = {}) {
// ...之前的代码...
let cleanup
let onInvalidate = (fn) => {
cleanup = fn
}
const job = () => {
if (cleanup) {
cleanup()
cleanup = null
}
const newValue = effectFn()
callback(newValue, oldValue, onInvalidate)
oldValue = newValue
}
// ...其余代码...
return () => {
if (cleanup) {
cleanup()
}
// 停止 effect 的逻辑
}
}
5. 性能优化与边界情况处理
在实际应用中,我们需要考虑各种边界情况和性能优化。
5.1 避免重复触发
当 watch 的回调函数中修改了监听的数据时,可能会导致无限循环。我们需要添加机制来避免这种情况。
javascript复制function watch(source, callback, options = {}) {
// ...之前的代码...
let isRunning = false
const job = () => {
if (isRunning) return
isRunning = true
if (cleanup) {
cleanup()
cleanup = null
}
const newValue = effectFn()
callback(newValue, oldValue, onInvalidate)
oldValue = newValue
isRunning = false
}
// ...其余代码...
}
5.2 停止监听
watch 应该返回一个函数,调用这个函数可以停止监听。
javascript复制function watch(source, callback, options = {}) {
// ...之前的代码...
const effectFn = effect(() => getter(), {
lazy: true,
scheduler
})
const stop = () => {
// 清除 effect 的逻辑
if (cleanup) {
cleanup()
}
}
return stop
}
5.3 内存管理
我们需要确保当 watch 停止时,所有相关的依赖都被正确清理,避免内存泄漏。
javascript复制function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
const res = fn()
activeEffect = null
return res
}
effectFn.deps = []
effectFn.options = options
if (!options.lazy) {
effectFn()
}
return effectFn
}
function cleanup(effectFn) {
for (const dep of effectFn.deps) {
dep.delete(effectFn)
}
effectFn.deps.length = 0
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
6. 完整实现与测试
现在我们可以将所有部分组合起来,形成一个完整的 watch 实现。
6.1 完整代码
javascript复制const reactiveMap = new WeakMap()
const targetMap = new WeakMap()
let activeEffect = null
function reactive(target) {
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
reactiveMap.set(target, proxy)
return proxy
}
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
const effects = new Set(dep)
effects.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
})
}
}
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
const res = fn()
activeEffect = null
return res
}
effectFn.deps = []
effectFn.options = options
if (!options.lazy) {
effectFn()
}
return effectFn
}
function cleanup(effectFn) {
for (const dep of effectFn.deps) {
dep.delete(effectFn)
}
effectFn.deps.length = 0
}
function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return value
}
seen.add(value)
for (const key in value) {
traverse(value[key], seen)
}
return value
}
function watch(source, callback, options = {}) {
let getter
if (typeof source === 'function') {
getter = source
} else if ('value' in source) {
getter = () => source.value
} else {
getter = () => source
}
if (options.deep) {
getter = () => traverse(getter())
}
let oldValue, newValue
let cleanup
let onInvalidate = (fn) => {
cleanup = fn
}
let isRunning = false
const job = () => {
if (isRunning) return
isRunning = true
newValue = effectFn()
if (cleanup) {
cleanup()
cleanup = null
}
callback(newValue, oldValue, onInvalidate)
oldValue = newValue
isRunning = false
}
const scheduler = () => {
if (options.flush === 'sync') {
job()
} else if (options.flush === 'post') {
Promise.resolve().then(job)
} else { // 'pre'
job()
}
}
const effectFn = effect(() => getter(), {
lazy: true,
scheduler
})
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
return () => {
if (cleanup) {
cleanup()
}
// 实际Vue中会有更复杂的停止逻辑
effectFn.options.stop && effectFn.options.stop()
}
}
6.2 测试示例
javascript复制// 测试 reactive
const state = reactive({ count: 0, nested: { value: 1 } })
// 测试 watch
watch(
() => state.count,
(newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
}
)
// 测试 immediate
watch(
() => state.nested.value,
(newVal, oldVal) => {
console.log(`nested.value changed from ${oldVal} to ${newVal}`)
},
{ immediate: true }
)
// 测试 deep
const objRef = ref({ a: 1 })
watch(
objRef,
(newVal, oldVal) => {
console.log('objRef changed', oldVal, newVal)
},
{ deep: true }
)
// 测试 onCleanup
watch(
() => state.count,
(newVal, oldVal, onInvalidate) => {
const timer = setTimeout(() => {
console.log('Delayed log:', newVal)
}, 1000)
onInvalidate(() => {
clearTimeout(timer)
console.log('Cleanup for', newVal)
})
}
)
// 触发变化
state.count++
state.nested.value++
objRef.value.a++
state.count++
这个实现涵盖了 Vue 3.4 中 watch 的核心功能,包括:
- 响应式数据监听
- 新旧值获取
- immediate 选项
- deep 选项
- flush 选项
- 清理函数
- 停止监听
虽然简化了一些边界情况处理,但核心逻辑与 Vue 3 的实现思路是一致的。理解这个实现有助于我们更好地使用 Vue 的响应式系统,并在需要时进行定制和优化。
