Vue.js设计与实现(第四章)— 响应式之执行调度

324 阅读4分钟

大家好我是小瑜,今天学习的是在Vue响应式中给effect函数添加 scheduler 也就是调度器,这样做的好处是可以给effect添加配置的方式,实现响应式的自定义设置执行顺序或者执行次数,可以使effect更加灵活。

要实现的目的是:

当trigger动作触发副作用函数重新执行时, 有能力决定副作用函数执行的时机,次数以及方式。

决定副作用函数的执行的时机

自定义修改执行顺序

例如下面这段代码,输出的结果是 1 2 结束了 要求将输出结果的顺序在不跳转现有代码的基础上进行输出的执行

const data = {foo:1}
const obj = new Proxy(data,{xxx})

effect(()=>{
 console.log(obj.foo)
})

obj.foo ++
console.log('结束了')

// 输出:1 2  结束了

此时就需要在触发 effect 的时候添加一个 scheduler, 交给用户来控制执行顺序 可以个 effect 添加一项配置,提供给用户来手动操作

effect(
  () => {
    console.log("foo:", obj.foo);
  },
  {
    // 调度器 scheduler 是一个函数
    scheduler(fn) {
      // 将副作用函数放到宏任务队列中执行
      setTimeout(fn);
    },
  }
);
obj.foo++;
console.log("结束了");

在 effect中进行挂载保存 在trigger 中就需要将 scheduler 保存并执行

  • 挂载
function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn();
    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };
  effectFn.deps = [];
  //  将 options 挂载到 effectFn 上
  effectFn.options = options; // 新增
  effectFn();
}
  • 执行
function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  const effectsToRun = new Set(effects);
  effectsToRun.forEach((effectFn) => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn);
    } else {
      effectFn();
    }
  });
}

这里首先是将 下面这段配置传给 trigger, trigger中找到 scheduler 这个函数, 并判断是否存在,若存在就将此次effect作为 scheduler 的参数进行传递并执行, 如果不存在则执行 effect

{
    // 调度器 scheduler 是一个函数
    scheduler(fn) {
      // 将副作用函数放到宏任务队列中执行
      setTimeout(fn);
    },
}

控制执行次数

例如下面这段代码的执行结果分别是:1 2 3 '结束了', 但是我想要关注的是执行最终的结果,并不关心执行过程, 我期望的输出结果为1 '结束了' 3。

effect(() => {
  console.log(obj.foo);
});
obj.foo++;
obj.foo++;
console.log('结束了')

这里同样也可以借助 scheduler 来实现逻辑 首选是需要利用 Set 去重的机制 保存 非重复的 effect 这里就代表 **console.log(obj.foo) **有且只能有一个 并且来设置一个开关,来给Set添加effect 那么以上这段逻辑就都需要放在 scheduler 也就是 effect的第二项配置中自定义实现

/**
 * 新增 scheduler 调度器
 * 通过set 将任务添加到调度器任务队列中自动去重
 */
// 调度器任务队列
export const jobQueue = new Set();
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve();
// 一个标志代表是否正在刷新队列
let isFlushing = false;

function flushJob() {
  // 如果正在刷新则不执行
  if (isFlushing) return;
  isFlushing = true;
  p.then(() => {
    jobQueue.forEach((job) => job());
  }).finally(() => {
    isFlushing = false;
  });
}
effect(
  () => {
    console.log(obj.foo);
  },
  {
    scheduler(job) {
      // 添加effect到微任务队列,并去重
      jobQueue.add(job);
      // 将微任务队列中的effect执行
      flushJob();
    },
  }
);
obj.foo++;
obj.foo++;
console.log("结束了");

// 输出: 1 '结束了'

完整代码

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <style></style>
  </head>
  <body>
    <script type="module">
      import { effect, obj, jobQueue, flushJob } from "./01-index.js";


      // 1.修改执行顺序
      effect(
        () => {
          console.log("foo:", obj.foo);
        },
        {
          // 调度器 scheduler 是一个函数
          scheduler(fn) {
            // 将副作用函数放到宏任务队列中执行
            setTimeout(fn);
          },
        }
      );
      obj.foo++;
      console.log("结束了");

     // 2、控制执行顺序即执行顺序
      effect(
        () => {
          console.log(obj.foo);
        },
        {
          scheduler(job) {
            jobQueue.add(job);
            flushJob();
          },
        }
      );
      obj.foo++;
      obj.foo++;
      console.log("结束了");
      // 此时的输出结果是 1 2 3 但是要求并不关心执行过程 只需要执行结果 1 3 '结束了'
    </script>
  </body>
</html>

const data = { foo: 1 };
let activeEffect;
const effectStack = [];

const bucket = new WeakMap();

export function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn);
    activeEffect = effectFn;
    effectStack.push(effectFn);
    fn();

    effectStack.pop();
    activeEffect = effectStack[effectStack.length - 1];
  };
  effectFn.deps = [];
  //  将 options 挂载到 effectFn 上
  effectFn.options = options; // 新增
  effectFn();
}

export const obj = new Proxy(data, {
  get(target, key) {
    track(target, key);
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    trigger(target, key);
    return true;
  },
});

function track(target, key) {
  if (!activeEffect) return target[key];
  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(activeEffect);
  activeEffect.deps.push(deps);
}

function trigger(target, key) {
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  const effectsToRun = new Set(effects);
  effectsToRun.forEach((effectFn) => {
    // 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
    if (effectFn.options.scheduler) {
      console.log(effectFn.options);
      effectFn.options.scheduler(effectFn);
    } else {
      effectFn();
    }
  });
}

function cleanup(fn) {
  fn && fn.deps.forEach((dep) => dep.delete(fn));
  fn.deps.length = 0;
}

/**
 * 新增 scheduler 调度器
 * 通过set 将任务添加到调度器任务队列中自动去重
 */
// 调度器任务队列
export const jobQueue = new Set();
const p = Promise.resolve();
let isFlushing = false;

export function flushJob() {
  if (isFlushing) return;
  isFlushing = true;
  p.then(() => {
    jobQueue.forEach((job) => job());
  }).finally(() => {
    isFlushing = false;
  });
}