大厂面试高频:一文彻底掌握EventLoop事件循环机制(微任务/宏任务)

本文概述

本文对EventLoop事件循环机制,包括微任务和宏任务,彻底详细的做一个解析,觉得对您有帮助,记得 收藏+关注,欢迎评论区留言~

浏览器是多线程的还是单线程的?

首先我们要知道浏览器是单线程的还是多线程的,答案是多线程的,具体包括:

  1. GUI渲染线程:渲染和解析页面
  2. JS引擎线程:渲染和解析JS 「浏览器只分配一个线程去解析JS,所以JS是单线程的」
  3. 定时器监听线程
  4. 事件监听线程
  5. HTTP网络请求线程「同源下,浏览器最多同时分配5~7个HTTP线程」

JS是“单线程 ”运行的还是多线程的?

答案:JS是“单线程 ”运行的,所以其中大部分代码都是“同步”的(例如:循环…),拿我们举个例子说明JS是不是单线程的:
我们先测一下下面这个循环大概需要多长时间:以及下面代码输出结果

console.time('AA');
for (let i = 0; i < 99999999;i++) { }
console.timeEnd('AA');
console.log('OK');

上述循环需要时间是70ms,根据浏览器性能,需要时间不尽相同,也就是说70ms后输出OK,假如我们去掉i++,那下面代码输出的结果又是怎样的呢?

console.time('AA');
for (let i = 0; i < 99999999;) { }
console.timeEnd('AA');
console.log('OK');

控制台执行过后,我们发现,这个OK永远不会输出,程序进入了死循环,所以,我们发现循环是同步的,即:js是单线程运行的,所以在JS中千万不要写“死循环”、“死递归”等操作,这些操作会一直占用JS引擎线程,导致后续其他的程序都无法执行!但是,JS中也有部分“异步”操作的代码:

异步微任务:

  1. requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
  2. Promise.then/catch/finally
  3. async/await
  4. queueMicrotask 手动创建一个异步的微任务
  5. MutationObserver 监听当前DOM元素属性值的改变
  6. IntersectionObserver 监听当前DOM元素和可视窗口交叉信息发生改变的时候它的一些状态和一些结果的

异步宏任务:

  1. setTimeout/setInterval
  2. 事件绑定/队列
  3. XMLHttpRequest/Fetch
  4. MessageChannel

JS中的异步操作是什么?

JS中的异步操作是:借用浏览器的多线程机制,再基于EventLoop事件循环机制,实现的单线程异步效果!!

setTimeout(() => { }, 0);是不是立即执行的,为什么?

答案:setTimeout(() => { }, 0);不是立即执行定时器的,而是要等待5-7ms,这个5-7ms是浏览器最快处理时间。

上个练习题,说说运行的结果:

setTimeout(() => {
    console.log(1);
}, 20);
console.log(2);
setTimeout(() => {
    console.log(3);
}, 10);
console.log(4);
for (let i = 0; i < 90000000; i++) {
    // do soming 80ms左右
}
console.log(5);
setTimeout(() => {
    console.log(6);
}, 8);
console.log(7);
setTimeout(() => {
    console.log(8);
}, 15);
console.log(9);

答案: 2 4 5 7 9 3 1 6 8 你算对了吗?没有的话请看下面的详细解析:
首先我们要知道 ## EventLoop事件循环机制,

