本文为《Vue.js的设计与实现》的笔记。
1. watch
watch,本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。
例子:
watch(obj, () => {
console.log('change');
});
obj.foo++;
watch的实质就是利用了effect和options.scheduler选项:
effect(() => console.log(obj.foo), {
scheduler(){ ... }
})
上面的代码中,副作用函数访问响应式数据obj.foo,当响应式数据变化时,会触发scheduler调度函数执行。
实现一个简单的watch:
function watch(source, cb) {
effect(() => source.foo, {
scheduler() {
cb();
},
});
}
上面的代码写死了访问source的foo字段,很多时候source是一个对象,由之前的响应式系统的设计我们可以知道,副作用函数的收集粒度是到对象的某个属性,且在副作用函数中访问(调用getter)才会被收集到,所以我们需要对source对象的所有属性进行遍历访问。
先写一个遍历访问对象属性的函数:
function traverse(value, seen = new Set()) {
// value为原始值 / null / 已处理 的情况,均不处理
if (typeof value !== "object" || value === null || seen.has(value)) return;
// 将当前obj加入已处理的集合,避免死循环
seen.add(value);
// 使用for...in...遍历键
for (const k in value) {
// 递归调用
traverse(value[k], seen);
}
return value;
}
修改watch:
function watch(source, cb) {
effect(() => traverse(source), {
scheduler() {
cb();
},
});
}
watch的对象也可以是一个getter:
watch(() => obj.a, () => {
console.log("a changed");
});
修改watch:
function watch(source, cb) {
let getter;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}
effect(() => getter(), {
scheduler() {
cb();
},
});
}
2. watch callback的新值旧值
watch的实际使用场景中,我们能在回调函数中得到变化前后的值:
watch(
() => obj.a,
(newVal, oldVal) => {
console.log(`a changed from ${oldVal} to ${newVal}`);
}
);
如何实现? 使用lazy懒执行。
由于需要获取副作用函数返回的值,所以我们需要懒执行,手动执行effectFn来获取返回值。
function watch(source, cb) {
let getter;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}
// 旧值 新值
let oldValue, newValue;
// 获取effectFn
const effectFn = effect(() => getter(), {
lazy: true,
scheduler() {
// 先手动调用effectFn,获取新值
newValue = effectFn();
// 将新值旧值传给cd
cb(newValue, oldValue);
// 更新旧值
oldValue = newValue;
},
});
// 手动调用
oldValue = effectFn();
}
3. watch immediate
vue中watch可通过immediate指定回调立即执行,即在watch创建时立即执行一次回调函数。
watch(
() => obj.a,
(newVal, oldVal) => {
console.log(`a changed from ${oldVal} to ${newVal}`);
},
{ immediate: true }
);
分析上面的代码,可以看到代码的末尾直接执行了effectFn后就结束了,在此处通过判断options的immediate参数,执行调度器函数即可实现此功能。
function watch(source, cb, options = {}) {
let getter;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}
// 旧值 新值
let oldValue, newValue;
// 将调度器函数提取出来
const job = () => {
// 先手动调用effectFn,获取新值
newValue = effectFn();
// 将新值旧值传给cd
cb(newValue, oldValue);
// 更新旧值
oldValue = newValue;
};
// 获取effectFn
const effectFn = effect(() => getter(), {
lazy: true,
scheduler: job,
});
if (options.immediate) {
job();
} else {
// 手动调用
oldValue = effectFn();
}
}