中断和恢复任务序列,面试官在考察我什么?

这是一道由渡一后期学员提供的面试题。

这道题出的非常好,题目本身没有什么难度,但是它考验了同学们的编程能力。

/**
 * 需求
 * 1. 依次顺序执行一系列任务
 * 2. 所有任务完成后可以得到每个任务的执行结果
 * 3. 需要返回两个方法,start 用于启动任务,pause 用于暂停任务
 * 4. 每个任务具有原子性,即不可中断,只能在两个任务之间中断
 * @param {...Function} tasks 任务列表,每个任务无参、异步
 */
function processTasks(...tasks) {}

同学们可以先停在这里,尝试一下把它解出来。

如果你看到这道题就有信心写出答案,说明你已经具备了扎实的基础能力和灵活的运用能力。

温馨提示:请同学们自备温水,因为这篇文章太干了!

分析需求

我们先把需求拆分成几个部分来逐一分析。

遇到问题时不要急于动手写代码,要先理清思路。

首先看需求 2 和 tasks 的描述,我们知道给我们的任务都是异步函数,并且需要在所有任务执行完毕后返回一个包含每个任务结果的数组。

这很简单,我们只要用 Promise.all 就可以实现。

然后看需求 1,要求我们按顺序执行任务,也就是说必须等待上一个任务完成才能执行下一个任务。

这也不难,我们只要用一个循环来依次等待每个任务就行了。

接着看需求 3,这个函数要返回两个方法。

function processTasks(...tasks) {
  return {
    // 用于启动 tasks 任务的执行
    start() {},
    // 用于中断 start 的执行
    pause() {},
  };
}

最后看需求 4,它说每个任务都是原子性的,不能被中断。这是什么意思呢?我们画个图来理解一下。

实现逻辑

到这里我们已经明白了需求,在整个过程中执行任务其实很容易,就是调用函数而已,难点在于如何控制中断。

首先我们想一想怎么知道是否要中断。

既然 start 是控制执行,pause 是控制中断,那肯定需要一个变量来存储执行状态。

通过这个变量我们就能判断是否要停止执行。

所以我们需要定义以下变量:

function processTasks(...tasks) {
  let isRunning = false; // true: 表示正在执行 false: 表示没有执行
  return {
    start() {
      isRunning = true; // 执行时设为 true
    },
    pause() {
      isRunning = false; // 中断时设为 false
    },
  };
}

因为可以在执行期间去中断,所以我们要在每一个任务执行完毕后都判断一下 isRunning 的状态,如果是 false 就说明被中断了。

并且最后还要返回每个任务的结果。

所以 start 的基本逻辑大概是这样的:

function processTasks(...tasks) {
  let isRunning = false;
  let result = []; // 用来接收每一个任务的执行结果
  return {
    async start() {
      isRunning = true;
      // 循环所有任务
      for (let i = 0; i < tasks.length; i++) {
        // 每一次循环去执行一个任务,并把任务的结果追加到 result 的数组中
        result.push(await tasks[i]());
        // 每一次任务执行完成后判断一下任务是否被中断
        if (!isRunning) {
          return;
        }
      }
    },
    pause() {
      isRunning = false;
    },
  };
}

现在基本的架子已经实现了,我们就得考虑,如果中断了,下次再去调用 start 的时候怎么办呢?

现在基本功能已经实现了,我们就得考虑:如果中断了再次调用 start 怎么办?比如说在第三个任务结束时被中断了,那再次调用 start 就会从头开始执行所有任务。所以我们需要记录当前执行到哪个任务了,所以也得有个变量来保存。

function processTasks(...tasks) {
  let isRunning = false;
  let result = [];
  let i = 0; // 当前任务执行的下标
  return {
    async start() {
      isRunning = true;
      // 将 for 改成 while,这样在中断后再次执行 i 表示的就是重新开始的位置了
      while (i < tasks.length) {
        result.push(await tasks[i]());
        i++; // 执行一个任务 i 加 1
        if (!isRunning) {
          return;
        }
      }
    },
    pause() {
      isRunning = false;
    },
  };
}

我们接下来要考虑的就是 start 的返回值,它返回的是一个 Promise,这很合理,因为它表示一系列异步操作。

但是这个 Promise 的状态是什么?什么时候完成?什么时候拒绝?什么时候挂起?这些都需要明确。

从逻辑上来讲,返回的这个 Promise 表示一系列任务的执行,只要没中断,它就会从头到尾把所有任务执行一遍。

因此这个 Promise 肯定是在所有任务全部执行完毕时 Promise 才完成。

那什么时候拒绝呢?其实就是有某一个任务失败的时候它就拒绝了。

那自然其他的情况都是挂起状态了。

但是如果按照我们目前的写法来看,即使是中断了,我们结束的状态也是以完成的形式结束的 Promise,因为返回的是 undefined,所以我们还得改造一下 start。

