watch
watch 用于在响应式数据发生变化时,调用你提供的回调函数。它属于惰性侦听器:默认情况下,仅在侦听的数据实际改变时才会执行回调,而不是立即执行。
语法
watch(source, callback, options?)
source:侦听源,可以是:- 一个 ref(包括
computed返回的 ref) - 一个 reactive 对象(会隐式开启深度侦听)
- 一个 getter 函数(
() => x) - 由上述类型组成的数组(同时侦听多个源)
- 其他情况会发出警告。
- 一个 ref(包括
callback:数据变化时的回调,接收(newValue, oldValue, onCleanup)。如果不是回调函数,会发出警告,提示使用 watchEffect。options:可选配置对象immediate,设为 true 时,侦听器会立即执行一次回调deep,默认值为 false,但当侦听源为 reactive 对象时默认为 true。设为 true 会递归遍历对象的所有嵌套属性,任何内部变化都会触发回调。flush,控制回调的调用时机。once,设为 true 时回调仅执行一次后自动停止侦听。
示例 监听 ref 基本类型
const count = ref(0);
const handleClick = () => {
count.value++;
};
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log("info.count 值", newVal, oldVal);
});
watch(count,()=> {}),count 是一个ref 对象,会创建一个 getter 函数,返回 count.value.
ReactiveEffect 创建了一个响应式副作用实例,用于追踪依赖变化并触发回调。
当执行 effect.run() ,实际上是执行 getter = () => count.value,即读取 count.value,开始收集依赖。
侦听 reactive 对象
- 直接监听 reactive 对象会隐式开启深度监听。
- 新旧值引用相同的问题。
- 监听 reactive 对象的某个属性必须使用 getter。
const reactiveObj = reactive({
age: 20,
address: "shenzhen",
});
watch(reactiveObj, (newVal, oldVal) => {
console.log("watch reactiveObj", newVal, oldVal);
});
执行 watch
执行 doWatch
执行 augmentJob
示例 监听 getter 函数
const count = ref(0);
watch(
() => count.value,
(newVal, oldVal) => {
console.log("watch count", newVal, oldVal);
}
);
示例 侦听多个源
使用数组同时侦听多个源,回调会收到新值和旧值的数组。
const count = ref(0)
const name = ref('Vue')
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`${oldCount}->${newCount}, ${oldName}->${newName}`)
})
示例 批量更新
const count = ref(0);
const handleClick = () => {
// 批量更新,触发 watch 一次
count.value++;
count.value++;
count.value++;
};
watch(count, (newVal, oldVal) => {
console.log("watch count 值", newVal, oldVal);
});
示例 异步更新
const count = ref(0);
const handleClick = () => {
count.value++; // 触发一次
// 异步更新
setTimeout(() => {
count.value++; // 触发一次
}, 1000);
};
watch(count, (newVal, oldVal) => {
console.log("watch count 值", newVal, oldVal);
});
示例 watch options 配置项
一、immediate 立即执行
watch(count, (newVal, oldVal) => {
// 立即执行一次,newVal 是当前值,oldVal 为 undefined
}, { immediate: true })
二、deep 深层遍历
设为 true 会递归遍历对象的所有嵌套属性,任何内部变化都会触发回调。
const state = reactive({ count: 0, nested: { a: 1 } })
watch(() => state, (newVal, oldVal) => {
// 任何深层变化都会触发
}, { deep: true })
三、once 只执行一次
Vue 3.4+ 支持,设为 true 时回调仅执行一次后自动停止侦听。
watch(count, callback, { once: true })
fluhs 实现机制
1、post
将 job 推入 queuePostFlushCb 队列。该队列会在组件更新完成后的微任务阶段执行,确保可以访问到最新渲染的 DOM。
2、默认 pre
将 job 推入 queueJob 队列。这是一个 异步微任务队列,并且会去重。多个 pre 类型的 watch 在同一轮事件循环中只会入队一次。这些 job 会在组件更新前被批量执行。
如果是首次执行,则是在 组件渲染前执行回调。
3、sync
将 scheduler 直接设置为需要执行的 job,不经过任何异步队列。
watchEffect
watchEffect 配置项只支持flush, 配置 deep、immediate、once 会发出警告,提示使用 wtach(source,callback,options) 签名语法。
watchEffect 在初始化时会立即执行一次,并在这个过程中完成依赖收集。
- 执行时机:默认在组件更新前执行(即
flush: 'pre')。 - 首次执行:创建后立即执行一次,自动收集依赖。
- 后续触发:依赖变化后,副作用会在组件更新前(同一微任务中)执行。
- 典型场景:大多数不需要访问更新后 DOM 的场景,且希望副作用在渲染前同步某些状态。
watchPostEffect
等价于 watchEffect(fn, { flush: 'post' })
- 执行时机:组件更新后执行(
flush: 'post')。 - 首次执行:创建后立即执行一次,自动收集依赖。
- 后续触发:依赖变化后,副作用会等待组件更新完成后(DOM 已更新)再执行。
- 典型场景:需要操作更新后的 DOM(如获取元素尺寸、滚动位置、焦点管理)。
watchSyncEffect
等价于 watchEffect(fn, { flush: 'sync' })
- 执行时机:依赖变化后立即同步执行(
flush: 'sync')。 - 首次执行:创建后立即执行一次,自动收集依赖。
- 后续触发:每当依赖变化,副作用同步执行,不经过任何异步队列(微任务或宏任务)。这可能会导致性能问题,因为无法批量处理多次变化。
- 典型场景:极少使用,通常用于调试或需要立即同步到外部系统的逻辑
示例 watcheffect 的 flush 配置
执行顺序
watchSyncEffect: 如果在侦听器中配置了flush: 'sync',它的回调会同步立即执行。watchEffect(flush: 'pre') : 默认的侦听器,会在组件更新前执行回调。onBeforeUpdate: 组件DOM更新前的生命周期钩子被调用。watchPostEffect(flush: 'post') : 执行所有后置侦听器的回调。onUpdated: 组件DOM更新后的生命周期钩子被调用。
<template>
<div>
<p id="cdom">count 值 {{ count }}</p>
<button @click="handleClick">点击</button>
</div>
</template>
<script lang="ts" setup>
import { ref, watchEffect, watchPostEffect, watchSyncEffect, onUpdated } from "vue";
const count = ref(0);
const handleClick = () => {
count.value++;
};
// 默认 pre
watchEffect(() => {
console.log("Pre watchEffect count", count.value);
const dom = document.getElementById("cdom") as HTMLElement;
console.log("pre-dom", dom, dom?.textContent);
});
watchPostEffect(() => {
console.log("watchPostEffect count", count.value);
const dom = document.getElementById("cdom") as HTMLElement;
console.log("post-dom", dom, dom?.textContent);
});
watchSyncEffect(() => {
console.log("watchSyncEffect count", count.value);
const dom = document.getElementById("cdom") as HTMLElement;
console.log("syncdom", dom, dom?.textContent);
});
onUpdated(() => {
console.log("onUpdated count", count.value);
const dom = document.getElementById("cdom") as HTMLElement;
console.log("update-dom", dom, dom?.textContent);
});
</script>
初始化
更新 count Ref
DOM 元素对象的引用是固定的,但它的 属性(例如 textContent)是可以被修改的。Vue 在组件更新时,会复用同一个 DOM 元素,只是更新其内容。
源码 effect
function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions,
): ReactiveEffectRunner<T> {
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
const e = new ReactiveEffect(fn)
if (options) {
extend(e, options)
}
try {
e.run() // 首次、立即执行一次
} catch (err) {
// 如果副作用函数执行过程中抛出异常,停止该副作用函数的依赖追踪
e.stop()
throw err
}
const runner = e.run.bind(e) as ReactiveEffectRunner
runner.effect = e
return runner
}
watch 源码
computed
在 Vue 3 的组合式 API 中,computed 是一个用于创建派生状态的核心函数。它允许你定义一个依赖于其他响应式数据(ref、reactive 等)的值,并且会自动追踪依赖、缓存计算结果,仅在依赖变化时才重新求值。
注意事项
- 计算属性必须是纯函数(无副作用)。Vue 的 ESLint 插件(
eslint-plugin-vue)提供了规则vue/no-side-effects-in-computed-properties,禁止在计算属性的 getter 中修改外部状态、执行异步请求或操作 DOM。 - 不要直接修改计算属性的值(只读场景)。
- 计算属性会缓存结果,但依赖必须是响应式的。
示例 只读计算属性
传入一个 getter 函数,返回一个只读的 ref 对象。
<script setup lang="ts">
import { computed, ref } from "vue";
const color = ref(0);
const sub = computed(() => {
return color.value;
});
console.log(sub.value);
</script>
初始化 ComputedRefImpl 实例时的 dep 属性。dep 管理计算属性自身的依赖,类型为 Dep
computed计算属性返回
示例 可写计算属性
传入一个带有 get 和 set 方法的对象,返回一个可读可写的 ref。
const firstName = ref<string>("");
const lastName = ref<string>("");
const fullName = computed({
get() {
return firstName.value + " " + lastName.value;
},
set(newValue: string) {
const [first = "", last = ""] = newValue.split(" ");
firstName.value = first;
lastName.value = last;
},
});
fullName.value = "Li Hi"; //会触发 setter,更新 firstName 和 lastName
源码
function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T> | undefined
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 创建计算属性实例
const cRef = new ComputedRefImpl(getter, setter, isSSR)
// 如果是开发环境,且提供了调试选项,将调试选项赋值给计算属性实例的 onTrack 和 onTrigger 属性
if (__DEV__ && debugOptions && !isSSR) {
cRef.onTrack = debugOptions.onTrack
cRef.onTrigger = debugOptions.onTrigger
}
// 返回计算属性实例
return cRef as any
}
class ComputedRefImpl<T = any> implements Subscriber {
/**
* @internal
* 存储计算结果,初始值为 undefined
*/
_value: any = undefined
/**
* @internal
* 管理计算属性自身的依赖,类型为 Dep
*/
readonly dep: Dep = new Dep(this)
/**
* @internal
* 标记为响应式引用
*/
readonly __v_isRef = true
// TODO isolatedDeclarations ReactiveFlags.IS_REF
/**
* @internal
*/
readonly __v_isReadonly: boolean
// TODO isolatedDeclarations ReactiveFlags.IS_READONLY
// A computed is also a subscriber that tracks other deps
/**
* @internal
* 计算属性所依赖的其他响应式数据 链表头
*/
deps?: Link = undefined
/**
* @internal
* 计算属性所依赖的其他响应式数据 链表尾
*/
depsTail?: Link = undefined
/**
* @internal
* 状态标志,初始为 EffectFlags.DIRTY,表示需要重新计算
*/
flags: EffectFlags = EffectFlags.DIRTY
/**
* @internal
* 全局版本号,初始为 globalVersion - 1,用于优化计算
*/
globalVersion: number = globalVersion - 1
/**
* @internal
*/
isSSR: boolean
/**
* @internal
* 指向下一个订阅者,用于批处理
*/
next?: Subscriber = undefined
// for backwards compat
// 为了向后兼容,指向自身
effect: this = this
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
/**
* Dev only
* @internal
*/
_warnRecursive?: boolean
constructor(
// 作为实例属性
public fn: ComputedGetter<T>,
// 作为实例属性
private readonly setter: ComputedSetter<T> | undefined,
isSSR: boolean,
) {
this[ReactiveFlags.IS_READONLY] = !setter
this.isSSR = isSSR
}
/**
* @internal
*/
notify(): true | void {
this.flags |= EffectFlags.DIRTY
if (
!(this.flags & EffectFlags.NOTIFIED) &&
// avoid infinite self recursion
activeSub !== this
) {
// 将计算属性加入计算批处理队列
batch(this, true)
// 返回 true,表示这是一个计算属性,需要通知其依赖
return true
} else if (__DEV__) {
// TODO warn
}
}
// 执行计算属性的 getter 函数
get value(): T {
// 记录依赖追踪信息
const link = __DEV__
? this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
: this.dep.track()
// 刷新计算值
refreshComputed(this)
// sync version after evaluation
if (link) {
// 同步版本号
link.version = this.dep.version
}
return this._value
}
// 执行计算属性的 setter 函数
set value(newValue) {
if (this.setter) {
this.setter(newValue)
} else if (__DEV__) {
warn('Write operation failed: computed value is readonly')
}
}
}