共计 13993 个字符,预计需要花费 35 分钟才能阅读完成。
前言
最近在筹备面试题,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 已空,同时整段代码执行结束。