《Vue.js设计与实现》 读书笔记

本文最后更新于:2022年4月22日 上午

第四章 响应系统的作用与实现

第四章 响应系统的作用与实现

响应式系统基本实现

通过 effect 把需要执行副作用的函数传入,当数据发生改变的时候将 这个传入的函数 重新执行

通过全局变量 activeFn 来记录当前正在执行的副作用函数,当发生属性获取的时候,将这个 activeFn 添加到 Set结构的 bucket 中,

当数据发生改变的时候,遍历 bucket 把之前添加过的 副作用函数都执行一遍

const bucket = new Set(); // 记录所有副作用
let activeFn; // 全局变量,标记当前活动的函数

// 将需要重新执行的函数传入
function effect(fn) {
  // 把当前的传入的赋值给 activeFn 
  activeFn = fn;
  fn();
}

// 原始数据
const data = { name: 'lfm', age: 18 };

// 代理成响应式数据
const newData = new Proxy(data, {
  get(target, key) {
    // 当获取对象属性的时候,把当前正在获取属性的方法 记录起来
    bucket.add(activeFn);
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    // 当对象的值发生更改的时候,把之前记录的副作用函数都执行一遍
    bucket.forEach((fn) => fn());
  },
});

// 测试
effect(() => {
  console.log('name:', newData.name);
});

setTimeout(() => {
  newData.name = 'lfm111';
}, 1000);

// 缺陷 会重新执行
setTimeout(() => {
  newData.age = 19;
}, 1000);

分支和cleanup

