今天来看一下 watch 的实现方式
watch 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。本质上跟 computed 一样,都是利用了effect去做一些事情。
基本实现
在 package/reactivity/src 新建 watch.ts 文件
export function watch(source, callback, options) {
let getter;
// 根据值的类型, 封装成getter函数
if (isReactive(source)) {
getter = () => source;
} else if (isFunction(source)) {
getter = source;
} else if (isRef(source)) {
getter = () => source.value
} else {
// ...其他情况的
}
let oldValue
const job = () => {
callback()
}
// 创建effect的时候, 会调用getter函数收集依赖, 到时候变动触发job函数
let effect = new ReactiveEffect(getter, job);
oldValue = effect.run() // 第一次运行返回的值保存起来
return effect.stop.bind(effect) // 可以通过调用stop函数停止watch监听
}
isReactive 和 isRef 在 reactive 文件中导出
export function isReactive(target) {
return target && !!target[reactiveFlags.IS_REACTIVE];
}
export function isRef(target) {
return target && !!target[reactiveFlags.IS_REF];
}
baseHandles 加入新枚举
export const enum reactiveFlags {
IS_REACTIVE = "__v_isReactive",
IS_REF = "__v_isRef",
}
traverse
如果传入的是一个proxy对象, 那么需要遍历该值的所有属性, 触发get的收集函数, 才能在变动的时候触发更新, 这里也可以看出来如果传入的是proxy对象, 默认是要做递归处理的, 所以尽量监听单个属性比较好
function traverse(value, seen = new Set()) {
if (!isObject(value)) {
return value;
}
// 为了防止重复引用导致死循环
if (seen.has(value)) {
return value;
}
seen.add(value);
// 递归访问所有属性进行一个依赖搜集
for (const key in value) {
traverse(value[key], seen);
}
return value;
}
// 这一步也要修改一下
if (isReactive(source)) {
getter = () => traverse(source);
}
新旧值的传递
const job = () => {
let newValue = effect.run(); // 重新运行一次得到新值
callback(newValue, oldValue);
oldValue = newValue; // 旧值改为新值
};
onCleanup
这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用。
let cleanup;
function onCleanup(fn) {
cleanup = fn; // 相当于第一次调用只是赋值
}
let oldValue;
const job = () => {
let newValue = effect.run();
if (cleanup) cleanup(); // 第一次还没有赋值完成, 所以无法调用, 等到第二次进入有值了才进行调用
callback(newValue, oldValue, onCleanup);
oldValue = newValue;
};