微任务、宏任务与Event-loop

182 阅读9分钟

需要知道两点

  • Javascript是单线程的语言
  • 事件循环(Event Loop)是Javascript事件循环机制。

JS是单线程

JS是单线程的,就是js的代码只能在一个线程上运行,也就是说,js同时只能执行一个js任务。既然js是单线程的,那么setTimeout、onclick回调是怎么实现的呢?因为浏览器或(宿主环境)是多线程的,就是说浏览器多搞了几个其他线程去辅助JS线程的运行。

浏览器线程

进程、线程的概念,可以这样说,浏览器的tab标签页就是一个进程,一个页面中又包含很多个线程。 浏览器的线程:

  1. GUI渲染线程(可理解为html css渲染的线程) 负责渲染浏览器界面HTML元素,当界面需要重绘或者由于某种操作引发回流,该线程就会执行。

  2. JS引擎线程 JS内核,负责处理JS脚本主程序。一直等待着任务队列的到来,然后解析js脚本,运行代码。也就是浏览器中的一个tab标签页中只会有一个JS线程在运行JS程序。

GUI渲染线程和JS引擎线程是互斥的,如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞

3.定时器触发线程

  • 定时器setInterval和setTimeout所在线程
  • 浏览器定时计数器并不是由js引起计数,因为js引擎是单线程,处于阻塞线程状态就会影响计时的准确,通过单独线程来计时更合理。
  • 定时器到时间后就会把回调函数放到任务队列中,等待js引擎处理。

4.浏览器事件线程

  • 用来控制时间,js引擎忙不过来 ,需要浏览器另开线程协助。
  • 当JS引擎执行代码块如鼠标点击等事件,会将对应的任务添加到事件触发线程中。
  • 当对应的事件符合触发条件被触发时,该线程就会把事件对应的函数添加到待处理任务队列的队尾
  • 由于JS的单线程关系,所以这些待处理任务队列中的事件都得排队等待js引擎处理。

5.http请求线程

在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的任务队列中等待处理

js是单线程的语言,所有的任务都在一个线程上面执行,任务(我们写的每一行代码或者代码块)排成一队,形成队列,也就是所谓的任务队列。

排在前面的任务先执行,排在后面的任务等前面的任务执行完成才能执行,这样很容易造成阻塞,所以引入了事件循环(EventLoop)机制。

事件循环(EventLoop)机制

任务队列、同步任务、异步任务、宏任务、微任务这几个概念

同步任务:js一行一行进入任务队列,排队执行,先进先出。

    const a = 1;
    console.log(a);
    console.log(2);
    console.log(3);

异步任务:

    console.log('1');
    setTimeout(() => {
        console.log(2);
    },1000);
    console.log(3);

通过上面例子,我们知道什么是同步,什么是异步。
浏览器执行代码的过程中,JS引擎会将代码进行分类,分别分到宏任务(macrotask)和微任务(microtask).

常见的宏任务:

整体代码script、常见的定时器(setTimeout,setInterval)、nodejs的setImmediate、MessageChannel(react的fiber用到)、postMessage、网络I/O、用户交互的回调时间、UI渲染事件(DOM解析、布局计算、绘制)等。

常见的微任务

process.nextTick(nodejs)、promise的相关任务、MutationObserver监控dom节点变化。

微任务是宏任务的组成部分,微任务与宏任务是包含关系,并非前后并列,宏任务包含微任务,每一个宏任务都可以创建自己的一个微任务队列,如果要谈论微任务,必须要指出它属于哪个宏任务才有意义。

事件循环机制 机制:

  • 浏览器的事件循环,是在渲染进程中的;
  • 执行一个宏任务,栈中没有就从事件队列中获取;
  • 执行过程中如果遇到微任务,就添加到微任务队列中;
  • 当前这个宏任务执行完毕后,立即执行当前微任务队列的所有微任务;
  • 当前宏任务执行完毕,GUI线程接管渲染;
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务;

简单来说就是:先执行一个宏任务,在执行它对应的所有微任务

