头条面试记录-promise篇

598 阅读7分钟

头条面试记录-promise篇

主要考察es6中的promise、宏任务、微任务、同步异步、EventLoop相关知识

1. promise.all() promise.race() promise.allSettled() 区别

点击查看参考答案和讲解 promise.all() Promise.all的特点是会短路,它可以将多个实例组装成一个新的实例,只有所有Promise都成功的时候才会返回一个成功数组,失败的时候则返回最先被reject失败状态的值

promise.race() race是赛跑的意思,也就是说Promise.race([p1, p2, p3])里面的结果哪个获取的快,就返回哪个结果,不管结果本身是成功还是失败 promise.allSettled()

Promise.allSettled跟Promise.all类似, 其参数接受一个Promise的数组, 返回一个新的Promise, 唯一的不同在于, 其不会进行短路, 也就是说当Promise全部处理完成后我们可以拿到每个Promise的状态, 而不管其是否处理成功.

2. Promise如果设置了failure回调,同时设定了catch语句,如果reject,谁会捕获到?

点击查看参考答案和讲解
failure
https://es6.ruanyifeng.com/#docs/promise

3. 手写promise.all()

点击查看参考答案和讲解
Promise.prototype.all = function(promises) {
  let results = [];
  let promiseCount = 0;
  let promisesLength = promises.length;
  return new Promise(function(resolve, reject) {
    for (let val of promises) {
      Promise.resolve(val).then(function(res) {
        promiseCount++;
        // results.push(res);
        results[i] = res;
        // 当所有函数都正确执行了,resolve输出所有返回结果。
        if (promiseCount === promisesLength) {
          return resolve(results);
        }
      }, function(err) {
        return reject(err);
      });
    }
  });
};

Promise.all正常使用 下面为Promise.all的功能展示。正常情况下我们应该输出数组对象[1, 2, 3]。

let promise1 = new Promise(function(resolve) {
  resolve(1);
});
let promise2 = new Promise(function(resolve) {
  resolve(2);
});
let promise3 = new Promise(function(resolve) {
  resolve(3);
});

let promiseAll = Promise.all([promise1, promise2, promise3]);
promiseAll.then(function(res) {
  console.log(res);
});

ok,我们的Promise.all只要实现上面的功能就可以了。

Promise.all = function(promises) {
  let results = [];
  return new Promise(function(resolve) {
      promises.forEach(function(val) {
      // 按顺序执行每一个Promise操作
      val.then(function(res) {
        results.push(res);
      });
    });
    resolve(results);
  });
}

上面是最简化的版本,但是也有两个问题。一、Promise.all传递的参数可能不是Promise类型,可能不存在then方法。二、如果中间发生错误,应该直接返回错误,不执行后面操作。

改造版本

Promise.prototype.all = function(promises) {
  let results = [];
  let promiseCount = 0;
  let promisesLength = promises.length;
  return new Promise(function(resolve, reject) {
    for (let val of promises) {
      Promise.resolve(val).then(function(res) {
        promiseCount++;
        // results.push(res);
        results[i] = res;
        // 当所有函数都正确执行了,resolve输出所有返回结果。
        if (promiseCount === promisesLength) {
          return resolve(results);
        }
      }, function(err) {
        return reject(err);
      });
    }
  });
};

4. promise.all() 存在并发问题,请实现一个就有并发控制的调度器 即上面文中图二

点击查看参考答案和讲解 我们都知道promise.all方法可以执行多个promise,你给他多少个他就执行多少个,而且是一起执行,也就是并发执行。如果你给他100个,他会同时执行100个,如果这100个promise内都包含网络请求呢?

可能有人说,这种场景不多吧,一个页面内加起来就没几个接口,何况是并发请求了

但是如果让你做个文件分片上传呢?一个几百兆的文件分片后可能有几百个片段了吧。当然这也是一种极端情况,不过这确实是一个很明显的问题,还是需要解决的。

所以需要我们控制同时执行的promise个数,比如控制为2个,后面的所有promise都排队等待前面的执行完成。

简单说下思路

1.先把要执行的promise function 存到数组内

2.既然是最多为2个,那我们必然是要启动的时候就要让两个promise函数执行

3.设置一个临时变量,表示当前执行ing几个promise

4.然后一个promise执行完成将临时变量-1

5.然后借助递归重复执行

