事件轮询(实战篇)

前言

对于前端人来说,事件轮询是个老生常谈的话题了,但我在网上查阅相关资料的时候,总觉得有些概念讲述地比较繁琐,如果对这些概念不熟悉,解题思路就会变得很乱;

而这篇文章主要任务,就是帮助大家在短时间内理解事件轮询,以及快速解答事件轮询相关的面试题,主打一个简洁+清晰~

基本概念

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

  • 单线程

    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')

执行顺序如下:

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

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

graph LR 同步任务(同步任务) scriptStart-->promise --> promiseAfterForloop --> promiseAfterTimeout --> scriptEnd 异步任务(微任务) promise1 -->promise3 -->promise2 -->intervalA -->timeout -->intervalC 异步(宏任务) promiseInTimeout--> promise4 -->promise5

写在最后

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

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

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

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing6 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试