宏任务执行例子

    setTimeout(()=>{
        console.log(1);
    },0)
    setTimeout(()=>{
        console.log(2);
    },0)
    console.log(3); // 执行顺序 3 --> 1 --> 2
  • 浏览器开始执行代码时启动了第一个宏任务并开始执行。
  • 在执行宏任务1途中遇到了第一个定时器,浏览器便会启动一个新线程去跑定时器的逻辑,而当前的js线程不会停直接跳过定时器继续往下执行。当定时器的那条线程跑完后,它的回调函数被添加到js线程的宏任务队列中等待,这就是宏任务2.
  • js线程又遇到定时器再开启一个线程跑定时器的逻辑,js线程跳过这段继续向下执行,当定时器线程跑完后怒,回调函数被添加到宏任务队列等待,这就形成了宏任务3,宏任务3在宏任务2的后面。
  • js线程走到最后输出3,此时宏任务1就结束了,浏览器此刻就会去宏任务队列中寻找,排在最前面的是宏任务2,执行输出1,宏任务2结束又执行宏任务3输出2.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
    <script>
        $(function(){
            $.ajax({
                url: 'http://baidu.com',
                success(res){
                    console.log('success', res);
                },
                error(err){
                    console.log('err', err);
                },
                complete(res){
                    console.log('complete', res);
                }
            });
            setTimeout(() => {
                console.log('1');
            }, 0);
            console.log('2');
        });
       
    </script>
</body>
</html> // 2 --> 1 --> ajax的回调
  • 浏览器开始执行代码时启动了第一个宏任务(script)并开始执行.
  • 遇到了ajax就开启了http 请求线程,然后等待服务器处理后,触发回调函数在放到js线程的宏任务队列中
  • 遇到定时器,开启定时器线程,一样等待定时器跑完之后把回调函数放到js线程的宏任务队列中
  • 遇到console.log('2')直接输出
  • 最后哪个线程先触发回调函数,就先放入js宏任务队列中,js线程的事件循环(EventLoop)一直在循环着等待任务的处理

微任务执行例子

    setTimeout(() => { //定时器1
        console.log(1);
    }, 0)
    new Promise((resolve) => {
        resolve();
    }).then(() => { // 宏任务1中的微任务1
        console.log(2)
    })
    console.log(3);  //输出 3 --> 2 --> 1 
  • 浏览器运行启动宏任务1
  • 遇到setTimeout定时器,放到宏任务队列中
  • 碰到promise,将then的回调函数放在微任务队列中等待,线程继续往下
  • 代码跑到最后一行,输出3,这个时候同步代码执行完毕,开始检查当前的宏任务中的微任务队列。
  • 运行微任务队列中的第一个then回调函数输出2,再检查微任务队列,没有发现其他任务。
  • 微任务队列执行完毕,就去宏任务队列,看到定时器1就拿出来执行输出1.

如果对EventLoop还不是很了解

未命名文件.jpg

从上图可知,EventLoop(事件循环)机制,把宏任务形成了一个拥有先后顺序的宏任务队列.每个宏任务中分为同步代码微任务队列.

  • 假设js当前的线程执行宏任务1,先执行宏任务1中的同步代码.
  • 如果碰到Promise或者process.nextTick,就把它们的回调放入当前宏任务1的微任务队列中.
  • 如果碰到setTimeout, setInterval之类就会另外开启线程去跑相应的逻辑,而js线程跳过这段继续往下执行.另起的线程执行完毕后再在当前宏任务1的队列后面创建新的宏任务并将定时器的回调函数放入其中宏任务队列中.
  • 同步代码执行完,开始执行当前宏任务的微任务队列,直到微任务队列的所有任务都执行完.
  • 微任务队列的所有任务执行完毕,宏任务1再看没有其他代码了,当前的宏任务循环结束后,js线程开始执行下一个宏任务,直到所有宏任务执行完毕.如此整体便构成了事件循环(EventLoop)机制.

promise相关的面试题

    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);
    }).then(() => {
        console.log("8");
    }).then(() => { 
        console.log("9");
    }); 
  • 宏任务1,4;微任务:空
  • 宏任务 2;微任务:3
  • 宏任务 5;微任务:resolve了,把对应的 微任务加入 7、8、9
  • 输出 6 // 1 4 2 3 5 7 8 9 6

扩展知识

  1. dom操作是宏任务还是微任务
 console.log(1);
 document.getElementById("div").style.color = "red";
 console.log(2);

当上面代码执行到第三行时,控制台输出了1并且页面已经完成了重绘,div的颜色变成了红色.dom操作它既不是宏任务也不是微任务,它应该归于同步执行的范畴.

  1. requestAnimationFrame属于宏任务还是微任务 requestAnimationFrame属于异步执行的方法,既不是宏任务也不属于微任务,按照MDN中定义
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用
指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

requestAnimationFrame是GUI渲染之前执行,但在微服务之后,不过requestAnimationFrame不一定会在当前帧必须执行,由浏览器根据当前的策略自行决定在哪一帧执行。

参考文献: 搞清事件循环、宏任务、微任务