目录
前言
一、前提知识
1、JS 单线程机制
2、JS 任务队列与事件循环
3、Promise 回顾
二、题目实战
1、开头提到的题目
2、稍有难度
3、挑战升级
上一篇文章,当面试官问 promise 的时候,他们希望听到什么(一)_czjl6886的博客-CSDN博客笔者介绍了有关 promise 的理解和基本使用相关的面试问题,但是,只会这些,是不能帮助我们通过面试的。接下来,笔者再来讲解在面试中 promise 常见的编程题目。
先来一道面试题试试水:
setTimeout(() => {
console.log(1)
},0)
Promise.resolve().then(()=>{
console.log(2)
})
Promise.resolve().then(()=>{
console.log(3)
})
console.log(4)
上面的代码的在浏览器的控制台的输出顺序是什么呢?不妨自己试试看。
注意:本篇文章中,我所说的打印顺序都是基于浏览器的。
之前学过 promise 了,肯定一部分人会说小意思,但是,只会 promise ,是远远不够滴!
要成功做对上面的题目,还需要掌握以下知识点。
作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM 。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
为了避免复杂性,从一诞生,JavaScript 就是单线程,也就是同一个时间内只能做一件事情。HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM 。所以,这个新标准并没有改变 JavaScript 单线程的本质。
由于 js 是单线程,如果一个任务执行时间过长,那么,它的下一个任务就会等待很久,如,浏览器加载图片、视频等资源需要很长时间,这个时候,浏览器页面如果是一直卡着,直到资源加载完成,无疑会降低用户体验。
因此,js 将任务分为两种:同步任务( synchronous )和异步任务( asynchronous )。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。
除了广义的同步任务和异步任务,对任务有更精细的定义:
macro-task (宏任务) :整体 script 代码、setTimeout,setInterval 等
micro-task (微任务) :Promise,async / await,process.nextTick 等
总结:
- 所有的同步任务都在主线程上执行,行成一个执行栈。
- 当主线程的同步任务执行完毕,再执行任务队列中的异步任务。遇到宏任务,先将宏任务放入到”宏任务队列“,遇到微任务,将微任务放到”微任务队列“。
- 判断”微任务队列“是否为空,不为空的话,将”微任务队列“执行完毕后,再执行”宏任务队列“。
- 这个不断重复的过程,就是事件循环( Event Loop ) 。
- Promise 构造函数会立即执行, Promise.then() 内部的代码在当次事件循环的结尾立即执行(微任务)。
- promise 的状态一旦由 pending 变为 fulfilled 或 rejected ,当前 promise 就被标记为完成,后面不会再次改变该状态。
- resolve() 函数和 reject() 函数都将当前 Promise 状态改为完成,并将异步结果 value 或错误结果 reason 当做参数返回。
- promise 对象的构造函数只会调用一次,then 方法和 catch 方法都能多次调用,但一旦有了确定的结果,再次调用就会直接返回结果。
以下会详解各个题目的执行顺序,建议大家先自己推敲结果,再来查看答案。
setTimeout(() => {
console.log(1)
},0)
Promise.resolve().then(()=>{
console.log(2)
})
Promise.resolve().then(()=>{
console.log(3)
})
console.log(4)
在浏览器的控制台的输出结果如下:
思路讲解:
①这段 script 代码作为宏任务,进入主线程,先遇到 setTimeout ,异步任务,将其添加到宏任务队列,记为 事件setTimeout1 ;
② 接下来遇到 Promise.resolve().then ,异步任务,将其添加到微任务队列,记为事件 then1 ;
③接下来遇到 Promise.resolve().then ,异步任务,将其添加到微任务队列,记为事件 then2 ;
④接下来遇到 console.log( ) ,是同步任务,立即执行该行代码,输出结果 4 。
⑤整体 script 代码作为第一个宏任务执行结束,看看有哪些微任务、宏任务?
宏任务 | 微任务 |
setTimeout1 | then1 |
then2 |
⑥按照先入先出的方式,执行微任务队列中的任务:执行 then1 ,打印输出 2 ,执行 then2 ,打印输出 3 。OK,微任务队列执行完毕,然后开始执行宏任务队列中的任务:setTimeout1 ,打印输出 1。所以,最终的输出顺序为: 4 2 3 1
第一轮事件循环结束(当然,这个例子只有一轮)。
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0)
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
在浏览器的控制台的输出结果如下:
思路讲解:
① 这段 script 代码作为宏任务,进入主线程,先遇到 console.log( ) ,同步任务,立即执行该行代码,输出结果:script start ;
② 接下来遇到 setTimeout ,异步任务,将其添加到宏任务队列,记为事件:setTimeout1 ;
③ 接下来遇到 new Promise,在 Promise 构造函数中,遇到 console.log( ) ,同步任务,立即执行该行代码,输出结果 promise1 ,resolve()函数执行,改变 promise 实例对象的状态;
④ 接下来遇到 Promise.then ,异步任务,将其添加到微任务队列,记为事件 then1 ;
⑤ 接下来遇到 console.log( ) ,是同步任务,立即执行该行代码,输出结果:script end ;
⑥ 整体 script 代码作为第一个宏任务执行结束,看看有哪些微任务、宏任务?
宏任务 | 微任务 |
setTimeout1 | then1 |
⑦ 按照先入先出的方式,执行微任务队列中的任务:执行 then1 ,打印输出:promise2 ;OK,微任务队列执行完毕,然后开始执行宏任务队列中的任务:setTimeout1 ,打印输出 setTimeout
所以,最终的输出顺序为:script start、promise1、script end、promise2、setTimeout
setTimeout(() => {
console.log("0")
}, 0)
new Promise((resolve, reject) => {
console.log("1")
resolve()
}).then(() => {
console.log("2")
new Promise((resolve, reject) => {
console.log("3")
resolve()
}).then(() => {
console.log("4")
}).then(() => {
console.log("5")
})
}).then(() => {
console.log("6")
})
new Promise((resolve, reject) => {
console.log("7")
resolve()
}).then(() => {
console.log("8")
})
① 这段 script 代码作为宏任务,进入主线程,先遇到 setTimeout1 ,异步任务,将其添加到宏任务队列,记为事件 setTimeout1 ;
② 接下来遇到 new Promise,在 Promise 构造函数中,遇到 console.log( "1" ) ,同步任务,立即执行该行代码,输出结果 1,resolve()函数执行,改变 promise 实例对象的状态;
③ 接下来遇到 Promise.then ,异步任务,将其添加到微任务队列,记为事件 then1 ;接下来遇到 Promise.then ,异步任务,但是事件 then1 还没有执行完毕,所以这个不会被放到微任务队列中;
④ 接下来遇到 new Promise,在 Promise 构造函数中,遇到 console.log( "7" ) ,同步任务,立即执行该行代码,输出结果 7,resolve()函数执行,改变 promise 实例对象的状态;
⑤ 接下来遇到 Promise.then ,异步任务,将其添加到微任务队列,记为事件 then2 ;
⑥ 整体 script 代码作为第一个宏任务执行结束,看看有哪些微任务、宏任务?
宏任务 | 微任务 |
setTimeout1 | then1 |
then2 |
⑦ 按照先入先出的方式,执行微任务队列中的任务:
执行 then1 ,遇到 console.log( "2" ) ,同步任务,立即执行该行代码,打印输出 2,接下来遇到 new Promise,在 Promise 构造函数中,遇到 console.log( "3" ) ,同步任务,立即执行该行代码,输出结果 3,resolve()函数执行,改变 promise 实例对象的状态;
⑧ 接下来遇到 Promise.then ,异步任务,将其添加到微任务队列,记为事件 then3 ;
这里需要注意:这是难点,也是易错点!!!
- then 内部的同步代码同步执行,异步代码添加到对应的异步队列即可,运行完毕就算执行完毕了,如果后面接了 then ,就要将后面 then 的调用放入微队列
- then 的链式调用依赖上个 then 的调用完成,被依赖的 then 未被调用,依赖的 then 此刻就为 undefined ,被依赖的 then 被调用了,就能将依赖的 then 的代码加入到微队列
⑨ 接下来,遇到代码第 30 行 的 Promise.then ,异步任务,将其添加到微任务队列,记为事件 then4 ;
现在的任务队列如下:
宏任务 | 微任务 |
setTimeout1 | then2 |
then3 | |
then4 |
执行 then2 ,遇到 console.log( "8" ) ,同步任务,立即执行该行代码,打印输出 8,
执行 then3 ,遇到 console.log( "4" ) ,同步任务,立即执行该行代码,打印输出 4,
⑩ 接下来遇到 Promise.then ,异步任务,将其添加到微任务队列,记为事件 then5 ;
现在的任务队列如下:
宏任务 | 微任务 |
setTimeout1 | then4 |
then5 |
执行 then4 ,遇到 console.log( "6" ) ,同步任务,立即执行该行代码,打印输出 6;
执行 then5 ,遇到 console.log( "5" ) ,同步任务,立即执行该行代码,打印输出 5;
OK,微任务队列执行完毕,然后开始执行宏任务队列中的任务:setTimeout1 ,打印输出 0
最终的顺序是:1 7 2 3 8 4 6 5 0
这个题目,最容易出错的地方是 6 和 5 的顺序,大家一定要仔细。
网上还有很多 promise 经典面试题,大家可以去运用我上面讲解的思路,看看自己是否掌握了。
参考资料:
JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志
这一次,彻底弄懂 JavaScript 执行机制 - 掘金