炒冷饭系列2:来看看面试题中的Javascript事件循环机制!

1,045 阅读12分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

前言

上一篇炒冷饭系列1:一道字节面试题引出的this指向问题开启了炒冷饭系列,想必很多人不知道炒冷饭的真正含义,那这里就引用一下百度百科的解释。

炒冷饭是一个网络流行语,意思是比喻重复已经说过的话或做过的事,没有新的内容。

逛掘金的都知道,平台有很多文章的主题都是写了又写,在一些用户看来就是一直在冷饭热炒的感觉;感觉没有其他内容主题可以写了,其实不然,冷饭热炒也是一门学问,毕竟每个人对每个事物的认知都是不一样的,你有你的见解我有我的看法,只是在别人写的时候你对其还没有一定的认知罢了。

所以我就干脆开启一个炒冷饭系列,但是此冷饭非彼冷饭,我的冷饭取材于面试或者工作中遇到的一些自己掌握不牢的知识点,而不是包罗万象地介绍全部,其实就是一个视自身掌握情况来决定是否冷饭热炒的系列。

背景

同样,这次还是由一道字节的面试引出要介绍的主题,还是上篇文章说的,真的是准备不足而不是别人问得深入、基础。所以再次提醒面试大厂一定要好好准备,不然真的机会面茫啊。题目还是一道代码题,要求你说出打印什么,为什么?

setTimeout(() => {
    console.log(1);
}, 0)
new Promise((resolve) => {
    console.log(2);
    for(let i = 0;i<10000;i++){
        if(i === 9999){
            resolve();
        }
    }
    console.log(3);
}).then(() => {
    console.log(4);
})
console.log(5);

题目就是这样的,其实真的不难,你可以试着去分析一下,如果觉得拿不准结果,那就耐心看完此文之后再来回看,相信那时你应该就能十拿九稳了。接下来就由这道面试引出这篇文章的主题:Js的事件循环机制,如果你很了解这个主题那就选择略过,否则就一起往下看看,这是面试题必考的点!

Javascript事件循环

一、概念

众所周知,为了与浏览器进行交互,Javascript是一门非阻塞单线程的脚本语言。怎么去理解?

  • 先来说说为什么是单线程?

DOM操作中,如果有一个添加节点线程和一个删除节点的线程,浏览器并不知道以哪个为准,所以只能选择一个线程来执行代码,从而防止冲突。

  • 再来说说为什么是非阻塞?

单线程就意味着任务需要排队,按顺序执行。如果某一任务很耗时,那后面的任务不得不排队等待,所以为了避免这种阻塞,就需要一种非阻塞机制。这种非阻塞机制就是异步机制,即需要等待的任务不会阻塞主线程中同步任务的执行。

既然主要的原因知道了,那就接着说说一些主要的概念然后再介绍具体的事件循环执行问题。

1.1 浏览器执行线程

Js是单线程的脚本语言,但是浏览器是多进程的。浏览器的每一个tab标签页都代表一个独立的进程,其中浏览器渲染进程也只属于浏览器多进程中的其中一种,其主要负责页面渲染,脚本执行,事件处理等。

浏览器进程还包含有以下主要线程:GUI渲染线程JS引擎线程事件触发线程定时器触发线程HTTP请求线程等。

image.png

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

(注意:一个进程挂掉不会影响其他进程,但是一个线程挂掉将导致整个进程挂掉)

主线程指的是JS引擎执行的线程。这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。

工作线程这个线程可能存在于浏览器或JS引擎内,与主线程是分开的;处理文件读取、网络请求等异步事件。

1.2 同步任务和异步任务

同步任务指的是在主线程上排队执行的那些任务。只有前一个任务执行完毕,才能执行后一个任务。

异步任务指的是由JavaScript委托给宿主环境(浏览器或Node.js)进行执行的任务。当异步任务执行完成后,会通知JavaScript主线程执行异步任务的回调函数。

举个例子:一个人吃完饭后看手机,看手机后聊八卦,这样按着顺序一步步来,等上一件事完了之后再执行后面事情的就是同步方式;而一个人在吃饭的同时,可以看手机和聊八卦,这样能做多件事情的就是异步方式。

1.3 任务队列

任务队列主要分两种:宏任务微任务

宏任务:每次执行栈执行的代码。常见的宏任务主要包含:script(整体代码)、setTimeout、setInterval、I/O、异步Ajax、setImmediate(Node.js环境)等。

微任务:当前宏任务执行结束后立即执行的任务。常见的微任务主要包含:Promise.then(catch、finally)、MutaionObserver、process.nextTick(Node.js环境)等。

好了,到这里事件循环机制里面相关的概念已在上面列出解释了。其实通过上面的介绍可以得到下面这样一个图示:

image.png

二、事件循环机制

执行流程

image.png

任务进入执行栈,就会根据任务类型判断是同步任务还是异步任务,然后分别进入不同的执行环境,同步任务进入主线程,异步任务进入任务队列。主线程内的任务执行完毕后,回去任务队列读取对应的任务,推入主线程执行。上述过程不断重复就是事件循环

任务队列执行顺序

image.png

每一个宏任务执行完之后,都会检查是否存在待执行的微任务;如果有,则执行完所有微任务后再继续执行下一个宏任务;反之没有就直接执行下一个宏任务。

根据上面图示可以得到事件循环机制的关键步骤,如下:

  1. 执行一个宏任务(栈中没有就从任务队列中读取);
  2. 执行过程中若遇到微任务,就将它添加到微任务的任务队列中;
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行);
  4. 当前宏任务执行完毕,开始下一个宏任务(从任务队列中获取);
  5. 这样往复循环。

