likes
comments
collection
share

事件轮询(实战篇)

作者站长头像
站长
· 阅读数 2

前言

对于前端人来说,事件轮询是个老生常谈的话题了,但我在网上查阅相关资料的时候,总觉得有些概念讲述地比较繁琐,如果对这些概念不熟悉,解题思路就会变得很乱; 而这篇文章主要任务,就是帮助大家在短时间内理解事件轮询,以及快速解答事件轮询相关的面试题,主打一个简洁+清晰~

基本概念

在看案例之前我们要先了解几个基本概念,这篇文章中我会简单阐述一下解题相关的逻辑,至于更深层的原理,有很多文章写的已经很详细了,我就不过多阐述啦。

  • 单线程 js是单线程的,简单来说,就是代码只能一行一行跑,它绝对不会在打印1的同时打印2,不管碰到多么复杂的操作,它也只能一次执行一个任务,无非是这些任务有先后顺序罢了,所以看到复杂的嵌套的时候不用慌,总会慢慢理清的( ^▽^ )。

  • 主线程 主线程更像是程序运行的大脑,它规定了执行栈里要执行什么任务。

  • 执行栈 执行栈就是用来执行代码的,比如我看到setInterval(()=>{console.log('hello,world')}) 那么js会先把setInterval()放入执行栈,执行完之后拿出来,然后再放console.log('hello,world')进去,执行完再拿出来,它就像一个开了一个口的箱子,只能‘后进先出’。

  • 同步、异步 同步任务就是在主线程上按顺序一个一个执行的任务,只有前一个执行完毕,才能执行下个任务;而异步任务可能比较耗时,比如延时器,ajax请求数据,图片上传等等,如果主线程一直在这里等待,请求到数据才开始渲染dom,网页可能会卡成白屏很久,所以主线程就先把它放在任务队列里,等同步代码执行完之后再来执行任务队列中的任务。

  • 宏任务队列与微任务队列 在任务队列中的任务分为宏任务和微任务,如果在同一层级下有微任务和宏任务,优先执行微任务;如果在执行宏任务过程中又碰到了微任务,还是会优先执行微任务代码。

宏任务微任务
setTimeoutPromise.then
setIntervalasync/await
I/OObject.observe
scriptNode.js中process.nextTick
UI rendering

注意:process.nextTick 不能完全当作 JavaScript 中的微任务,process.nextTick 执行顺序早于微任务

  • 事件轮询 顾名思义,就是js轮流询问任务队列里面是否还有未执行的任务,它好比一个通讯员,在主线程与任务队列之间来回跑,当主线程碰到异步代码时,会先将其放到任务队列里面,当同步代码执行完之后,主线程会不断地从任务队列中取出任务,然后执行。

这里画了一个流程图方便大家理解

事件轮询(实战篇)

案例分析

  • 案例一 我们来小试牛刀,这个例子包含了一个宏任务setTimeout() 一个微任务new Promise().then()
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。

  • 案例二 在看到很长的代码的时候不用慌,我们先理清同步代码、宏任务以及微任务,具体的执行顺序我已经在文中标好了。
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')

执行顺序如下:

同步任务
scriptStart
async1Start
async2
promise1
promise2
scriptEnd
微任务
nextTick
async1End
promise3
宏任务
setTimeout0
setTimeout3
  • 案例三 这里代码比较长,所以同步执行的任务我就不加注释了,异步任务执行顺序我会加在每一行的开头。
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');

下面是执行顺序的流程图,可以把代码复制到电脑上跑一遍试试。

同步任务
scriptStart
promise
promiseAfterForloop
promiseAfterTimeout
scriptEnd
微任务
promise1
promise3
promise2
intervalA
timeout
intervalC
宏任务
promiseInTimeout
promise4
promise5

写在最后

判断同步、异步代码的执行顺序并不难,解决这类问题需要有自己的思考和理解;

我个人建议是先了解其大概运行逻辑,把握宏观的规律,能够解决大部分面试题之后,再去网上搜索更深层原理,这样也许能够让学习事半功倍!

如果各位道友有什么好的idea,欢迎在评论区留言~ ๑乛◡乛๑