题目一
说出下面代码输出的结果。
for(var i=0; i<5; i++){
setTimeout(function(){
console.log(i);
}, 3000);
}
回答:55555。
说一下,为什么?
解答:因为这里for中的var声明是全局的。
运行这段代码,会往异步事件表(异步Event Table)中注册5个 “过3秒后运行的function(){console.log(i);}
任务” 。直到同步队列(主队列)中的任务执行完成后,此时主队列为空,才会去检查异步事件表。按顺序按照对应的时间等待(这里都是3秒)结束后,才注册到异步任务队列中(也成等待队列),按照队列先进先出的原则,从异步任务队列不断取任务出来并放入到主队列中并由主线程来执行。此时在全局中,i的值为5,因此取出来的5个异步任务中运行结果都是打印5。
那你说说,既然js是单线程的,它又是如何实现异步的呢?
js中的异步以及多线程都可以理解成为一种“假象”,就拿h5的WebWorker来说,子线程有诸多限制,不能控制DOM,不能修改全局对象等等,通常只用来做计算做数据处理。
这些限制并没有违背js是单线程语言的思想,所以说是“假象”。
总的来说,JS异步的执行机制其实就是事件循环(eventloop),理解了eventloop机制,就理解了js异步的执行机制。
好,你提到了JS的事件循环(eventloop),你能说一下它是怎么运作的?
- 首先判断JS是同步还是异步,同步就进入主线程运行,异步就进入event table.
- 异步任务在event table中注册事件,当满足触发条件后,(触发条件可能是延时也可能是ajax回调),被推入event queue
- 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中。
- 每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行,也就是优先级比宏任务高,且与微任务所处的代码位置无关。
题目二
for主体和seTimeout条件不变的前提下,那如果我要输出01234呢?
- 使用自执行函数实现闭合作用域
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 3000);
})(i);
}
- 直接用let,块级作用域,使每次注册异步任务时使用的i都是当前块内的值。
for(let i=0; i<5; i++){
setTimeout(function(){
console.log(i);
}, 3000);
}
- 其实和上面道理类似,使用了function实现闭合作用域。
var output01 = function(i){
setTimeout(function () {
console.log(i);
}, 3000);
}
for (var i = 0; i < 5; i++) {
output01(i);
}
- 与上同理,闭合作用域
var output02 = (i)=>{
setTimeout(function () {
console.log(i);
}, 3000);
}
for (var i = 0; i < 5; i++) {
output02(i);
}
题目三
那如果我要输出012345呢?
- 道理同上,先输出01234,再输出5
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 500 * j);
})(i);
}
setTimeout(function () {
console.log(i);
}, 500 * i);
- 这是比较骚的实现方法,使用promise.all(),等待所有异步任务执行完成后,才输出5
const tasks = [];
for (var i = 0; i < 5; i++) {
(
(j) => {
tasks.push(new Promise((resolve) => {
setTimeout(() => {
console.log(j)
resolve();
}, 500 * j)
}))
}
)(i);
}
// 等待所有异步任务执行完成
Promise.all(tasks).then(() => {
setTimeout(function () {
console.log(i);
})
})
- 道理同上
const tasks = []
var output03 = function(i){
new Promise((resolve)=>{
setTimeout(function(){
console.log(i);
}, 500*i)
})
}
for(var i=0;i<5;i++){
tasks.push(output03(i));
}
Promise.all(tasks).then(()=>{
setTimeout(function(){
console.log(i)
}, 500*i)
})
结语
这个Promise和setTimeout这块确实比较绕,不过只要遵守事件队列先进先出的原则来分析之,总能慢慢分析清楚,有空我将做一做这方面的笔记,并分享之。
参考资料
《js执行机制》 https://www.jianshu.com/p/1368d375aa66