前言
对于前端人来说,事件轮询是个老生常谈的话题了,但我在网上查阅相关资料的时候,总觉得有些概念讲述地比较繁琐,如果对这些概念不熟悉,解题思路就会变得很乱;
而这篇文章主要任务,就是帮助大家在短时间内理解事件轮询,以及快速解答事件轮询相关的面试题,主打一个简洁+清晰~
基本概念
在看案例之前我们要先了解几个基本概念,这篇文章中我会简单阐述一下解题相关的逻辑,至于更深层的原理,有很多文章写的已经很详细了,我就不过多阐述啦。
-
单线程
js是单线程的,简单来说,就是代码只能一行一行跑,它绝对不会在打印1的同时打印2,不管碰到多么复杂的操作,它也只能一次执行一个任务,无非是这些任务有先后顺序罢了,所以看到复杂的嵌套的时候不用慌,总会慢慢理清的( ^▽^ )。
-
主线程
主线程更像是程序运行的大脑,它规定了执行栈里要执行什么任务。
-
执行栈
执行栈就是用来执行代码的,比如我看到
setInterval(()=>{console.log('hello,world')})
那么js会先把setInterval()放入执行栈,执行完之后拿出来,然后再放console.log('hello,world')进去,执行完再拿出来,它就像一个开了一个口的箱子,只能'后进先出'。 -
同步、异步
同步任务就是在主线程上按顺序一个一个执行的任务,只有前一个执行完毕,才能执行下个任务;而异步任务可能比较耗时,比如延时器,ajax请求数据,图片上传等等,如果主线程一直在这里等待,请求到数据才开始渲染dom,网页可能会卡成白屏很久,所以主线程就先把它放在任务队列里,等同步代码执行完之后再来执行任务队列中的任务。
-
宏任务队列与微任务队列
在任务队列中的任务分为宏任务和微任务,如果在同一层级下有微任务和宏任务,优先执行微任务;如果在执行宏任务过程中又碰到了微任务,还是会优先执行微任务代码。
宏任务 | 微任务 |
---|---|
setTimeout | Promise.then |
setInterval | async/await |
I/O | Object.observe |
script | Node.js中 process.nextTick |
UI rendering |
注意:process.nextTick 不能完全当作 JavaScript 中的微任务,process.nextTick 执行顺序早于微任务
- 事件轮询
顾名思义,就是js轮流询问任务队列里面是否还有未执行的任务,它好比一个通讯员,在主线程与任务队列之间来回跑,当主线程碰到异步代码时,会先将其放到任务队列里面,当同步代码执行完之后,主线程会不断地从任务队列中取出任务,然后执行。
这里画了一个流程图方便大家理解
案例分析
- 案例一
我们来小试牛刀,这个例子包含了一个宏任务setTimeout()
一个微任务new Promise().then()
。
js
setTimeout(() => {
// 放入宏任务队列
console.log(1);
}, 0);
new Promise((resolve) => {
// promis对象在创建的时候就同步执行下面代码
console.log(2);
// 执行成功的回调函数,也就是console.log(3),这是一个异步任务,放入微任务队列
resolve();
}).then(() => {
console.log(3);
});
//同步任务
console.log(4);
简单分析一下,我们先按顺序执行代码中的同步任务,打印2,4,然后检索微任务队列,打印3,最后执行宏任务队列中的任务,打印1。
- 案例二
在看到很长的代码的时候不用慌,我们先理清同步代码、宏任务以及微任务,具体的执行顺序我已经在文中标好了。
js
async function async1() {
// 2.直接执行后面的代码,打印async1Start
console.log('async1Start')
//3.遇见await的时候,可以将后面函数中的代码当作同步执行,同时将下面的async1End视为异步代码
await async2()
// 9.继续检索微任务队列,打印async1End
console.log('async1End')
}
async function async2() {
//4.打印async2
console.log('async2')
}
// 1.同步代码,打印scriptStart
console.log('scriptStart')
setTimeout(function () {
// 12.打印最后一个宏任务setTimeout3
console.log('setTimeout3')
}, 3)
setTimeout(function () {
// 11.因为这个延时器时间比较短,就比setTimeout3先进入宏任务队列,打印setTimeout0
console.log('setTimeout0')
}, 0)
//2.同步执行async1
async1()
// 8.nextTick比较特殊,会被放置于微任务队列的队首,所以先打印nextTick
process.nextTick(() => console.log('nextTick'))
new Promise(function (resolve) {
//5.当promise对象创建时,会同步执行其回调函数中的代码,打印promise1
console.log('promise1')
resolve()
//6.同步打印promise2
console.log('promise2')
}).then(function () {
//10.打印微任务promise3
console.log('promise3')
})
//7.打印scriptEnd,到此同步的代码就执行完啦
console.log('scriptEnd')
执行顺序如下:
- 案例三
这里代码比较长,所以同步执行的任务我就不加注释了,异步任务执行顺序我会加在每一行的开头。
js
console.log('scriptStart');
var intervalA = setInterval(() => {
// 5.开始执行宏任务,打印intervalA
console.log('intervalA');
}, 0);
setTimeout(() => {
// 6.执行宏任务timeout,同时清除intervalA,不然的话定时器会一直打印intervalA,完全停不下来
console.log('timeout');
clearInterval(intervalA);
}, 0);
var intervalB = setInterval(() => {
// 这里的任务被提前清除掉了,所以不会执行
console.log('intervalB');
}, 0);
var intervalC = setInterval(() => {
// 7.执行宏任务intervalC
console.log('intervalC');
}, 0);
new Promise((resolve, reject) => {
console.log('promise');
resolve()
console.log('promiseAfterForloop');
}).then(() => {
//1.执行微任务promise1
console.log('promise1');
}).then(() => {
//3.第二次调用then方法,会把当前层级的任务放入微任务队列的队尾,所以在promise3之后执行
console.log('promise2');
//4.清除宏任务intervalB,该宏任务里面的打印intervalB不会再执行了
clearInterval(intervalB);
});
new Promise((resolve, reject) => {
setTimeout(() => {
//8.继续执行宏任务promiseInTimeout,
console.log('promiseInTimeout');
//9.同时产生了一个微任务,这时仍然执行微任务,打印promise4
resolve();
});
console.log('promiseAfterTimeout');
}).then(() => {
console.log('promise4');
}).then(() => {
// 10.这里又碰到新的微任务,将其放到微任务队尾,最后打印promise5然后清除定时器intervalC,防止其一直执行
console.log('promise5');
clearInterval(intervalC);
});
Promise.resolve().then(() => {
//2.执行微任务promise3
console.log('promise3');
});
console.log('scriptEnd');
下面是执行顺序的流程图,可以把代码复制到电脑上跑一遍试试。
写在最后
判断同步、异步代码的执行顺序并不难,解决这类问题需要有自己的思考和理解;
我个人建议是先了解其大概运行逻辑,把握宏观的规律,能够解决大部分面试题之后,再去网上搜索更深层原理,这样也许能够让学习事半功倍!
如果各位道友有什么好的idea,欢迎在评论区留言~ ๑乛◡乛๑