【框架实现】深入scheduler调度系统实现机制

在学习了computed和watch的代码之后,我们会发现,computed和watch中都包含调度器scheduler的概念。完整的来说,它应该是调度系统;

调度系统包含两部分实现:

  1. lazy:懒执行;
  2. scheduler:调度器;

懒执行

参考vue3源码packages/reactivity/src/effect.ts中183-185行:

ts 复制代码
// 如果lazy是真,不会直接执行run函数;
// 当我们setter触发时,才会执行watch
if (!options || !options.lazy) {
    _effect.run()
}

在我们的项目vue-mini中的effect.ts修改:

ts 复制代码
export interface ReactiveEffectOptions {
  lazy?: boolean;
  scheduler?: EffectScheduler;
}

export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
  const _effect = new ReactiveEffect(fn);
  if (!options || !options.lazy) {
    _effect.run();
  }
}

修改完成之后,写一个测试实例;

js 复制代码
 <script>
    const { reactive, effect } = Vue;

    const obj = reactive({
      count: 1,
    });

    effect(
      () => {
        console.log(obj.count);
      },
      {
        lazy: true,
      }
    );
    obj.count = 2;
    console.log("代码结束");
</script>

当lazy是true的时候,只打印出代码结束;

当lazy是false的时候,打印出1和2:

调度器

调度器分为两个部分:

  1. 控制执行顺序;
  2. 控制执行规则;

控制执行顺序

我们上面的测试实例:

js 复制代码
 <script>
    const { reactive, effect } = Vue;

    const obj = reactive({
      count: 1,
    });

    effect(
      () => {
        console.log(obj.count);
      },
    );
    obj.count = 2;
    console.log("代码结束");
</script>

最后打印输出的顺序如下图所示:

如果我们想要输出的顺序变成下面这样呢?

1

代码结束

2

是否能用scheduler做到?我们来创建一个测试实例运行一下!

ts 复制代码
const { reactive, effect } = Vue

const obj = reactive({
  count: 1
})

effect(
  () => {
    console.log(obj.count)
  },
  {
    scheduler() {
      setTimeout(() => {
        console.log(obj.count)
      })
    }
  }
)
obj.count = 2
console.log('代码结束')

打印的顺序就变成了1、代码结束、2了!

这是为什么呢? 会想到我们自己实现的triggerEffect方法,其中有scheduler的判断;当我们在watch中传入scheduler时就不会去执行run方法了,scheduler方法是一个异步任务,只有等所有的同步任务完成后才会触发;也就是说scheduler会影响代码的执行顺序

js 复制代码
// 触发指定依赖
export function triggerEffect(effect: ReactiveEffect) {
  // 判读是否有scheduler
  if (effect.scheduler) {
    effect.scheduler();
  } else {
    effect.run();
  }
}

那现在为我们的项目vue-mini去增加scheduler相关的逻辑; 在shared/index.ts中增加一个extend方法;

ts 复制代码
export const extend = Object.assign;

修改effect.ts方法,增加合并_effect和options的逻辑:

ts 复制代码
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
  const _effect = new ReactiveEffect(fn);
  if (options) {
    // 合并_effect和options
    extend(_effect, options);
  }
  if (!options || !options.lazy) {
    _effect.run();
  }
}

运行上面相同的实例,会发现打印的顺序改变了;

ts 复制代码
const { reactive, effect } = Vue

const obj = reactive({
  count: 1
})

effect(
  () => {
    console.log(obj.count)
  },
  {
    scheduler() {
      setTimeout(() => {
        console.log(obj.count)
      })
    }
  }
)
obj.count = 2
console.log('代码结束')

控制执行规则

创建一个新的测试实例:

ts 复制代码
const { reactive, effect } = Vue

const obj = reactive({
  count: 1
})

effect(() => {
  console.log(obj.count)
})
obj.count = 2
obj.count = 3

这次打印出1、2、3;最后obj.count都会被修改成3,那我们能跳过2的流程吗?在vue3的源码中scheduler.ts文件中有一个queuePreFlushCb方法,它是用来控制scheduler的执行规则的,现在我们来看一下它是怎么运行的;

首先要去vue3源码中export queuePreFlushCb方法,重新build一遍;然后修改我们的测试实例,再运行一下;

ts 复制代码
const { reactive, effect, queuePreFlushCb } = Vue

const obj = reactive({
  count: 1
})

effect(
  () => {
    console.log(obj.count)
  },
  {
    scheduler() {
      queuePreFlushCb(() => console.log(obj.count))
    }
  }
)
obj.count = 2
obj.count = 3

queueFlush方法是关键

ts 复制代码
function queueFlush() {
  // 此时isFlushing和isFlushPending都为false
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    // resolvedPromise是Promise.resolve(),flushJobs都变成异步微任务;
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

接着我们来实现scheduler的控制执行规则功能; 新建packages/runtime-core/src/scheduler.ts;

ts 复制代码
let isFlushPending = false;
const pendingPreFlushCbs: Function[] = [];
let currentFlushPromise: Promise<void> | null = null;
const resolvedPromise = Promise.resolve() as Promise<any>;

export function queuePreFlushCb(cb: Function) {
  queueCb(cb, pendingPreFlushCbs);
}

export function queueCb(cb: Function, pendingQueue: Function[]) {
  pendingQueue.push(cb);
  queueFlush();
}

export function queueFlush() {
  if (!isFlushPending) {
    isFlushPending = true;
    currentFlushPromise = resolvedPromise.then(flushJobs);
  }
}

function flushJobs() {
  isFlushPending = false;
  flushPostFlushCbs();
}

export function flushPostFlushCbs() {
  if (pendingPreFlushCbs.length) {
    let activePostFlushCbs = [...new Set(pendingPreFlushCbs)];
    pendingPreFlushCbs.length = 0;

    for (
      let postFlushIndex = 0;
      postFlushIndex < activePostFlushCbs.length;
      postFlushIndex++
    ) {
      activePostFlushCbs[postFlushIndex]();
    }
  }
}

记得还有两个地方需要export queuePreFlushCb方法;

相关推荐
速易达网络9 分钟前
Vue3 原生移动应用开发来了
前端·javascript·css
GISer_Jing12 分钟前
LLM对话框项目技术栈&重难点总结
前端·ai·node.js
我爱学习_zwj33 分钟前
【鸿蒙进阶-7】鸿蒙与web混合开发
前端·华为·harmonyos
小谭鸡米花34 分钟前
高德地图电子围栏/地图选区/地图打点
前端·javascript·vue.js
摆烂工程师37 分钟前
什么是 ChatGPT Business 会员?与 ChatGPT Plus 有什么不同?
前端·后端·程序员
闲不住的李先森1 小时前
使用Context7:告别AI“幻觉”,让编程助手永远生成最新、可用的代码
前端·ai编程·cursor
西瓜树枝1 小时前
解决 JS 大整数精度丢失?一文读懂 BigInt 的底层逻辑与实战规则
前端·javascript
刺客_Andy1 小时前
React 第四十六节 Router中useInRouterContext的使用详细介绍及注意事项
前端·javascript·react.js
刺客_Andy1 小时前
React 第四十四节Router中 usefetcher的使用详解及注意事项
前端·javascript·react.js
该用户已不存在1 小时前
我的Python工具箱,不用加班的秘密
前端·后端·python