好啦,JS事件循环的相关概念和流程顺序就介绍完了。掌握概念之后,来看一些例子加深一下→

三、示例

回到一开始提到面试题,那就看看它的执行结果是怎样的,分析如下:

  1. 遇到setTimeout,属于新的宏任务,留着后面执行
  2. 遇到new Promise,这个是直接执行的,打印2 3
  3. Promisethen属于微任务,放在微任务队列
  4. 遇到console.log(5),直接打印5
  5. 好了本轮宏任务执行完毕,去微任务列表查看是否有微任务,发现Promise.then的回调,执行打印4
  6. 当一次宏任务执行完,再去执行新的宏任务,里面就剩一个定时器任务,执行打印1
  7. 最后结果为:2 3 5 4 1

下面来一个例子试试水:

console.log(1);
setTimeout(() => {
    console.log(2);
    Promise.resolve().then(() => {
        console.log(3)
    });
}, 0);
new Promise((resolve, reject) => {
    console.log(4);
    setTimeout(() => {
        console.log(5);
        resolve(6);
    }, 0);
}).then(res => {
    console.log(7);
    setTimeout(() => {
        console.log(res);
    }, 0);
});

分析过程,如下:

  1. 代码开始主任务中的第一轮宏任务,遇到console.log(1),直接打印1
  2. 遇到setTimout定时器,它是新的宏任务,先放着不执行
  3. 遇到new Promise,直接执行,打印4
  4. 又遇到定时器,放入宏任务队列中,先不执行
  5. 第一轮宏任务就执行完了,且没有微任务。问:Promise后的.then是第一轮宏任务的微任务么?不是!因为resolve都没有执行,promise的状态还是pending,故就不是第一轮的微任务
  6. 开始下一轮宏任务,执行第一个setTimeout,打印2,第二轮宏任务结束
  7. 遇到第一个setTimeout内的promise.then,开始执行微任务,直接打印3,第二轮宏任务结束
  8. 接着开始第二个setTimeout宏任务,打印5resolve()在这里执行了
  9. 遇到Promise.then放入微任务队列,执行微任务打印7,遇到第三个setTimeout,放入宏任务队列
  10. 又开始第三个setTimeout宏任务,打印6
  11. 最后结果为:1 4 2 3 5 7 6

下面来一个经典的例子:

async function async1() {
    console.log(1);
    await async2();
    console.log(2);
}
async function async2() {
    console.log(3);
}
console.log(4);
setTimeout(function() {
    console.log(5);
});
async1();
new Promise((resolve) => {
    console.log(6);
    resolve();
}).then(() => {
    console.log(7);
});
console.log(8);

分析过程,如下:

  1. 代码开始执行,async是异步的意思,返回的也是promise对象,但是它没有执行,直接跳过async1async2
  2. 遇到console.log(4),直接打印4
  3. 遇到定时器,它是新的宏任务,先放着不执行
  4. 遇到async1,执行它先打印1;然后遇到await async2()await是等待的意思,后面的代码要先等它执行,执行它打印3;然后阻塞下面的代码需要加入微任务队列记为微1,然后跳出去执行同步代码
  5. 然后遇到new Promise,直接执行,打印6
  6. 遇到promise.then属于微任务,放入微任务列表记为微2
  7. 遇到console.log(8),直接打印8
  8. 然后去执行微任务,微任务队列有微1、微2两个任务,依次执行打印2 7
  9. 上一个宏任务所有事都完了,开始下一个宏任务,执行定时器,打印5
  10. 最后结果为:4 1 3 6 8 2 7 5

将上面的经典改造一下,用最终完全体来结束示例:

async function async1() {
    console.log(1);
    await async2();
    setTimeout(() => {
        console.log(2);
    }, 0)
}
async function async2() {
    console.log(3);
    new Promise((resolve) => {
        console.log(9);
        resolve();
    }).then(res => {
        console.log(10);
    })
}
console.log(4);
setTimeout(() => {
    console.log(5);
});
async1();
new Promise((resolve) => {
    console.log(6);
    resolve();
}).then(() => {
    console.log(7);
});
console.log(8);

分析过程,如下:

  1. 代码开始执行
  2. 遇到console.log(4),直接打印4
  3. 遇到第一个setTimeout定时器,它是新的宏任务,先放着不执行
  4. 遇到async1,执行它先打印1;然后遇到await async2(),执行打印3;然后遇到new Promise直接打印9resolve()执行,promise.then放入微任务队列微1;await阻塞的代码也需要加入微任务队列记为微2;然后跳出去执行同步代码
  5. 遇到new Promise,直接执行,打印6
  6. 遇到promise.then属于微任务,放入微任务列表记为微3
  7. 遇到console.log(8),直接打印8
  8. 然后去执行微任务,微任务队列有微1、微2、微3三个任务,依次执行,先打印10,遇到微2是setTimeout定时器,所以放入宏任务队列;执行微3,打印7
  9. 上一个宏任务所有事都完了,开始下一个宏任务,执行定时器,打印5;没有微任务,结束第二轮宏任务
  10. 开始第二个宏任务,执行打印2,到此全部结束
  11. 最后结果为:4 1 3 9 6 8 10 7 5 2

到这里,JS事件循环机制示例相关的题和分析过程就介绍完了,想必通过上面的介绍你一定能对JS的事件循环机制有更深的理解,相信以后遇见其他类似的面试题也能迎刃而解了。冲冲冲!→

最后,xdm看文至此,点个赞👍再走哦,3Q^_^

往期精彩文章

后语

伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注在走呗^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。