主要为了 fn1(){ console.log('name:', newData.ok ? ``[newData.name](http://newData.name)`` : 'not'); } 避免这种情况

newData.okfalse 的时候, newData.name如果发生了更新,那么 fn1 是不需要被重新执行的,但是之前的实现是没办法实现的

解决思路就是在 effect 中代理一个副作用函数,这个代理的副作用函数体中会真正执行副作用函数,然后这个代理函数有一个 deps 数组来记录这个副作用函数一共使用了哪些响应式变量(往这个代理函数的deps数组添加 当前响应式变量的依赖数组 的步骤在track中执行)

如上就会得到 copyFn1.deps = [name的deps,age的deps];

在触发更新的时候 会执行代理的副作用函数,执行时会先调用 cleanup,这一步会把将当前函数 和之前记录的依赖追踪断开联系,然后重新进行追踪依赖

这样就会把 name 的依赖给取消掉

const bucket = new WeakMap(); // 修改为 WeakMap,不会影响垃圾回收  记录所有副作用
let activeFn; // 全局变量,标记当前活动的函数

function effect(fn) {
  // 新建一个代理的副作用函数,在这个函数中再执行真正的副作用函数,其实可以把这个 effectFn 当成一个对象(class)看待,更好理解
  // 最终在触发更新的时候会执行这个函数
  const effectFn = () => {
    // 执行时先将之前已经将该函数加入在deps中清除,因为马上会再次调用一次,重新收集依赖
    cleanup(effectFn);

    // 把代理的副作用函数 赋值给 activeFn
    activeFn = effectFn;

    fn(); // 执行函数,重新收集依赖
  };

  // 初始化当前 副作用函数的依赖项
  effectFn.deps = [];

  // 执行代理的副作用函数
  effectFn();
}

/**
 * 清空当前副作用函数之前添加过的所有依赖
 * @param {Function} effectFn
 */
function cleanup(effectFn) {
  // 遍历所有将该函数加入到 deps 中的 Set,把当前副作用函数从这些deps进行删除
  // 然后将当前函数的 deps 置空,相当于给当前副作用函数做一次重置的操作
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];

    // 只把当前副作用函数 从这个 deps 进行删除,
    // 这步可以避免 不需要进行跟踪的响应式变量更新后,当前函数又被执行,如下这种情况,就会把 name的deps中的当前副作用函数进行删除
    // console.log('name:', newData.ok ? newData.name : 'not');
    deps.delete(effectFn);
  }

  // 将当前函数的 所有依赖进行清空
  // 如果不进行清空的话,在 track的时候都会在 effectFn.deps 进行一次添加
  // 因为这是一个数组的的结构,所以 effectFn.deps 的长度就会不断的增加,后续再执行 cleanup 的时候就会增加很多不必要的遍历
  effectFn.deps.length = 0;
}

const data = { name: 'lfm', age: 18, ok: true };

const newData = new Proxy(data, {
  get(target, key) {
    track(target, key);

    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
  },
});

/**
 * 收集依赖
 * @param {{}} target
 * @param {String} key
 * @returns
 */
function track(target, key) {
  if (!activeFn) return;

  // 把当前这个对象的所有属性依赖拿到
  let depsMap = bucket.get(target);

  // 如果没有则进行创建,第一次执行的时候需要先创建
  if (!depsMap) bucket.set(target, (depsMap = new Map()));

  // 把当前这个 key 的所有副作用函数列表取出来, 如 :fn1 中使用了 obj.name, fn2 中也使用了 obj.name
  // 那么这个deps的数据结构就是 [fn1, fn2];
  let deps = depsMap.get(key);

  // 第一次的情况同上,会存在没有的情况,需要先进行赋值
  if (!deps) depsMap.set(key, (deps = new Set()));

  /**
   * 将当前副作用函数添加到 当前访问的 key 所对应的 副作用列表中
   * 这个 key 所对应的 副作用列表 会有多个 副作用函数,当前函数只是其中一个 (在触发更新的时候,会将当前函数取出来进行删除,防止已经不需要跟踪的响应式变量更新后依旧调用了当前副作用函数)
   * 因为 这个副作用列表是一个Set结构,所以不用担心会添加多次的情况,如下例子,只会把fn1添加进去一次
   * fn1(){
   *  console.log(obj.name);
   *  console.log(obj.age);
   * }
   */
  deps.add(activeFn);

  // 这一步很关键,在当前正在执行的副作用函数的 deps 中记录它具体访问了哪几个响应式变量的 deps
  activeFn.deps.push(deps);
}

/**
 * 触发更新
 * @param {{}} target
 * @param {String} key
 * @returns
 */
function trigger(target, key) {
  // 把当前这个对象的所有属性依赖拿到
  let depsMap = bucket.get(target);
  if (!depsMap) return;

  // 把当前 key 的所有 副作用函数列表都取到,一会要进行调用,告诉这些函数,这个key更新了
  let effects = depsMap.get(key);

  // 拷贝一份 副作用函数列表
  const effectToRun = new Set(effects);

  // 将当前副作用函数遍历进行执行
  effectToRun && effectToRun.forEach((fn) => fn());
}

effect(() => {
  console.log('name:', newData.ok ? newData.name : 'not');
  // 第一次运行将这个匿名函数分别添加到 ok 和 name 两个属性的 依赖中,

  /* 
    {
      name: [当前函数],
      age: [当前函数]
    }, 
    反过来 
    当前函数.deps = [name的deps,age的deps];
  */

  // 当把 ok 改成 false 的时候,会遍历 当前函数.deps,然后将其里面的所有先进行删除,然后重新执行当前函数,再次收集依赖
  // 这一次收集,只会收集一次了,不会将 name 收集进去,所有无论name怎么修改都不会重新执行

  /* 
    {
      name: [当前函数],
    },
    反过来 
    当前函数.deps = [name的deps];
  */
});

setTimeout(() => {
  newData.ok = false;
}, 1000);

setTimeout(() => {
  newData.name = 'lfm 111';
}, 2000);

setTimeout(() => {
  newData.name = 'lfm 222';
}, 3000);

嵌套的effect 和effect栈

const bucket = new WeakMap(); // 记录所有副作用
let activeFn;
const effectStack = []; // effect 函数栈,用以解决嵌套effect

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn); // 当数据发生改变的时候,先将之前已经将该函数加入在deps中清除,因为马上会再次调用一次,重新收集依赖
    activeFn = effectFn;
    effectStack.push(effectFn);
    fn(); // 执行函数,重新收集依赖

    effectStack.pop();
    activeFn = effectStack[effectStack.length - 1];
  };
  effectFn.deps = [];
  effectFn();
}

