一个小题目考考你的数据结构与链式调用

388

题目描述

person.talk('hello').sleep(3000).talk('world')

实现一个类 Person,其实例支持上面的链式调用:打印 hello,3 秒后再打印 world。

问题分析

链式调用,每个方法肯定需要返回当前实例,首先我们初始化代码如下:

class Person {
  talk(str: string) {
    return this;
  }

  sleep(milSec: number) {
    return this;
  }
}

const person = new Person();

person.talk("hello").sleep(3000).talk("world");

再来逐个分析成员方法:

  • talk: 简单,其实就是 console.log
  • sleep: 也很简单,一个 setTimeout

完善方法实现得到下面的代码:

class Person {
  talk(str: string) {
    console.log(str);
    return this;
  }

  sleep(milSec: number) {
    setTimeout(() => {
      console.log("get up");
    }, milSec);
    return this;
  }
}

const person = new Person();

person.talk("hello").sleep(3000).talk("world");

问题来了,由于 js 的事件循环问题,上述代码会马上依次输出 helloworld,然后三秒后输出 get up,如下所示:

hello
world
# 三秒后输出 get up
get up

这个题目本质上就是一个任务调度的问题,既然是任务调度,那肯定就会联想到任务队列,思路就豁然开朗了:

  • 成员方法不执行具体逻辑,而是将具体逻辑放到一个任务队列中,然后通知执行器去执行
  • 定义执行器方法 run,不断从任务队列中获取任务并且执行
  • 有新的任务执行通知时,如果当前有任务正在执行或者队列中还有其他任务正在等待,阻塞当前任务,待清空队列之前的任务后再执行,否则直接执行该任务

完整代码如下所示:

type Task = () => Promise<boolean>;

class Person {
  tasks: Task[] = [];
  isRunning: boolean = false;

  talk(str: string) {
    const fn: Task = () => {
      return new Promise((resolve) => {
        console.log(str);
        resolve(true);
      });
    };
    this.tasks.push(fn);
    this.run();
    return this;
  }

  sleep(milSec: number) {
    const fn: Task = () => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(true);
        }, milSec);
      });
    };
    this.tasks.push(fn);
    this.run();
    return this;
  }

  run() {
    if (this.isRunning || !this.tasks.length) return;
    this.isRunning = true;
    // 从队列中取出第一个任务执行
    const task = this.tasks.shift() as Task;
    task().then(() => {
      this.isRunning = false;
      // 执行下一个任务
      this.run();
    });
  }
}

const person = new Person();

person.talk("hello").sleep(3000).talk("world");

至此,大功告成~