1.通过阅读本篇文章你可以学到:
Promise的几道基础题
Promise结合setTimeout
Promise中的then、catch、finally
Promise中的all和race
async/await的几道题
async处理错误
综合题
几道大厂的面试题
2.前期准备
在做下面?的题目之前,我希望你能清楚几个知识点。
(如果你感觉一上来不想看这些列举的知识点的话,直接看后面的例子再来理解它们也可以)
event loop它的执行顺序:
一开始整个脚本作为一个宏任务执行
执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
执行浏览器UI线程的渲染工作
检查是否有Web Worker任务,有则执行
执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
宏任务包括:script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering。
微任务包括:MutationObserver、Promise.then()或catch()、Promise为基础开发的其它技术,比如fetch API、V8的垃圾回收过程、Node独有的process.nextTick。
注意⚠️:在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。
3.试题
1.Promise的几道基础题
1.1题目一
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
})
console.log('1', promise1);
过程分析:
从上至下,先遇到new Promise,执行该构造函数中的代码promise1 然后执行同步代码1,此时promise1没有被resolve或者reject,因此状态还是pending
结果:
'promise1'
'1' Promise{<pending>}
1.2题目二
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
过程分析:
从上至下,先遇到new Promise,执行其中的同步代码1 再遇到resolve('success'), 将promise的状态改为了resolved并且将值保存下来 继续执行同步代码2 跳出promise,往下执行,碰到promise.then这个微任务,将其加入微任务队列 执行同步代码4 本轮宏任务全部执行完毕,检查微任务队列,发现promise.then这个微任务且状态为resolved,执行它。
结果:
1 2 4 3
1.3题目三
const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
过程分析
和题目二相似,只不过在promise中并没有resolve或者reject 因此promise.then并不会执行,它只有在被改变了状态之后才会执行。
结果:
1 2 4
1.4题目四
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
过程分析:
从上至下,先遇到new Promise,执行该构造函数中的代码promise1 碰到resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来 碰到promise1.then这个微任务,将它放入微任务队列 promise2是一个新的状态为pending的Promise 执行同步代码1, 同时打印出promise1的状态是resolved 执行同步代码2,同时打印出promise2的状态是pending 宏任务执行完毕,查找微任务队列,发现promise1.then这个微任务且状态为resolved,执行它。
结果:
'promise1'
'1' Promise{<resolved>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'
1.5题目五
const fn = () => (new Promise((resolve, reject) => {
console.log(1);
resolve('success')
}))
fn().then(res => {
console.log(res)
})
console.log('start')
这道题里最先执行的是'start'吗 ?️ ?
请仔细看看哦,fn函数它是直接返回了一个new Promise的,而且fn函数的调用是在start之前,所以它里面的内容应该会先执行。
结果:
1
'start'
'success'
1.6题目六
const fn = () =>
new Promise((resolve, reject) => {
console.log(1);
resolve("success");
});
console.log("start");
fn().then(res => {
console.log(res);
});
是的,现在start就在1之前打印出来了,因为fn函数是之后执行的。 注意⚠️:之前我们很容易就以为看到new Promise()就执行它的第一个参数函数了,其实这是不对的,就像这两道题中,我们得注意它是不是被包裹在函数当中,如果是的话,只有在函数调用的时候才会执行。
答案:
"start"
1
"success"
好嘞,学完了这几道基础题,让我们来用个表情包压压惊。
2.Promise结合setTimeout
2.1题目一
console.log('start')
setTimeout(() => {
console.log('time')
})
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')
过程分析:
刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈进行执行,因此先打印出start和end。 setTimout作为一个宏任务被放入宏任务队列(下一个) Promise.then作为一个微任务被放入微任务队列 本次宏任务执行完,检查微任务,发现Promise.then,执行它 接下来进入下一个宏任务,发现setTimeout,执行。
结果:
'start'
'end'
'resolve'
'time'
2.2题目二
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
过程分析: 和题目1.2很像,不过在resolve的外层加了一层setTimeout定时器。
从上至下,先遇到new Promise,执行该构造函数中的代码1 然后碰到了定时器,将这个定时器中的函数放到下一个宏任务的延迟队列中等待执行 执行同步代码2 跳出promise函数,遇到promise.then,但其状态还是为pending,这里理解为先不执行 执行同步代码4 一轮循环过后,进入第二次宏任务,发现延迟队列中有setTimeout定时器,执行它 首先执行timerStart,然后遇到了resolve,将promise的状态改为resolved且保存结果并将之前的promise.then推入微任务队列 继续执行同步代码timerEnd 宏任务全部执行完毕,查找微任务队列,发现promise.then这个微任务,执行它。
因此执行结果为:
1
2
4
"timerStart"
"timerEnd"
"success"
2.3题目三
题目三分了两个题目,因为看着都差不多,不过执行的结果却不一样,大家不妨先猜猜下面两个题目分别执行什么:
(1)
setTimeout(() => {
console.log('timer1');
setTimeout(() => {
console.log('timer3')
}, 0)
}, 0)
setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
(2)
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise')
})
}, 0)
setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
执行结果:
'start'
'timer1'
'timer2'
'timer3'
'start'
'timer1'
'promise'
'timer2'
这两个例子,看着好像只是把第一个定时器中的内容换了一下而已。 一个是为定时器timer3,一个是为Promise.then 但是如果是定时器timer3的话,它会在timer2后执行,而Promise.then却是在timer2之前执行。 你可以这样理解,Promise.then是微任务,它会被加入到本轮中的微任务列表,而定时器timer3是宏任务,它会被加入到下一轮的宏任务中。 理解完这两个案例,可以来看看下面一道比较难的题目了。
2.4题目四
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
这道题稍微的难一些,在promise中执行定时器,又在定时器中执行promise; 并且要注意的是,这里的Promise是直接resolve的,而之前的new Promise不一样。 (偷偷告诉你,这道题往下一点有流程图) 因此过程分析为:
刚开始整个脚本作为第一次宏任务来执行,我们将它标记为宏1,从上至下执行 遇到Promise.resolve().then这个微任务,将then中的内容加入第一次的微任务队列标记为微1 遇到定时器timer1,将它加入下一次宏任务的延迟列表,标记为宏2,等待执行(先不管里面是什么内容) 执行宏1中的同步代码start 第一次宏任务(宏1)执行完毕,检查第一次的微任务队列(微1),发现有一个promise.then这个微任务需要执行 执行打印出微1中同步代码promise1,然后发现定时器timer2,将它加入宏2的后面,标记为宏3 第一次微任务队列(微1)执行完毕,执行第二次宏任务(宏2),首先执行同步代码timer1 然后遇到了promise2这个微任务,将它加入此次循环的微任务队列,标记为微2 宏2中没有同步代码可执行了,查找本次循环的微任务队列(微2),发现了promise2,执行它 第二轮执行完毕,执行宏3,打印出timer2
所以结果为:
'start'
'promise1'
'timer1'
'promise2'
'timer2'
2.5题目五
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
setTimeout(() => {
console.log('promise1', promise1)
console.log('promise2', promise2)
}, 2000)
过程分析:
从上至下,先执行第一个new Promise中的函数,碰到setTimeout将它加入下一个宏任务列表 跳出new Promise,碰到promise1.then这个微任务,但其状态还是为pending,这里理解为先不执行 promise2是一个新的状态为pending的Promise 执行同步代码console.log('promise1'),且打印出的promise1的状态为pending 执行同步代码console.log('promise2'),且打印出的promise2的状态为pending 碰到第二个定时器,将其放入下一个宏任务列表 第一轮宏任务执行结束,并且没有微任务需要执行,因此执行第二轮宏任务 先执行第一个定时器里的内容,将promise1的状态改为resolved且保存结果并将之前的promise1.then推入微任务队列 该定时器中没有其它的同步代码可执行,因此执行本轮的微任务队列,也就是promise1.then,它抛出了一个错误,且将promise2的状态设置为了rejected 第一个定时器执行完毕,开始执行第二个定时器中的内容 打印出'promise1',且此时promise1的状态为resolved 打印出'promise2',且此时promise2的状态为rejected
完整的结果为:
'promise1' Promise{<pending>}
'promise2' Promise{<pending>}
test5.html:102 Uncaught (in promise) Error: error!!! at test.html:102
'promise1' Promise{<resolved>: "success"}
'promise2' Promise{<rejected>: Error: error!!!}
2.6题目六
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("success");
console.log("timer1");
}, 1000);
console.log("promise1里的内容");
});
const promise2 = promise1.then(() => {
throw new Error("error!!!");
});
console.log("promise1", promise1);
console.log("promise2", promise2);
setTimeout(() => {
console.log("timer2");
console.log("promise1", promise1);
console.log("promise2", promise2);
}, 2000);
结果:
'promise1里的内容'
'promise1' Promise{<pending>}
'promise2' Promise{<pending>}
'timer1'
test5.html:102 Uncaught (in promise) Error: error!!! at test.html:102
'timer2'
'promise1' Promise{<resolved>: "success"}
'promise2' Promise{<rejected>: Error: error!!!}
3.Promise中的then、catch、finally
总结:
Promise的状态一经改变就不能再改变。(见3.1)
.then和.catch都会返回一个新的Promise。(上面的?1.4证明了)
catch不管被连接到哪里,都能捕获上层未捕捉过的错误。(见3.2)
在Promise中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如return 2会被包装为return Promise.resolve(2)。
Promise 的 .then 或者 .catch 可以被调用多次, 但如果Promise内部的状态一经改变,并且有了一个值,那么后续每次调用.then或者.catch的时候都会直接拿到该值。(见3.5)
.then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。(见3.6)
.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。(见3.7)
.then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。(见3.8)
.then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为catch是.then第二个参数的简便写法。(见3.9)
.finally方法也是返回一个Promise,他在Promise结束的时候,无论结果为resolved还是rejected,都会执行里面的回调函数。
3.1题目一
const promise = new Promise((resolve, reject) => {
resolve("success1");
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then: ", res);
}).catch(err => {
console.log("catch: ", err);
})
结果:
"then: success1"
构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用 。验证了第一个结论,Promise的状态一经改变就不能再改变。
3.2题目二
const promise = new Promise((resolve, reject) => {
reject("error");
resolve("success2");
});
promise
.then(res => {
console.log("then1: ", res);
}).then(res => {
console.log("then2: ", res);
}).catch(err => {
console.log("catch: ", err);
}).then(res => {
console.log("then3: ", res);
})
结果:
"catch: " "error"
"then3: " undefined
验证了第三个结论,catch不管被连接到哪里,都能捕获上层未捕捉过的错误。
至于then3也会被执行,那是因为catch()也会返回一个Promise,且由于这个Promise没有返回值,所以打印出来的是undefined。
3.3题目三
(1)
Promise.resolve(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
结果:
1
2
Promise可以链式调用,不过promise 每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般我们任务的链式调用一样return this。 上面的输出结果之所以依次打印出1和2,那是因为resolve(1)之后走的是第一个then方法,并没有走catch里,所以第二个then中的res得到的实际上是第一个then的返回值。 且return 2会被包装成resolve(2)。
(2)
Promise.resolve(1)
.then(res => {
console.log(res);
})
.catch(err => {
return 3;
})
.then(res => {
console.log(res);
});
结果:
1
undefined
.then执行成功后必然返回一个新的Promise,不论有没有return 新值,都会执行下一个.then,默认参数为undefined。
3.4题目四
如果把3.3中的Promise.resolve(1)改为Promise.reject(1)又会怎么样呢?
Promise.reject(1)
.then(res => {
console.log(res);
return 2;
})
.catch(err => {
console.log(err);
return 3
})
.then(res => {
console.log(res);
});
结果:
1
3
结果打印的当然是 1 和 3啦,因为reject(1)此时走的就是catch,且第二个then中的res得到的就是catch中的返回值。
3.5题目五
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('timer')
resolve('success')
}, 1000)
})
const start = Date.now();
promise.then(res => {
console.log(res, Date.now() - start)
})
promise.then(res => {
console.log(res, Date.now() - start)
})
执行结果:
'timer'
'success' 1001
'success' 1002
当然,如果你足够快的话,也可能两个都是1001。
Promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。
3.6题目六
Promise.resolve().then(() => {
return new Error('error!!!')
}).then(res => {
console.log("then: ", res)
}).catch(err => {
console.log("catch: ", err)
})
猜猜这里的结果输出的是什么 ?️ ?
你可能想到的是进入.catch然后被捕获了错误。
结果并不是这样的,它走的是.then里面:
"then: " "Error: error!!!"
这也验证了第4点和第6点,返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也被包裹成了return Promise.resolve(new Error('error!!!'))。 当然如果你抛出一个错误的话,可以用下面?两的任意一种:
return Promise.reject(new Error('error!!!'));
// or
throw new Error('error!!!')
3.7题目七
const promise = Promise.resolve().then(() => {
return promise;
})
promise.catch(console.err)
.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
因此结果会报错:
Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
3.8题目八
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
这道题看着好像很简单,又感觉很复杂的样子,怎么这么多个.then啊... ?
c其实你只要记住原则8:.then 或者 .catch 的参数期望是函数,传入非函数则会发生值透传。 第一个then和第二个then中传入的都不是函数,一个是数字类型,一个是对象类型,因此发生了透传(跳过非函数的then函数),将resolve(1) 的值直接传到最后一个then里。
所以输出结果为:
1
3.9题目九
下面来介绍一下.then函数中的两个参数。
第一个参数是用来处理Promise成功的函数,第二个则是处理失败的函数。
也就是说Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。
让我们来看看这个例子?:
(1)
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).catch(err => {
console.log('catch', err)
})
这里的执行结果是:
'error' 'error!!!'
它进入的是then()中的第二个参数里面,而如果把第二个参数去掉,就进入了catch()中,错误执行后不再执行.catch函数,但是可以执行.then函数:
Promise.reject('error!!!')
.then((res) => {
console.log('success', res)
}).catch(err => {
console.log('catch', err)
})
执行结果:
'catch' 'error!!!'
(2)
Promise.reject('err!!!')
.then((res) => {
console.log('success', res)
}, (err) => {
console.log('error', err)
}).then(err => {
console.log(1)
})
执行结果:
'error' 'error!!!'
1
3.10题目十
接着来看看.finally(),这个功能一般不太用在面试中,不过如果碰到了你也应该知道该如何处理。 其实你只要记住它三个很重要的知识点就可以了:
.finally()方法不管Promise对象最后的状态如何都会执行 .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是没法知道Promise最终的状态是resolved还是rejected的 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
来看看这个简单的例子?:
Promise.resolve('1')
.then(res => {
console.log(res)
})
.finally(() => {
console.log('finally')
})
Promise.resolve('2')
.finally(() => {
console.log('finally2')
return '我是finally2返回的值'
})
.then(res => {
console.log('finally2后面的then函数', res)
})
这两个Promise的.finally都会执行,且就算finally2返回了新的值,它后面的then()函数接收到的结果却还是'2',因此打印结果为:
'1'
'finally2'
'finally'
'finally2后面的then函数' '2'
至于为什么finally2的打印要在finally前面,请看下一个例子中的解析。
不过在此之前让我们再来确认一下,finally中要是抛出的是一个异常是怎样的:
Promise.resolve('1')
.finally(() => {
console.log('finally1')
throw new Error('我是finally中抛出的异常')
})
.then(res => {
console.log('finally后面的then函数', res)
})
.catch(err => {
console.log('捕获错误', err)
})
执行结果为:
'finally1'
'捕获错误' Error: 我是finally中抛出的异常
但是如果改为return new Error('我是finally中抛出的异常'),打印出来的就是'finally后面的then函数 1'
OK,?,让我们来看一个比较难的例子?:
function promise1 () {
let p = new Promise((resolve) => {
console.log('promise1');
resolve('1')
})
return p;
}
function promise2 () {
return new Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally1'))
promise2()
.then(res => console.log(res))
.catch(err => console.log(err))
.finally(() => console.log('finally2'))
执行过程: 首先定义了两个函数promise1和promise2,先不管接着往下看。
promise1函数先被调用了,然后执行里面new Promise的同步代码打印出promise1
之后遇到了resolve(1),将p的状态改为了resolved并将结果保存下来。
此时promise1内的函数内容已经执行完了,跳出该函数
碰到了promise1().then(),由于promise1的状态已经发生了改变且为resolved因此将promise1().then()这条微任务加入本轮的微任务列表(这是第一个微任务)
这时候要注意了,代码并不会接着往链式调用的下面走,也就是不会先将.finally加入微任务列表,那是因为.then本身就是一个微任务,它链式后面的内容必须得等当前这个微任务执行完才会执行,因此这里我们先不管.finally()
再往下走碰到了promise2()函数,其中返回的new Promise中并没有同步代码需要执行,所以执行reject('error')的时候将promise2函数中的Promise的状态变为了rejected
跳出promise2函数,遇到了promise2().catch(),将其加入当前的微任务队列(这是第二个微任务),且链式调用后面的内容得等该任务执行完后才执行,和.then()一样。
OK, 本轮的宏任务全部执行完了,来看看微任务列表,存在promise1().then(),执行它,打印出1,然后遇到了.finally()这个微任务将它加入微任务列表(这是第三个微任务)等待执行
再执行promise2().catch()打印出error,执行完后将finally2加入微任务加入微任务列表(这是第四个微任务)
OK, 本轮又全部执行完了,但是微任务列表还有两个新的微任务没有执行完,因此依次执行finally1和finally2。 结果:
'promise1'
'1'
'error'
'finally1'
'finally2'
在这道题中其实能拓展的东西挺多的,之前没有提到,那就是你可以理解为链式调用后面的内容需要等前一个调用执行完才会执行。 就像是这里的finally()会等promise1().then()执行完才会将finally()加入微任务队列,其实如果这道题中你把finally()换成是then()也是这样的:
function promise1 () {
let p = new Promise((resolve) => {
console.log('promise1');
resolve('1')
})
return p;
}
function promise2 () {
return new Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then(res => console.log(res))
.catch(err => console.log(err))
.then(() => console.log('finally1'))
promise2()
.then(res => console.log(res))
.catch(err => console.log(err))
.then(() => console.log('finally2'))
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!