什么是EventLoop事件循环机制?

  1. 浏览器加载页面,除了开辟堆栈内存外,还创建了两个队列:WebAPI 和EventQueue
  2. WebAPI:任务监听队列,监听异步的任务是否可执行了
  3. EventQueue:事件队列 或 任务队列,所有可执行的异步任务,需要在这里排队等待执行。意思就是设置个定时器,20ms后让它执行,哪怕计时器已经数到20ms了,它也不能立即执行,它需要在EventQueue里面排队等着,等待主线程自上而下把所有同步代码都执行完,它才能再来执行,整个EventLoop事件循环的顺序就是这样的。
  4. 首先:当主线程自上而下执行代码过程中,或者说同步代码执行过程当中,如果遇到“异步”代码,它会把异步任务放到WebAPI中去监听!具体怎么监听?浏览器开辟新的线程去监听是否可以执行,而且,不会阻碍主线程的渲染,它会继续向下执行同步的代码。
  5. 其次:当异步任务被检测为可以执行了,也不会立即去执行,而是把其挪到EventQueue中排队等待执行!而挪进来的,会根据微任务还是宏任务,放在不同的队列中,谁先进来排队的,谁在各自队伍的最前面(就跟食堂打饭一样,谁先进来的谁排在前面)。
  6. 另外需要注意的是:对于定时器来讲,设定一个等待时间,到时间后并不一定会立即去执行
  7. 最后,当“同步代码”(有写人把同步代码叫 同步宏任务 )都“执行完毕”,主线程空闲下来,此时会去EventQueue中把正在排队的可执行的异步任务按照顺序取出来执行!{
    在EventQueue中:
    ①异步的微任务优先级比较高,不论其任务是先放入的,还是后放入的,只要有可执行的异步微任务,永远先执行它!(VIP用户)
    ②同样级别的任务,是谁先放入的(谁先到),谁先执行,但是要等同步代码执行完毕!
    ③是要把任务拿到栈中执行,而且是交给主线程去执行,所以,只要这个拿出来的任务没有执行完,也不会再去拿其他的任务!
    }
    那么EventLoop事件循环机制明白了之后,我们再来看这道题 浏览器 是怎么运行的:
    首先,浏览器从上往下执行代码,遇到
setTimeout(() => {
    console.log(1);
}, 20);

因为定时器是异步宏任务,我们把它叫做宏1,我们把它叫 异步宏任务1 ,放到WepAPI任务监听队列中去等待,然后遇到了同步的

console.log(2);

立即输出2,然后再往下执行,遇到了定时器

setTimeout(() => {
    console.log(3);
}, 10);

因为定时器是异步宏任务,我们把它叫做宏2,浏览器会把这个定时器放到WepAPI任务监听队列中去等待,,然后遇到了同步的

console.log(4);

,立即输出4,然后代码继续往下执行,遇到了

for (let i = 0; i < 90000000; i++) {
    // do soming 70ms左右
}s

这个for循环需要70ms左右,当到这里的时候,当过了10ms的时候,宏2已经到时间了,就放到异步宏任务里面去排队等待执行,然后再过了10ms后,for循环还没结束,宏1已经到时间了,宏1再去‘异步宏任务’队列中去排队等待执行,在等上面我们详细讲了for循环是同步的,所以等for循环循环完成70ms后,代码继续往下执行,遇到了同步的

console.log(5);

立即输出5,然后代码继续往下执行,遇到了定时器,

setTimeout(() => {
    console.log(6);
}, 8);

因为定时器是异步宏任务,我们把它叫做宏3,所以放到WebAPI任务监听队列中去等待,然后代码继续往下执行,遇到了同步的

console.log(7);

立即输出7
然后代码继续往下执行,又遇到了定时器

setTimeout(() => {
    console.log(8);
}, 15);

因为定时器是异步宏任务,我们把它叫做宏4,,所以放到WebAPI任务监听队列中去等待,然后代码继续往下执行,,遇到了同步的

console.log(9);

立即输出9
所以,第一轮同步代码都执行完了 分别输出 2 4 5 7 9
下面我们看异步代码:
当代码运行到这里的时候,已经过了70ms 这时宏1 和宏2 已经到时间了,并且再“异步宏任务”列表里面排队等待了,宏2 比宏1 先到时间,所以在输出3 和1
然后再去“异步宏任务”队列中去找,发现还有宏3 和宏4 ,宏3 是8ms到时间,所以宏3先执行输出6,然后在等7ms,宏4也到时间了,然后宏4输出9
上述就是这个案例代码运行的详细解析,下图是上述代码宏观描述:不懂请下面放留言,谢谢!
大厂面试高频:一文彻底掌握EventLoop事件循环机制(微任务/宏任务)_第1张图片

