头条面试记录-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 执行顺序如下所示:
- 首先执行同步代码,这属于宏任务
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有必要会渲染页面
- 然后开始下一轮 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、宏任务、微任务、同步异步、事件轮询机制相关知识哦,给大家提供下学习资料链接:
”
- 微任务、宏任务与Event-Loop
- 事件轮询机制,以及宏任务队列与微任务队列
- 什么是 Event Loop?作者: 阮一峰
- 这一次,彻底弄懂 JavaScript 执行机制
- 10分钟理解JS引擎的执行机制
- 你不懂JS: 异步与性能(You Dont Know JS)(第一版)
- 详解JavaScript中的Event Loop(事件循环)机制
- Promise
- Promise 对象-阮一峰
更多前端文档请参考 小圆脸儿[1]
参考资料
[1]更多文档: https://juejin.cn/user/1398234520230989