前言
最近在筹备面试题,console的输入程序之前始终迷迷糊糊。
必备常识
JS是单线程的
单线程是 JavaScript 外围特色之一。这意味着,在 JS 中所有工作都须要排队执行,前一个工作完结,才会执行后一个工作。
所以这就造成了一个问题:如果前一个工作耗时很长,后一个工作就不得不始终等着后面的工作执行完能力执行。比方咱们向服务器申请一段数据,因为网络问题,可能须要期待 60 秒左右能力胜利返回数据,此时只能期待申请实现,JS 能力去解决前面的代码。
同步工作和异步工作
为了解决JS单线程带来的问题,JavaScript 就将所有工作分成了同步工作和异步工作。
同步工作(Synchronous)
同步工作指的是以后一个(如果有)工作执行结束,接下来能够立刻执行的工作。这些工作将在主线程上顺次排队执行。也就是说排排队
//for(){} 和 console.log() 将会顺次执行,最终输入 0 1 2 3 4 done。for (let i = 0; i < 5; i++) {console.log(i)}console.log('done')
异步工作(Asynchronous)
异步工作绝对于同步工作,指的是不须要进入主线程排队执行,而是进入超车道、并车道。也就是工作队列中,造成一系列的工作。这些工作只有当被告诉能够执行的时候,该工作才会从新进入主线程执行。
//上面的 then() 办法须要期待 Promise 被 resolve() 之后能力执行,它是一个异步工作。最终输入 1 3 2。console.log(1)Promise.resolve().then(() => { console.log(2)})console.log(3)
具体来说就是,所有同步工作会在主线程上顺次排队执行,造成一个执行栈(Execution Context
Stack)。主线程之外,还存在一个工作队列。当异步工作有了运行后果,会在工作队列之中搁置对应的事件。当执行栈中的所有同步工作执行结束,工作队列里的异步工作就会进入执行栈,而后持续顺次执行。
异步工作(工作队列)能够分为
macrotasks(taskQueue):宏工作 task,也是咱们常说的工作队列
macrotasks 的划分:(留神先后顺序!)
- (1)setTimeout(提早调用)
- (2)setInterval(间歇调用)
- (3)setImmediate(Node 的立刻调用)
- (4)requestAnimationFrame(高频的 RAF)
- (5)I/O(I/O 操作)
- (6)UI rendering(UI 渲染)
- (7) 包裹在一个 script 标签中的 js 代码也是一个 Macrotasks
留神: (1)每一个 macrotask 的回调函数要放在下一车的结尾去执行! (2)只有 setImmediate 可能确保在下一轮事件循环立刻失去解决
microtasks:微工作(也称 job)调度在以后脚本执行完结后,立刻执行的工作,以防止付出额定一个 task 的费用。
microtasks :(留神先后顺序!)
- (1)process.nextTick(Node 中 定义出一个动作,并且让这个动作在下一个事件轮询的工夫点上执行)
- (2)Promises(详情看这篇文章:www.jianshu.com/p/06d16ce41…
- (3)Object.observe(原生观察者实现,已废除)
- (4)MutationObserver(监听 DOM change) 只有在 nextTick 空了才解决其它 microtask。(Next tick queue has even higher priority
over the Other Micro tasks queue.)
一个事件循环(eventLoop)的执行程序(十分重要):
- ① 开始执行脚本。
- ② 取 macrotasks(taskQueue)中的第一个 task 执行,该 task 的回调函数 放在下一个 task 结尾 执行。
- ③ 取 microtasks 中的全副 microtask 顺次执行,当这些 microtask 执行完结后,可持续增加 microtask 继续执行,直到 microtask 队列为空。
- ④ 取 macrotasks(taskQueue)中的第二个 task 执行,该 task 的回调函数 放在下一个 task 结尾 执行。
- ⑤ 再取 microtasks 中的全副 microtask 顺次执行,当这些 microtask 执行完结后,可持续增加 microtask 继续执行,直到 microtask 队列为空。
- ⑥ 循环 ② ③ 直到 macrotasks、microtasks 为空。
Promise 之所以无奈应用 catch 捕捉 setTimeout 回调中的谬误,是因为 Promise 的 then/catch 是在 setTimeout 之前执行的。
事件循环的程序,决定了 JavaScript 代码的执行程序。它从 script (整体代码) 开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空 (只剩全局),而后执行所有的 microtasks。当所有可执行的
microtasks 执行结束之后。循环再次从 macrotasks 开始,找到其中一个工作队列执行结束,而后再执行所有的 microtasks,这样始终循环上来。
翻译过去就是,先执行 Microtasks queue 中的所有 Microtasks,再挑一个 Macrotasks queue 来执行其中所有 Macrotasks,而后继续执行 Microtasks queue 中的所有
Microtasks,再挑一个 Macrotasks queue 来执行其中所有 Macrotasks ……
这也就解释了,为什么同一个事件循环中的 Microtasks 会比 Macrotasks 先执行。
练习题
练习题做题的根据就是以下图为主,留神要点宏工作是在下一次才执行呢,本次有微工作须要先执行,随后参考下图
习题1
console.log(1)setTimeout(()=>{ console.log(2)},0)process.nextTick(()=>{ console.log(3)})new Promise((resolve)=>{ console.log(4) resolve()}).then(()=>{ console.log(5)})
习题1解析
第一轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(1) | setTimeout | process |
console.log(4) | then |
- 首先执行同步工作,按呈现程序,输入 1
- 遇到 setTimeout,放入Macro event queue
- 遇到 process,放入 Micro event queue
- 遇到 promise,先立刻执行,输入 4,并将 then 回调放入 Micro event queue
- 而后看 Micro event queue,一一执行,输入 3, 输入 5
- 第一轮 Event Loop 执行完结
第二轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout | ||
- 取出 Macro event queue 第一个放入主流程执行
- 输入 2
- Micro event queue 没有工作
- 第二轮 Event Loop 执行完结
习题2
console.log(1)setTimeout(() => { console.log(2)}, 0)process.nextTick(() => { console.log(3)})new Promise((resolve) => { console.log(4) resolve()}).then(() => { console.log(5)})setTimeout(() => { console.log(6)}, 0)new Promise((resolve) => { console.log(7) setTimeout(() => { console.log(8) resolve() }, 0)}).then(() => { console.log(9) setTimeout(() => { console.log(10) new Promise((resolve) => { console.log(11) resolve() }).then(() => { console.log(12) }) }, 0)})// 1, 4, 7, 3, 5, 2, 6, 8, 9, 10, 11, 12
习题2解析
第一轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(1) | setTimeout2 | process3 |
console.log(4) | setTimeout6 | then5 |
console.log(7) | setTimeout8 |
- 主流程输入:1, 4, 7
- 执行第一个 Micro event queue:输入 3
- 第二个 Micro event queue:输入 5
- Micro event queue 清空,第一轮执行结束
第二轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout2 | setTimeout6 | |
setTimeout8 |
- 主流程输入 2
- Micro event queue 为空,第二轮执行结束
第三轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout6 | setTimeout8 | |
- 主流程输入 6
- 第三轮执行结束
第四轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
setTimeout9 | setTimeout10 | then9 |
- 留神,这里执行输入 8 后,resolve,这时才向 Micro event queue 压入 then 回调
- 执行 then9 回调,输入 9
- 又有新的 setTimeout,压入 Macro event queue
- 这轮循环没有货色可执行,完结
第五轮事件循环
主流程 | Macro event queue | Micro event queue |
---|---|---|
console.log(10) | then12 | |
console.log(11) |
- 第五轮,setTimeout10 进入主流程,输入 10
- 遇到 promise,输入 11
- resolve, 压入 then 到 Micro event queue
- 取出 Micro event queue 执行,输入 12
习题3
// 以下代码在 Node 环境运行:process.nextTick 由 Node 提供console.log("1")setTimeout(function () { console.log("2") process.nextTick(function () { console.log("3") }) new Promise(function (resolve) { console.log("4") resolve() }).then(function () { console.log("5") })})process.nextTick(function () { console.log("6")})new Promise(function (resolve) { console.log("7") resolve()}).then(function () { console.log("8")})setTimeout(function () { console.log("9") process.nextTick(function () { console.log("10") }) new Promise(function (resolve) { console.log("11") resolve() }).then(function () { console.log("12") })})// 最终输入:1 7 6 8 2 4 3 5 9 11 10 12
习题4
setTimeout(()=>{ console.log("setTimeout1"); Promise.resolve().then(data => { console.log(222); });},0);setTimeout(()=>{ console.log("setTimeout2");},0);Promise.resolve().then(data=>{ console.log(111);});//111 setTimeout1 222 setTimeout2
习题4解析
- 主线程上没有须要执行的代码
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏工作队列中(这个工作在下一次的事件循环中执行)。
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏工作队列中(这个工作在再下一次的事件循环中执行)。
- 首先查看微工作队列, 即 microtask队列,发现此队列不为空,执行第一个promise的then回调,输入 '111'。
- 此时microtask队列为空,进入下一个事件循环, 查看宏工作队列,发现有 setTimeout的回调函数,立刻执行回调函数输入 'setTimeout1',查看microtask
队列,发现队列不为空,执行promise的then回调,输入'222',microtask队列为空,进入下一个事件循环。 - 查看宏工作队列,发现有 setTimeout的回调函数, 立刻执行回调函数输入'setTimeout2'。参考: 前端进阶面试题具体解答
习题5
console.log('script start');setTimeout(function () { console.log('setTimeout---0');}, 0);setTimeout(function () { console.log('setTimeout---200'); setTimeout(function () { console.log('inner-setTimeout---0'); }); Promise.resolve().then(function () { console.log('promise5'); });}, 200);Promise.resolve().then(function () { console.log('promise1');}).then(function () { console.log('promise2');});Promise.resolve().then(function () { console.log('promise3');});console.log('script end');/*script startscript endpromise1promise3promise2setTimeout---0setTimeout---200promise5inner-setTimeout---0*/
习题5解析
- 首先程序执行完主过程上的同步工作,第一句和最初一句的console.log
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏工作队列中(这个工作在下一次的事件循环中执行)。
- 接着遇到setTimeout 200,它的作用是在 200ms 后将回调函数放到宏工作队列中(这个工作在再下一次的事件循环中执行)。
- 同步工作执行完之后,首先查看微工作队列, 即 microtask队列,发现此队列不为空,执行第一个promise的then回调,输入 'promise1',而后执行第二个promise的then回调,输入'
promise3',因为第一个promise的.then()的返回仍然是promise,所以第二个.then()会放到microtask队列继续执行,输入 'promise2'; - 此时microtask队列为空,进入下一个事件循环, 查看宏工作队列,发现有 setTimeout的回调函数,立刻执行回调函数输入 'setTimeout---0',查看microtask 队列,队列为空,进入下一次事件循环.
- 查看宏工作队列,发现有 setTimeout的回调函数, 立刻执行回调函数输入'setTimeout---200'.
- 接着遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏工作队列中,查看微工作队列,即 microtask 队列,发现此队列不为空,执行promise的then回调,输入'promise5'。
- 此时microtask队列为空,进入下一个事件循环,查看宏工作队列,发现有 setTimeout 的回调函数,立刻执行回调函数输入,输入'inner-setTimeout---0'。代码执行完结.
习题6
console.log("1");setTimeout(function cb1(){ console.log("2")}, 0);new Promise(function(resolve, reject) { console.log("3") resolve();}).then(function cb2(){ console.log("4");})console.log("5")// 1 3 5 4 2
习题6解析
习题7
console.log("1");setTimeout(() => { console.log("2") new Promise(resolve => { resolve() }).then(() => { console.log("3") })}, 0);setTimeout(() => { console.log("4")}, 0);console.log("5")// 1 5 2 3 4
习题7解析
习题8
console.log("1");setTimeout(() => { console.log("2") new Promise(resolve => { console.log(6) resolve() }).then(() => { console.log("3") })}, 0);setTimeout(() => { console.log("4")}, 0);console.log("5")// 1 5 2 6 3 4
习题8解析
习题9
console.log('start')setTimeout(function(){ console.log('宏工作1号')})Promise.resolve().then(function(){ console.log('微工作0号')})console.log('执行栈执行中')setTimeout(function(){ console.log('宏工作2号') Promise.resolve().then(function(){ console.log('微工作1号') })},500)setTimeout(function(){ console.log('宏工作3号') setTimeout(function(){ console.log('宏工作4号') Promise.resolve().then(function(){ console.log('微工作2号') }) },500) Promise.resolve().then(function(){ console.log('微工作3号') })},600)console.log('end')// start 执行栈执行中 end 微工作0号 宏工作1号 宏工作2号 微工作1号 宏工作3号 微工作3号 宏工作4号 微工作2号
习题9解析
习题10
function test() { console.log(1) setTimeout(function () { // timer1 console.log(2) }, 1000)}test();setTimeout(function () { // timer2 console.log(3)})new Promise(function (resolve) { console.log(4) setTimeout(function () { // timer3 console.log(5) }, 100) resolve()}).then(function () { setTimeout(function () { // timer4 console.log(6) }, 0) console.log(7)})console.log(8)//1 4 8 7 3 6 5 2
习题10解析
联合咱们上述的JS运行机制再来看这道题就简单明了的多了
- JS是程序从上而下执行
- 执行到test(),test办法为同步,间接执行,console.log(1)打印1
- test办法中setTimeout为异步宏工作,回调咱们把它记做timer1放入宏工作队列
- 接着执行,test办法上面有一个setTimeout为异步宏工作,回调咱们把它记做timer2放入宏工作队列
- 接着执行promise,new Promise是同步工作,间接执行,打印4
- new Promise外面的setTimeout是异步宏工作,回调咱们记做timer3放到宏工作队列
- Promise.then是微工作,放到微工作队列
- console.log(8)是同步工作,间接执行,打印8
- 主线程工作执行结束,查看微工作队列中有Promise.then
- 开始执行微工作,发现有setTimeout是异步宏工作,记做timer4放到宏工作队列
- 微工作队列中的console.log(7)是同步工作,间接执行,打印7
- 微工作执行结束,第一次循环完结
- 查看宏工作队列,外面有timer1、timer2、timer3、timer4,四个定时器宏工作,依照定时器延迟时间失去能够执行的程序,即Event
Queue:timer2、timer4、timer3、timer1,顺次拿出放入执行栈开端执行 (插播一条:浏览器 event loop 的 Macrotask queue,就是宏工作队列在每次循环中只会读取一个工作) - 执行timer2,console.log(3)为同步工作,间接执行,打印3
- 查看没有微工作,第二次Event Loop完结
- 执行timer4,console.log(6)为同步工作,间接执行,打印6
- 查看没有微工作,第三次Event Loop完结
- 执行timer3,console.log(5)同步工作,间接执行,打印5
- 查看没有微工作,第四次Event Loop完结
- 执行timer1,console.log(2)同步工作,间接执行,打印2
- 查看没有微工作,也没有宏工作,第五次Event Loop完结 后果:1,4,8,7,3,6,5,2
习题11
setTimeout(() => { console.log(1)}, 0)new Promise((resolve, reject) => { console.log(2) resolve(3)}).then(val => { console.log(val)})console.log(4) // 2 4 3 1
习题11解析
习题12
for (let i = 0; i < 5; i++) { console.log(i)}console.log('done')// 0 1 2 3 4 done
习题12解析
习题13
console.log(1)Promise.resolve().then(() => { console.log(2)})console.log(3)//1 3 2
习题13解析
习题14
setTimeout(() => { console.log(1)}, 0)for (let i = 2; i <= 3; i++) { console.log(i)}console.log(4)setTimeout(() => { console.log(5)}, 0)for (let i = 6; i <= 7; i++) { console.log(i)}console.log(8)//2 3 4 6 7 8 1 5
习题14解析
习题15
console.log(1)async function async1() { await async2() console.log(2)}async function async2() { console.log(3)}async1()setTimeout(() => { console.log(4)}, 0)new Promise(resolve => { console.log(5) resolve()}) .then(() => { console.log(6) }) .then(() => { console.log(7) })console.log(8)// 1 3 5 8 2 6 7 4
习题15解析
习题16
console.log(1)function a() { return new Promise(resolve => { console.log(2) setTimeout(() => { console.log(3) }, 0) resolve() })}a().then(() => { console.log(4)})//1 2 4 3
习题16解析
习题17
console.log('script start');setTimeout(function() { console.log('setTimeout');}, 0);Promise.resolve().then(function() { console.log('promise1');}).then(function() { console.log('promise2');});console.log('script end');//script start、script end、promise1、promise2、setTimeout
习题17解析
- 整体script作为第一个宏工作进入主线程,遇到console.log,输入script start
- 遇到 setTimeout,其回调函数被散发到宏工作 Event Queue 中
- 遇到 Promise,其 then函数被分到到微工作 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微工作 Event Queue 中,记为 then2
遇到 console.log,输入 script end。至此,Event Queue中存在三个工作,如下表:
主流程 Macro event queue Micro event queue console.log('script start') console.log('setTimeout') console.log('promise1') console.log('script end') console.log('promise1')
习题18
console.log('script start');setTimeout(function() { console.log('timeout1');}, 10);new Promise(resolve => { console.log('promise1'); resolve(); setTimeout(() => console.log('timeout2'), 10);}).then(function() { console.log('then1')})console.log('script end');//script start、promise1、script end、then1、timeout1、timeout2
习题18解析
首先,事件循环从宏工作 (macrotask) 队列开始,最初始,宏工作队列中,只有一个 script(整体代码)工作;当遇到工作源 (task source)
时,则会先散发工作到对应的工作队列中去。所以,就和下面例子相似,首先遇到了console.log,输入 script start; 接着往下走,遇到 setTimeout 工作源,将其散发到工作队列中去,记为 timeout1; 接着遇到
promise,new promise 中的代码立刻执行,输入 promise1, 而后执行 resolve ,遇到 setTimeout ,将其散发到工作队列中去,记为 timemout2, 将其 then 散发到微工作队列中去,记为
then1; 接着遇到 console.log 代码,间接输入 script end 接着查看微工作队列,发现有个 then1 微工作,执行,输入then1 再查看微工作队列,发现曾经清空,则开始查看宏工作队列,执行 timeout1,输入
timeout1; 接着执行 timeout2,输入 timeout2 至此,所有的都队列都已清空,执行结束。其输入的程序顺次是:script start, promise1, script end, then1, timeout1,
timeout2。
有个小 tip:从标准来看,microtask 优先于 task 执行,所以如果有须要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。 最初的最初,记住,JavaScript
是一门单线程语言,异步操作都是放到事件循环队列外面,期待主执行栈来执行的,并没有专门的异步执行线程。
习题19
console.log(1)setTimeout(function() { console.log(2)},0)setTimeout(function() { console.log(3)},0)console.log(4)// 1 4 2 3
习题19解析
习题20
function fun1(){ console.log(1)}function fun2(){console.log(2)fun1()console.log(3)}fun2()// 2 1 3
习题20解析
习题21
function func1(){ console.log(1)}function func2(){ setTimeout(()=>{ console.log(2) },0) func1() console.log(3)}func2()// 1 3 2
习题21解析
习题22
var p = new Promise(resolve=>{ console.log(4) //这里没有执行p也要有输入 所以4是最开始的 resolve(5)})function func1(){ console.log(1)}function func2(){ setTimeout(()=>{ console.log(2) },0) func1() console.log(3) p.then(resolve=>{ console.log(resolve) })}func2()//4 1 3 5 2
习题22解析
习题21
console.log('start')const interval = setInterval(() => { console.log('setInterval')}, 0)setTimeout(() => { console.log('setTimeout 1') Promise.resolve() .then(() => { console.log('promise 1') }) .then(() => { console.log('promise 2') }) .then(() => { setTimeout(() => { console.log('setTimeout 2') Promise.resolve() .then(() => { console.log('promise 3') }) .then(() => { console.log('promise 4') }) .then(() => { clearInterval(interval) }) }, 0) }) console.log('time end')}, 0)Promise.resolve().then(() => { console.log('promise 5')}).then(() => { console.log('promise 6')})// start// promise 5// promise 6// setInterval// setTimeout 1// time end// promise 1// promise 2// setInterval// setTimeout 2// setInterval// promise 3// promise 4
习题22解析
解析: (1)先依照 macrotask 和 microtask 划分代码:
console.log('start')
setInterval 是 macrotask,其回调函数在 microtask 后执行
const interval = setInterval(() => { console.log('setInterval') }, 0)
setTimeout 是 macrotask,其回调函数放在下一车(cycle 2)执行
setTimeout(() => ... , 0)
Promise.resolve () 的两个 then () 是 microtask
Promise.resolve() //microtask .then(() => { console.log('promise 5') }) //microtask .then(() => { console.log('promise 6') })
(2)第一班车(cycle 1):
进栈
第一个 macrotask 是 setInterval,回调函数放下一车(cycle 2)的结尾执行, 第二个 macrotask 是 setTimeout,回调函数放下下一车(cycle 3)的结尾执行,
清空栈, 输入:start 执行 microtasks,直至清空该队列,即 Promise.resolve () 的两个 then (), 输入:promise 5 promise 6
(3)第二班车(cycle 2): 执行 setInterval 的回调, 输入:setInterval, 同时下一个 setInterval 也是 macrotask 但要放到 下下下一车(cycle 4)执行回调,即
下下一车(cycle 3)setTimeout 的前面
此时 setInterval 中没有 microtasks,所以该队列是空的,故进行下一车(cycle 3)
(4)第三班车(cycle 3) 执行 setTimeout 的回调, 输入 setTimeout 1 执行 microtasks,直至清空该队列,即 Promise.resolve () 的第一个和第二个 then (),
输入:promise 1 promise 2
而 第三个 then () 中的 setTimeout 是 macrotask ,放到下下下下一车(cycle 5)执行回调, 第四个 then () 是紧跟着第三个 then () 的,所以在 下下下下一车(cycle 5)执行
此时 microtasks 已空,故进行下一车(cycle 4)
(5)第四班车(cycle 4) 由(3)得,执行 setInterval , 输入:setInterval
此时 setInterval 中没有 microtasks,所以该队列是空的,故进行下一车(cycle 5)
同时下一个 setInterval 也是 macrotask 但要放到 下下下下下一车(cycle 6)执行回调,
(6)第五班车('cycle 5') 由(4)得,执行 setTimeout 输入:setTimeout 2
执行 microtasks,直至清空该队列,即 Promise.resolve () 的第一个和第二个 then (),
输入:promise 3 promise 4
接着执行第三个 then () --> clearInterval(interval),将下下下下下一车(cycle 6)要执行回调的 setInterval 革除
此时 microtasks 已空, 同时整段代码执行结束。