对于EventLoop事件循环机制还有一种情况,我们再来个案例:

let p = new Promise(resolve => {
    resolve(10);
});
p.then(value => {
    console.log('成功:', value);
});
console.log(1);

分析:结果输出1 ,成功:10,你算对了吗?没有看解析:p.then(onfulfilled,onrejected)的时候,已知实例p的状态和值,也不会立即把 onfulfilled/onrejected 执行,而是创建“异步微任务”「具体怎么做?先进入WebAPI中去排队等着,发现状态是成功,则onfulfilled可以被执行,把其再挪至到EventQueue中排队等着」

另外对于EventLoop事件循环机制我们再看一种情况,我们再来个案例:

let p = new Promise(resolve => {
    setTimeout(() => {
        resolve(10);
        console.log(p, 2);
    }, 1000);
});
p.then(value => {
    console.log('成功:', value);
});
console.log(1);

分析:结果输出1 ,Promise实例 和2 , 成功:10 ,你算对了吗?没有看解析: 如果还不知道实例p的状态,则先把onfulfilled/onrejected存储起来「理解为:进入WebAPI去监听,只有知道实例的状态,才可以执行」;resolve/reject执行,立即修改实例的状态和值,也决定了WebAPI中监听的方法(onfulfilled/onrejected)哪一个去执行「挪至到EventQueue中,异步微任务队列」;等待其它同步代码执行完,再拿出来执行!!

上述都了解 ,那么我们再来一个有关async和await的来看看:

const fn = async () => {
    console.log(1);
    return 10;
};
(async function () {
    let result = await fn();
    console.log(2, result);
})();
console.log(3);

分析:输出结果 1 ,3 ,2 10,你算对了吗?没有看解析:遇到await,会立即执行其后面代码,看返回的promise实例是否是成功,如果不是promise实例也会变为promise实例;不论是成功还是失败,会把当前上下文中,await下面代码当做异步的微任务进入到WebAPI中去监听,只有后面实例的状态是成功的,才可以执行,可执行则进入到EventQueue中排队等着,当同步代码都执行完之后,再把它拿出来执行

你到底有没有彻底了解EventLoop事件循环机制,我们看一个测试题吧(字节的一个面试题):说出下面程序执行结果

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end'); 

答案是
script start ,async1 start , async2,promise1,script end,async1 end,promise2,setTimeout,你算对了吗?没有看详细解析:首先代码从上往下执行,遇到同步代码

console.log('script start');

立即输出script start,然后继续往下执行,遇到定时器

setTimeout(function() {
    console.log('setTimeout');
}, 0)

,因为定时器是异步宏任务,我们把这个定时器叫做宏1,上面说了,宏任务的执行必须等到所以同步代码都执行完成,并且没有“异步微任务”等待的情况下,才能执行,很显然,同步代码还没有执行完成,所以,我把宏1放到WebAPI任务监听队列中去等待,然后,代码继续往下执行,遇到了

async1();

我们上面说了,代码执行遇到async 其后面的代码立即执行,所以,输出‘async1 start’,然后代码继续往下执行,遇到await,会立即执行其后面代码,看返回的promise实例是否是成功,如果不是promise实例也会变为promise实例;不论是成功还是失败,会把当前上下文中,await下面代码当做异步的微任务进入到WebAPI中去监听,只有后面实例的状态是成功的,才可以执行,可执行则进入到EventQueue中排队等着,当同步代码都执行完之后,再把它拿出来执行所以,这里遇到了await,在当前上下文中,不管成功还是失败,await下面代码console.log(‘async1 end’)当作异步微任务(我们叫微1)进入到WebAPI中去监听,然后会立即把async2()函数立即执行,输出‘async2’,返回状态是成功,值是undefined的实例,,函数执行返回状态是成功,值是undefined,那么‘微1’就可以执行了,然后代码继续往下执行,接下来把‘微1’挪到‘异步微任务’队列中排队等着,然后代码继续往下走,遇到了

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