function Scheduler(concurrentCount) {
    this.list = []
    this.concurrentCount = concurrentCount;//并发数

    this.add = function(promiseCreator) {
        this.list.push(promiseCreator)
    }
    var tempRunIndex = 0;//临时计数器

    this.taskStart = function() {
        for (var i = 0; i < this.concurrentCount; i++) {
            request.bind(this)()
        }
    }

    function request() {
        if (!this.list || !this.list.length || tempRunIndex >= this.concurrentCount) {
            return
        }
        tempRunIndex++
        this.list.shift()().then(()=>{
                tempRunIndex--
                request.bind(this)()
            }
        )
    }
}


var scheduler = new Scheduler(2)
scheduler.add(()=>{
    return new Promise(resolve=>{
        setTimeout(()=>{
            console.log(1)
            resolve()
        },1000)
    })
})

scheduler.add(()=>{
    return new Promise(resolve=>{
        setTimeout(()=>{
            console.log(2)
            resolve()
        },500)
    })
})

scheduler.add(()=>{
    return new Promise(resolve=>{
        setTimeout(()=>{
            console.log(3)
            resolve()
        },300)
    })
})


scheduler.add(()=>{
    return new Promise(resolve=>{
        setTimeout(()=>{
            console.log(4)
            resolve()
        },400)
    })
})

scheduler.taskStart() 
//setTimeout 1000 value 1
//setTimeout 500 value 2
//setTimeout 300 value 3
//setTimeout 400 value 4

并发为2:输出结果 2 3 1 4

5. promise async、await 场景题,输出下面的执行结果

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
点击查看参考答案和讲解
script start => async2 end => Promise => script end => async1 end => promise1 => promise2  => setTimeout

首先先来解释下上述代码的 async 和 await 的执行顺序。当我们调用 async1 函数时,会马上输出 async2 end,并且函数返回一个 Promise,接下来在遇到 await的时候会就让出线程开始执行 async1 外的代码,所以我们完全可以把 await 看成是让出线程的标志。

然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置执行返回的 Promise 的 resolve 函数,这又会把 resolve 丢到微任务队列中,接下来去执行 then 中的回调,当两个 then 中的回调全部执行完毕以后,又会回到 await 的位置处理返回值,这时候你可以看成是 Promise.resolve(返回值).then(),然后 await 后的代码全部被包裹进了 then 的回调中,所以 console.log('async1 end') 会优先执行于 setTimeout。

如果你觉得上面这段解释还是有点绕,那么我把 async 的这两个函数改造成你一定能理解的代码

new Promise((resolve, reject) => {
  console.log('async2 end')
  // Promise.resolve() 将代码插入微任务队列尾部
  // resolve 再次插入微任务队列尾部
  resolve(Promise.resolve())
}).then(() => {
  console.log('async1 end')
})

也就是说,如果 await 后面跟着 Promise 的话,async1 end 需要等待三个 tick 才能执行到。那么其实这个性能相对来说还是略慢的,所以 V8 团队借鉴了 Node 8 中的一个 Bug,在引擎底层将三次 tick 减少到了二次 tick。但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR,目前已被同意这种做法。

所以 Event Loop 执行顺序如下所示:

  1. 首先执行同步代码,这属于宏任务
  2. 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
  3. 执行所有微任务
  4. 当执行完所有微任务后,如有必要会渲染页面
  5. 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数

所以以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。

微任务包括 process.nextTick ,promise ,MutationObserver,其中 process.nextTick 为 Node 独有。

宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。

这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。

6. 继续上面的场景题讲解后,再来做一道巩固一下刚才的知识点,下面的打印结果和顺序是什么呢?

async function async1() {
       console.log( 'async1 start');
    await async2;
    console.log(' async1 end');
}

async function async2() {
    console.log('async2');
}

console.log( 'script start');

setTimeout(function () {
    console.log( 'setTimeout');
}, 0);

async1();
new Promise(function(resolve) {
    console.log( 'promise1');

    resolve();
}).then(function () {
    console.log( 'promise2');
});
console.log('script end');
点击查看参考答案和讲解
//打印顺序
script start
async1 start
promise1
script end
async1 end
promise2
undefined
setTimeout

7. 说明

说明:如果还没有答对或者没搞清楚原因建议重新学习es6中的promise、宏任务、微任务、同步异步、事件轮询机制相关知识哦,给大家提供下学习资料链接:

更多前端文档请参考 小圆脸儿[1]

参考资料

[1]

更多文档: https://juejin.cn/user/1398234520230989