javascript中的generator和协程以及使用

593 阅读3分钟

genreator

generator 函数的特点是执行到 yield 之后可以停止,等待外部调用 next 之后继续向下执行。简单来说就是给外部提供了控制函数内部执行的方法。

例如:

function* test() {
  yield "1";
  yield "2";
  yield "3";
  return "4";
}

const t = test();
console.log(t.next());
console.log(t.next());
console.log(t.next());
console.log(t.next());

// 输出
// { value: '1', done: false }
// { value: '2', done: false }
// { value: '3', done: false }
// { value: '4', done: true }

调用 next 调用一次执行一次 yield,并且 next 会有返回值。

利用以上这个特性,可以将一个个 generator 抽象成一个个任务,类似于操作系统的进程被单核 cpu 轮询调度执行遇到 io 操作就会阻塞,然后触发其他进程进入调度。js 的单线程就相当于单核 cpu 调度,在代码层面实现一个多任务处理机制。

在线程内部的这种调度叫做协程。引用一段比较完善的说法

协程(Coroutines)是一种比线程更加轻量级的存在。协程完全由程序所控制(在用户态执行),带来的好处是性能大幅度的提升。

一个操作系统中可以有多个进程;一个进程可以有多个线程;同理,一个线程可以有多个协程。

协程是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处继续运行。

一个线程内的多个协程的运行是串行的,这点和多进程(多线程)在多核CPU上执行时是不同的。 多进程(多线程)在多核CPU上是可以并行的。当线程内的某一个协程运行时,其它协程必须挂起。

接下来一步步实现一个多任务调度处理。

首先实现两个耗时任务。

module.exports = function* task1() {
  while (true) {
    let input = yield "输入";
    console.log("耗时任务1", input);
  }
};

module.exports = function* task2() {
  while (true) {
    let input = yield "输入";
    console.log("耗时任务2", input);
  }
};

以上是两个死循环,来模拟长时间的耗时任务,为了这俩任务都能被执行,使用 generator 函数进行调度,模拟的调度代码如下:

const task1 = require("./tasks/task1.cjs");
const task2 = require("./tasks/task2.cjs");
const tasks = [task1(), task2()];

let i = 2;
setInterval(() => {
  console.log(tasks[i % 2].next(i));
  i++;
}, 1000); // 模拟1s调度一次

// 输出:
// { value: '输入', done: false }
// { value: '输入', done: false }
// 耗时任务1 4
// { value: '输入', done: false }
// 耗时任务2 5
// { value: '输入', done: false }
// 耗时任务1 6
// { value: '输入', done: false }
// 耗时任务2 7
// { value: '输入', done: false }
// 耗时任务1 8
// { value: '输入', done: false }
// 耗时任务2 9
// { value: '输入', done: false }
// 耗时任务1 10
// { value: '输入', done: false }
// 耗时任务2 11
// { value: '输入', done: false }
// 耗时任务1 12
// { value: '输入', done: false }
// 耗时任务2 13
// { value: '输入', done: false }
// 耗时任务1 14
// { value: '输入', done: false }
// 耗时任务2 15
// { value: '输入', done: false }
// 耗时任务1 16
// { value: '输入', done: false }
// 耗时任务2 17
// { value: '输入', done: false }
// 耗时任务1 18
// { value: '输入', done: false }
// 耗时任务2 19
// { value: '输入', done: false }

可以看到任务 1 和任务 2 被交替执行,死循环执行到 yield 的时候就被挂起,由外部决定是不是继续执行,解决了一个任务时间过长饿死其他任务的现象发生。

这个也可以延伸到对于耗时任务和渲染之间的处理,比如一个任务需要占用 20 秒的时间,这样会造成页面假死 3-4s,这显然是不行的,可以通过 generator 分解任务,将任务可以挂起并且可恢复执行。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <input type="text" placeholder="测试输入" / >
    <script>
      function* task() {
        while (true) {
          const input = yield "耗时任务结果";
          console.log("任务被执行");
        }
      }
      const t = task();
      function runTask(idleDeadline) {
        if (idleDeadline.timeRemaining() <= 10) {
          console.log("时间不够");
          requestIdleCallback(runTask);
        } else {
          t.next();
          requestIdleCallback(runTask);
        }
      }
      window.requestIdleCallback(runTask);
    </script>
  </body>
</html>

以上在渲染进程正常的情况下尽可能快的执行耗时任务。

当在输入框输入的时候运行输出如下:

image.png

通过requestIdleCallback获取空闲时间作为调度并且判断剩余时间作为兜底,可以保证优先保证渲染进程的执行再尽量快的执行这个死循环的任务。

参考

cloud.tencent.com/developer/a…