function processTasks(...tasks) {
  let isRunning = false;
  let result = [];
  let i = 0;
  return {
    start() {
      // start 直接返回一个 promise 我们就可以手动的控制 promise 的状态了
      return new Promise(async (resolve, reject) => {
        isRunning = true;
        while (i < tasks.length) {
          // 使用 try catch 捕获错误
          try {
            result.push(await tasks[i]());
          } catch (err) {
            // 有一个失败时返回失败的状态
            reject(err);
            return;
          }
          i++;
          if (!isRunning) {
            return;
          }
        }
        // 当 while 循环结束说明没有任何的失败情况,全部成功了
        // 我们调用 resolve,返回所有结果
        resolve(result);
      });
    },
    pause() {
      isRunning = false;
    },
  };
}

现在我们已经知道 start 返回值的状态了。

但是这么写呢,还有个问题,就是当 Promise 已经有状态了,我们下次再次调用 start 的时候,还会把所有的任务再次执行一遍,所以我们要在调用 start 的时候判断一下 Promise 是否有状态,如果有状态的就直接返回数据。

还有两个小细节我们要考虑到,就是当正在运行时如果我们多次调用的话,就会多次的执行任务序列,所以我们在调用 start 的时候要判断一下 isRunning 的状态,如果正在执行的话就什么也不做,并且在所有任务结束的时候我们要重置 isRunning 的状态。

最后一个就是当执行的是最后一个任务的时候,中断也没有意义了,所有在执行最后一个任务的时候即使 isRunning 的状态为 false 也不中断。

function processTasks(...tasks) {
  let isRunning = false;
  let result = [];
  let prom = null; // 声明一个变量用于保存 Promise 的状态
  let i = 0;
  return {
    start() {
      return new Promise(async (resolve, reject) => {
        // 每次执行 start 之前判断一下 prom 是否有值,有值就直接返回结果值
        if (prom) {
          prom.then(resolve, reject);
          return;
        }
        // 如果是运行状态就什么也不做
        if (isRunning) {
          return;
        }
        isRunning = true;
        while (i < tasks.length) {
          try {
            result.push(await tasks[i]());
          } catch (err) {
            isRunning = false; // 重置 isRunning 的状态
            reject(err);
            prom = Promise.reject(err); // 失败的时候保存状态
            return;
          }
          i++;
          // 但是当我们执行的是最后一个任务的话中断也没有意义了,所以 i 必须小于 tasks.length
          if (!isRunning && i < tasks.length) {
            return;
          }
        }
        isRunning = false; // 重置 isRunning 的状态
        resolve(result);
        prom = Promise.resolve(result); // 成功的时候保存状态
      });
    },
    pause() {
      isRunning = false;
    },
  };
}

写到这我们就大功告成了,如果你认真跟着xxx一步一步思考的同学想必也都学会了这个面试题的解决方法,希望在以后遇到类似的情况同学可以轻松解决。

测试

测试代码

<body>
  <button id="begin">开始任务</button>
  <button id="pause">暂停任务</button>
  <script src="./index.js"></script>
  <script>
    const tasks = []
    // 生成几个异步函数
    for (let i = 0; i < 5; i++) {
      tasks.push(
        () =>
          new Promise((resolve) => {
            setTimeout(() => {
              resolve(i)
            }, 1000)
          })
      )
    }
    const processor = processTasks(...tasks)
    begin.onclick = async () => {
      console.log('点击开始');
      const results = await processor.start()
      console.log('任务执行完成', results);
    }
    pause.onclick = async () => {
      console.log('点击暂停');
      processor.pause()
    }
  </script>
</body>

我们在代码中输入一些 console 方便查看测试效果

function processTasks(...tasks) {
  let isRunning = false;
  let result = [];
  let prom = null;
  let i = 0;
  return {
    start() {
      return new Promise(async (resolve, reject) => {
        if (prom) {
          prom.then(resolve, reject);
          return;
        }
        if (isRunning) {
          return;
        }
        isRunning = true;
        while (i < tasks.length) {
          try {
            console.log(i, "执行中");
            result.push(await tasks[i]());
            console.log(i, "执行完成");
          } catch (err) {
            isRunning = false;
            reject(err);
            prom = Promise.reject(err);
            return;
          }
          i++;
          if (!isRunning && i < tasks.length) {
            console.log("执行被中断");
            return;
          }
        }
        isRunning = false;
        resolve(result);
        prom = Promise.resolve(result);
      });
    },
    pause() {
      isRunning = false;
    },
  };
}

我们运行一下。

可以看出在任务 3 执行期间点击暂停,在任务 3 完成后,任务中断,再次点击开始,任务继续执行。

再测试下 Promise 有结果时的效果。

可以看到结果被直接返回了。

总结

其实这个面试题整个代码用到的都是 JS 的基础,所以说开发能力一方面在于同学们的知识储备和准确度,以及知识的深度,另一方面就在于同学们能不能灵活运用所学到的知识了。

基础不牢地动山摇,在学习新知识的同时同学们也不要忘记对基础的掌握。

如果你有什么问题或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值