上面我们说了,代码执行遇到 new Promise会立即执行,根据执行结果是成功还是失败来决定.then()中onfulfilled/onrejected哪个执行,那么就先把.then()当作异步微任务(我们叫‘微2’)放到WebAPI任务监听队列中去排队,很显然new Promise执行先输出‘promise1’,返回状态是成功,值是undefined的实例,‘微2’就可以执行了,挪到‘异步微任务’队列中放到‘微1’后面去排队等,然后代码继续往下执行,遇到了同步的

console.log('script end'); 

立即输出 ‘script end’,到这里,同步代码都执行完了,下面我们看异步代码
首先我们去‘异步微任务’中去找,有‘微1 和 微2’,谁下来的先执行谁,微1执行输出 ‘async1 end ’,然后‘微2’执行输出 promise2,当所有的异步微任务都执行完,异步用任务才执行,这是‘宏1’时间到了,别挪到了‘异步宏任务里面排队等着’,然后输出setTimeout,下面是用画图具体说明:有不明白的下方评论区留言
大厂面试高频:一文彻底掌握EventLoop事件循环机制(微任务/宏任务)_第2张图片

下面我们再送个阿里的没面试题,说出下面程序执行结果,看看你学会了?

let body = document.body;
body.addEventListener('click', function () {
    Promise.resolve().then(() => {
        console.log(1);
    });
    console.log(2);
});
body.addEventListener('click', function () {
    Promise.resolve().then(() => {
        console.log(3);
    });
    console.log(4);
});

答案:2 1 4 3 如果你没做对,看下面的详细解析吧;
首先进来有两个队列 WebAPI队列和EventQueue队列,EventQueue队列中又分为‘异步微任务’和‘异步宏任务’,接下来代码执行,首先获取body,然后点击body,上面我们说 事件绑定属于异步宏任务,我们把

let body = document.body;
body.addEventListener('click', function () {
    Promise.resolve().then(() => {
        console.log(1);
    });
    console.log(2);
});

叫做‘宏1’,然后又做了个事件绑定

body.addEventListener('click', function () {
    Promise.resolve().then(() => {
        console.log(3);
    });
    console.log(4);
});

我们叫‘宏1’,把‘宏1’和‘宏2’放到WebAPI中监听,当点击body的时候才出发执行,所以body只要不点击,‘宏1’和‘宏2都不执行,假设我们点击 了body ,‘宏1’和‘宏2’都可以执行了,‘宏1’先做的事件绑定,所以‘宏1’先执行,‘宏1’执行过程当中遇到.then()想都不要想直接把.then后面的创建异步微任务,我们叫‘微1’,放到WebAPI中监听,‘微1’获取的实例状态是成功的 也不会立即执行,需要挪到‘异步微任务’队列中去排队,代码继续往下走,遇到

console.log(2);

立即输出2,所以 ‘宏1’执行,创建微1,并且输出2
宏1执行完之后,再往下找,这是有个问题是已经有了微1 和宏2,上面说了 异步微任务的优先级高于异步宏任务的优先级,所以微1 比宏2 先执行,微1 执行输出1,然后宏2 再执行,遇到.then,那么.then后面的被创建异步微任务,我们叫微2,Promise().resolve()执行返回状态是成功,值是undefied的实例,那么代码继续往下执行遇到同步的

console.log(3);

立即输出3,然后还剩一个微2,微2执行输出3,下面是画图宏观说明:
大厂面试高频:一文彻底掌握EventLoop事件循环机制(微任务/宏任务)_第3张图片
小伙伴们,你有没有彻底掌握EventLoop事件循环机制 以及微任务和宏任务,欢迎下面评论区留言,我看到会第一时间给您回复,如果觉得这个文章对您有帮助,收藏+关注吧,面试之前打开看看

你可能感兴趣的:(javascript,前端,前端面试题,javascript,前端,面试)