function cleanup(effectFn) {
  // 遍历所有将该函数加入到 deps 中的Set,将其全部进行删除,然后将这个 deps 置空
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

const data = { name: 'lfm', age: 18, ok: true };

const newData = new Proxy(data, {
  get(target, key) {
    track(target, key);

    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
  },
});

function track(target, key) {
  if (!activeFn) return;

  let depsMap = bucket.get(target);

  if (!depsMap) bucket.set(target, (depsMap = new Map()));

  let deps = depsMap.get(key);

  if (!deps) depsMap.set(key, (deps = new Set()));

  deps.add(activeFn);

  activeFn.deps.push(deps);
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) return;
  let effects = depsMap.get(key);

  const effectToRun = new Set();

  effects &
    effects.forEach((fn) => {
      if (fn !== activeFn) effectToRun.add(fn);
    });

  effectToRun && effectToRun.forEach((fn) => fn());
}

effect(() => {
  console.log('effect1:');

  effect(() => {
    console.log('effect2: ', newData.age);
  });
  console.log('effect1:', newData.name);
});

effect(() => newData.age++);


setTimeout(() => {
  newData.name = 'lfm111'; // 希望执行 effect1,但是实际会执行effect2,因为 执行effect2 的时候 activeFn变成了 effect2,且不能回退回去
}, 1000);

调度执行

const bucket = new WeakMap(); // 记录所有副作用
let activeFn;
const effectStack = []; // effect 函数栈,用以解决嵌套effect

const jobQueue = new Set(); // 定义一个任务队列
const p = Promise.resolve(); // 创建一个微任务队列
let isFlushing = false; // 是否正在刷新
function flushJob() {
  if (isFlushing) return;
  isFlushing = true;

  p.then(() => {
    // 将 fn 添加到宏任务中执行
    jobQueue.forEach((fn) => fn());
  }).finally(() => (isFlushing = false));
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn); // 当数据发生改变的时候,先将之前已经将该函数加入在deps中清除,因为马上会再次调用一次,重新收集依赖
    activeFn = effectFn;
    effectStack.push(effectFn);
    fn(); // 执行函数,重新收集依赖

    effectStack.pop();
    activeFn = effectStack[effectStack.length - 1];
  };
  effectFn.options = options; // 记录 options
  effectFn.deps = [];
  effectFn();
}

function cleanup(effectFn) {
  // 遍历所有将该函数加入到 deps 中的Set,将其全部进行删除,然后将这个 deps 置空
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

const data = { name: 'lfm', age: 18, ok: true };

const newData = new Proxy(data, {
  get(target, key) {
    track(target, key);

    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
  },
});

function track(target, key) {
  if (!activeFn) return;

  let depsMap = bucket.get(target);

  if (!depsMap) bucket.set(target, (depsMap = new Map()));

  let deps = depsMap.get(key);

  if (!deps) depsMap.set(key, (deps = new Set()));

  deps.add(activeFn);

  activeFn.deps.push(deps);
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) return;
  let effects = depsMap.get(key);

  const effectToRun = new Set();

  effects &
    effects.forEach((fn) => {
      if (fn !== activeFn) effectToRun.add(fn);
    });

  // 调度执行
  effectToRun && effectToRun.forEach((fn) => (fn.options.scheduler ? fn.options.scheduler(fn) : fn()));
}

effect(
  () => {
    console.log('effect1:', newData.age);
  },
  {
    scheduler(fn) {
      jobQueue.add(fn);

      flushJob(); // 跳过过渡值, 多次对同一个值进行操作时,只会刷新一次
      // 将该任务放到宏任务中执行
      // setTimeout(fn);
    },
  }
);

newData.age++;
newData.age++;

console.log('结束了');

lazy和computed

