Vue3 - watch

81 阅读2分钟

本文为《Vue.js的设计与实现》的笔记。

1. watch

watch,本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。

例子:

watch(obj, () => {
    console.log('change');
});
obj.foo++;

watch的实质就是利用了effectoptions.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();
  }
}