4.9 watch 的实现原理
watch的本质是观测一个响应式数据,变化时执行回调,实现方式就是复用 effect 的scheduler。
一、核心思路
- 副作用函数与响应式数据建立联系后,数据变化会触发副作用函数重新执行;
- 若副作用函数带有
scheduler选项,数据变化时触发 scheduler 而不是副作用函数本身; - 于是把
scheduler当作回调函数,watch就实现出来了。
二、第一版:硬编码读取一个属性
function watch(source, cb) {
effect(
// 触发读取操作,从而建立联系
() => source.foo,
{
scheduler() {
cb() // 数据变化时执行回调
}
}
)
}
watch(obj, () => console.log('数据变了'))
obj.foo++ // → 打印「数据变了」
缺陷:只能观测
source.foo这一个字段,换个属性就失效。
三、第二版:traverse 递归读取
function traverse(value, seen = new Set()) {
// 原始值或已读取过则直接返回(后者用于避免循环引用死循环)
if (typeof value !== 'object' || value === null || seen.has(value)) return
seen.add(value)
// 假设 value 是对象:逐个读取属性并递归遍历
for (const k in value) {
traverse(value[k], seen)
}
return value
}
function watch(source, cb) {
effect(
() => traverse(source), // 递归读取,任意属性变化都能触发
{ scheduler: () => cb() }
)
}
seen集合必不可少:对象存在循环引用时,没有它就会无限递归。
四、第三版:source 支持 getter 函数
function watch(source, cb) {
let getter
if (typeof source === 'function') {
getter = source // 用户传 getter:依赖由用户指定
} else {
getter = () => traverse(source) // 传对象:递归读取全部属性
}
effect(() => getter(), { scheduler: () => cb() })
}
watch(
() => obj.foo, // 只关心 obj.foo
() => console.log('数据变了')
)
五、第四版:回调中拿到新值与旧值
function watch(source, cb) {
let getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
let oldValue, newValue
const effectFn = effect(
() => getter(),
{
lazy: true, // 不立即执行,改为手动调用以拿旧值
scheduler() {
newValue = effectFn() // 重新执行副作用函数得到新值
cb(newValue, oldValue) // 新旧值作为回调参数
oldValue = newValue // 更新旧值,否则下次拿到的旧值是错的
}
}
)
// 手动执行一次,返回值就是旧值
oldValue = effectFn()
}
| 步骤 | 说明 |
|---|---|
lazy: true | 副作用函数不自动执行,交给 watch 手动调用 |
oldValue = effectFn() | 首次执行结果即初始值 |
newValue = effectFn() | 变更后重新执行得到新值 |
oldValue = newValue | 每轮结束必须更新旧值 |
忘记
oldValue = newValue是典型 bug:第二次及之后回调拿到的oldValue会一直停在最初始的值。
相关
- 4.8 计算属性 computed 与 lazy:
lazy与scheduler的组合 - 4.10 立即执行的 watch 与回调执行时机:
immediate与flush