const bucket = new WeakMap(); // 记录所有副作用
let activeFn;
const effectStack = []; // effect 函数栈,用以解决嵌套effect

const jobQueue = new Set(); // 定义一个任务队列
const p = Promise.resolve(); // 创建一个微任务队列
let isFlushing = false; // 是否正在刷新
function flushJob() {
  if (isFlushing) return;
  isFlushing = true;

  p.then(() => {
    // 将 fn 添加到宏任务中执行
    jobQueue.forEach((fn) => fn());
  }).finally(() => (isFlushing = false));
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn); // 当数据发生改变的时候,先将之前已经将该函数加入在deps中清除,因为马上会再次调用一次,重新收集依赖
    activeFn = effectFn;
    effectStack.push(effectFn);
    const res = fn(); // 执行函数,重新收集依赖

    effectStack.pop();
    activeFn = effectStack[effectStack.length - 1];

    return res;
  };
  effectFn.options = options; // 记录 options
  effectFn.deps = [];

  // 如果是懒运行,则由用户手动执行
  if (options.lazy) return effectFn;

  effectFn();
}

function cleanup(effectFn) {
  // 遍历所有将该函数加入到 deps 中的Set,将其全部进行删除,然后将这个 deps 置空
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

const data = { name: 'lfm', age: 18, ok: true };

const newData = new Proxy(data, {
  get(target, key) {
    track(target, key);

    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
  },
});

function track(target, key) {
  if (!activeFn) return;

  let depsMap = bucket.get(target);

  if (!depsMap) bucket.set(target, (depsMap = new Map()));

  let deps = depsMap.get(key);

  if (!deps) depsMap.set(key, (deps = new Set()));

  deps.add(activeFn);

  activeFn.deps.push(deps);
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) return;
  let effects = depsMap.get(key);

  const effectToRun = new Set();

  effects &
    effects.forEach((fn) => {
      if (fn !== activeFn) effectToRun.add(fn);
    });

  // 调度执行
  effectToRun && effectToRun.forEach((fn) => (fn.options.scheduler ? fn.options.scheduler(fn) : fn()));
}

// 封装基本计算属性
function computed(getter) {
  let value; // 缓存数据
  let dirty = true; // true 表示 脏,需要重新计算

  const effectFn = effect(getter, {
    lazy: true,
    scheduler: () => {
      // 当 getter 中依赖的数据发生改变的时候,再将其设为脏数据,下次获取的时候会重新计算
      dirty = true;

      // 手动进行一次 通知
      trigger(obj, 'value');
    },
  });
  const obj = {
    get value() {
      // 如果 脏,则需要重新计算
      if (dirty) {
        value = effectFn();
        dirty = false;
      }

      // 手动进行一次跟踪
      track(obj, 'value');
      return value;
    },
  };

  // 当访问 .value 的时候才会进行计算
  return obj;
}

/* ***********************************************************88 */

// const res = effect(() => newData.age, {
//   lazy: true,
// });

const res2 = computed(() => newData.name + newData.age);

// console.log(res());
// console.log(res2.value);
// console.log(res2.value);

effect(() => {
  console.log(res2.value); // 当在 effect 中使用了computed 时,当值更新的时候希望该函数能够重新执行
});

newData.age++;

watch 基本实现

const bucket = new WeakMap(); // 记录所有副作用
let activeFn;
const effectStack = []; // effect 函数栈,用以解决嵌套effect

const jobQueue = new Set(); // 定义一个任务队列
const p = Promise.resolve(); // 创建一个微任务队列
let isFlushing = false; // 是否正在刷新
function flushJob() {
  if (isFlushing) return;
  isFlushing = true;

  p.then(() => {
    // 将 fn 添加到宏任务中执行
    jobQueue.forEach((fn) => fn());
  }).finally(() => (isFlushing = false));
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn); // 当数据发生改变的时候,先将之前已经将该函数加入在deps中清除,因为马上会再次调用一次,重新收集依赖
    activeFn = effectFn;
    effectStack.push(effectFn);
    const res = fn(); // 执行函数,重新收集依赖

    effectStack.pop();
    activeFn = effectStack[effectStack.length - 1];

    return res;
  };
  effectFn.options = options; // 记录 options
  effectFn.deps = [];

  // 如果是懒运行,则由用户手动执行
  if (options.lazy) return effectFn;

  effectFn();
}

