vv13
博客订阅

关于一道面试题的反思

偶然看到了一道面试题,限时15分钟,以下是它的题目:

// 修改下面的 start 函数, 使 execute 对应的 id 按顺序打印.

function start(id) {
  execute(id).catch(console.error);
}

// 测试代码 (请勿更改):

console.log('输出结果:');

for (let i = 0; i < 5; i++) {
  start(i);
}

function sleep(duration) {
  return new Promise(resolve => setTimeout(resolve, duration));
}

function execute(id) {
  let duration = Math.floor(Math.random() * 500);

  return sleep(duration).then(() => {
    console.log('id', id);
  });
}

// 输出结果参考
// id 0
// id 1
// id 2
// id 3
// id 4

希望你也直接尝试一下解题,这不会耗费多少时间,然后再接着往下阅读。

解题思路

从小有一个不好的习惯,做数学题或是物理题时,总想快速阅读完,也不会打草稿(首先是懒,其次是马虎,最致命的还是盲目自信)。因为只有寥寥15分钟,我首要想法是用最短时间做出来,所以直接寻找入口函数,也并没有注意到注释中“测试代码,请勿更改”八个字。

看到for循环i,函数执行结果是一个Promise,顿时让我联想到Promise属于微任务,for循环为当前的宏任务,因此打印值都可能是5,示例如下:

for (var i = 0; i < 5; i++) {
  new Promise(resolve => {
    resolve();
  }).then(() => {
    console.log(i);
  });
}

很显然原题是let变量,具有块级作用域,并且传值也不会出现同一个变量的问题,但我还是先傻乎乎的改了代码,利用闭包的特性规避此问题:

for (let i = 0; i < 5; i++) {
  (function(input) {start(input)})(i)
}

运行代码一看,嗯,果然不会输出同一个数字了(原题本身就不会产生这个问题,自己还以为是通过这种方式解决了问题)。

接着再往下看,知道了核心逻辑就在于execute函数,这个函数返回一个Promise对象,对象先执行一个setTimeout设置一个0.5秒以内的随机延迟时间,再调用resolve,然后在接下来的then方法内打印id。平时异步编程用的比较多,也经常通过Promise封装一些异步的方法,因此电光火石之间,我将start的函数调用结果进行了返回,并修改了for方法:

function start(id) {
  return execute(id).catch(console.error);
}

// 测试代码 (请勿更改):

console.log('输出结果:');
(async () => {
  for (let i = 0; i < 5; i++) {
    await start(i);
  }
})()

run了一下,输出结果ok,整个过程耗时了4分钟,自信回头。

二次解题

过了不久,再次打开题目一看,测试代码,请勿修改。 哦原来是粗心了啊,没关系,我只改start函数就好了嘛,思路肯定没问题:

const arrs = []
async function start(id) {
  arrs.push(() =>  execute(id).catch(console.error))
  if (arrs.length === 5) {
    for (item of arrs) {
      await item()
    }
  }
}

此时真心觉得这题没什么考点,根本不用动脑子好吧,比平时做的LeetCode简单多了,直到我看了一下标准答案,沉默许久,然后写了一遍,以下为最终答案:

let queue = [];
let running = false;
function start(id) {
  queue.push(id);
  if (running) return;
  next();
}

function next() {
  running = true;
  execute(queue.shift())
    .catch(console.error)
    .then(() => {
      running = false;
      if (queue.length) {
        next();
      }
    });
}

console.log('输出结果:');

for (let i = 0; i < 5; i++) {
  start(i);
}

function sleep(duration) {
  return new Promise(resolve => setTimeout(resolve, duration));
}

function execute(id) {
  let duration = Math.floor(Math.random() * 500);

  return sleep(duration).then(() => {
    console.log('id', id);
  });
}

以上代码大致思路为:

  1. 通过定义一个队列,以及一个全局锁
  2. 当有新任务来时,将任务入队列,并检查是否有锁。若有锁,则直接返回;若无锁,则执行next函数
  3. 在next函数中,设置锁,允许入队列但不允许再一次调用next,使用queue.shift()取出第一个异步任务进行执行,执行完成后,首先移除锁,并检查队列中是否还有任务,如果有的话,继续调用next函数

这个答案很难想到吗?非也,这只是一个队列的基本应用而已,但是除非题意指明,这样我可能会在不到10分钟之内解决,否则以我可能花1小时也不会想到去使用这样的方法。对于我自己写的代码来讲,我只是在利用async/await的特性,从而达到了顺序输出的效果。而这道题真正需要考察的不仅仅是语法层面,而是一个异步队列编程思想的运用。

很多时候,我们不知不觉就失去了思考与想象,沦为代码搬运工。希望自己平时能戒骄戒躁,踏踏实实写程序,从而享受代码世界中的乐趣,然后自然的讲出:写代码只是我的兴趣爱好罢了