function cleanup(effectFn) {
  // 遍历所有将该函数加入到 deps 中的Set,将其全部进行删除,然后将这个 deps 置空
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

const data = { name: 'lfm', age: 18, ok: true };

const newData = new Proxy(data, {
  get(target, key) {
    track(target, key);

    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
  },
});

function track(target, key) {
  if (!activeFn) return;

  let depsMap = bucket.get(target);

  if (!depsMap) bucket.set(target, (depsMap = new Map()));

  let deps = depsMap.get(key);

  if (!deps) depsMap.set(key, (deps = new Set()));

  deps.add(activeFn);

  activeFn.deps.push(deps);
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) return;
  let effects = depsMap.get(key);

  const effectToRun = new Set();

  effects &
    effects.forEach((fn) => {
      if (fn !== activeFn) effectToRun.add(fn);
    });

  // 调度执行
  effectToRun && effectToRun.forEach((fn) => (fn.options.scheduler ? fn.options.scheduler(fn) : fn()));
}

// 封装基本计算属性
function computed(getter) {
  let value; // 缓存数据
  let dirty = true; // true 表示 脏,需要重新计算

  const effectFn = effect(getter, {
    lazy: true,
    scheduler: () => {
      // 当 getter 中依赖的数据发生改变的时候,再将其设为脏数据,下次获取的时候会重新计算
      dirty = true;

      // 手动进行一次 通知
      trigger(obj, 'value');
    },
  });
  const obj = {
    get value() {
      // 如果 脏,则需要重新计算
      if (dirty) {
        value = effectFn();
        dirty = false;
      }

      // 手动进行一次跟踪
      track(obj, 'value');
      return value;
    },
  };

  // 当访问 .value 的时候才会进行计算
  return obj;
}

function watch(source, cb) {
  let getter, oldValue, newValue;
  // 兼容 回调函数的方式
  getter = typeof source === 'function' ? source : () => traverse(source);

  // 获取 oldValue 和 newValue
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
      newValue = effectFn();
      cb(newValue, oldValue);
      oldValue = newValue;
    },
  });
  
  // 手动调用一次
  oldValue = effectFn();
}

/**
 * 递归遍历
 * @param {any} value 需要遍历的值
 * @param {*} seen 记录以及遍历过的值
 */
function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return;

  // 暂时不考虑其它情况,只考虑对象
  for (const key in value) {
    traverse(value[key], seen);
  }
  return value;
}

/* ***********************************************************88 */

watch(newData, () => {
  console.log('数据发生改变');
});

watch(
  () => newData.age,
  (newValue, oldValue) => {
    console.log('age发生改变');
    console.log('newValue,oldValue:', newValue, oldValue);
  }
);

newData.age++;

watch immediate, flush, onInvalidate

const bucket = new WeakMap(); // 记录所有副作用
let activeFn;
const effectStack = []; // effect 函数栈,用以解决嵌套effect

const jobQueue = new Set(); // 定义一个任务队列
const p = Promise.resolve(); // 创建一个微任务队列
let isFlushing = false; // 是否正在刷新
function flushJob() {
  if (isFlushing) return;
  isFlushing = true;

  p.then(() => {
    // 将 fn 添加到宏任务中执行
    jobQueue.forEach((fn) => fn());
  }).finally(() => (isFlushing = false));
}

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn); // 当数据发生改变的时候,先将之前已经将该函数加入在deps中清除,因为马上会再次调用一次,重新收集依赖
    activeFn = effectFn;
    effectStack.push(effectFn);
    const res = fn(); // 执行函数,重新收集依赖

    effectStack.pop();
    activeFn = effectStack[effectStack.length - 1];

    return res;
  };
  effectFn.options = options; // 记录 options
  effectFn.deps = [];

  // 如果是懒运行,则由用户手动执行
  if (options.lazy) return effectFn;

  effectFn();
}

function cleanup(effectFn) {
  // 遍历所有将该函数加入到 deps 中的Set,将其全部进行删除,然后将这个 deps 置空
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

const data = { name: 'lfm', age: 18, ok: true };

const newData = new Proxy(data, {
  get(target, key) {
    track(target, key);

    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
  },
});

function track(target, key) {
  if (!activeFn) return;

  let depsMap = bucket.get(target);

  if (!depsMap) bucket.set(target, (depsMap = new Map()));

  let deps = depsMap.get(key);

  if (!deps) depsMap.set(key, (deps = new Set()));

  deps.add(activeFn);

  activeFn.deps.push(deps);
}

function trigger(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) return;
  let effects = depsMap.get(key);

  const effectToRun = new Set();

  effects &
    effects.forEach((fn) => {
      if (fn !== activeFn) effectToRun.add(fn);
    });

  // 调度执行
  effectToRun && effectToRun.forEach((fn) => (fn.options.scheduler ? fn.options.scheduler(fn) : fn()));
}

// 封装基本计算属性
function computed(getter) {
  let value; // 缓存数据
  let dirty = true; // true 表示 脏,需要重新计算

  const effectFn = effect(getter, {
    lazy: true,
    scheduler: () => {
      // 当 getter 中依赖的数据发生改变的时候,再将其设为脏数据,下次获取的时候会重新计算
      dirty = true;

      // 手动进行一次 通知
      trigger(obj, 'value');
    },
  });
  const obj = {
    get value() {
      // 如果 脏,则需要重新计算
      if (dirty) {
        value = effectFn();
        dirty = false;
      }

      // 手动进行一次跟踪
      track(obj, 'value');
      return value;
    },
  };

  // 当访问 .value 的时候才会进行计算
  return obj;
}

// 监听
function watch(source, cb, options = {}) {
  let getter, oldValue, newValue, cleanup;
  // 兼容 回调函数的方式
  getter = typeof source === 'function' ? source : () => traverse(source);

  const onInvalidate = (fn) => {
    cleanup = fn; // 记录传入的 onInvalidate
  };

  const job = () => {
    newValue = effectFn();
    if (cleanup) cleanup(); // 将上一次的进行清除
    cb(newValue, oldValue, onInvalidate);
    oldValue = newValue;
  };

  // 获取 oldValue 和 newValue
  const effectFn = effect(() => getter(), {
    lazy: true,
    scheduler: () => {
      if (options.flush === 'post') {
        // 组件更新后执行,放到微任务中执行

        Promise.resolve().then(job);
      } else {
        job();
      }
    },
  });

  // 是否需要立即执行一次
  if (options.immediate) {
    job();
  } else {
    // 手动调用一次
    oldValue = effectFn();
  }
}

/**
 * 递归遍历
 * @param {any} value 需要遍历的值
 * @param {*} seen 记录以及遍历过的值
 */
function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return;

  // 暂时不考虑其它情况,只考虑对象
  for (const key in value) {
    traverse(value[key], seen);
  }
  return value;
}

/* ***********************************************************88 */

watch(
  newData,

  // 每次数据变化时,都会执行一次 回调,默认 expired 是没有过期的,如果多次点击,则会调用上一次的 onInvalidate,将上一次设为过期,然后舍弃掉上一次服务端返回的数据
  (newValue, oldValue, onInvalidate) => {
    console.log('数据发生改变');
    let expired = false;
    onInvalidate(async () => {
      expired = true;
      const data = await fetch('http://example.com/movies.json').then((response) => response.json());

      // 当没有过期时才使用哈这个数据
      if (!expired) {
        finalData = data;
      }
    });
  },
  {
    immediate: true, // 是否立即执行一次
    flush: 'post', // pre: 组件更新前 post: 组件更新后 sync: 同步执行(默认)
  }
);

newData.age++;