• 浏览器事件循环


    学习JS,Event Loop是一个绕不开的点。JS 的异步执行逻辑依赖 Event Loop 机制,但是这套机制却是定义在 HTML 标准中的。因为 Event Loop 本身并不属于 ES 层面的功能,是宿主环境给脚本提供了这一机制,才让脚步有了异步执行的能力。根据JS宿主环境的不同,可以分为浏览器的事件循环和node的事件循环,两者之间会有一些不同。这里我们只讲浏览器的事件循环。
    一、为什么要有事件循环

    浏览器运行过程中会同时面对多种任务,用户交互事件(鼠标、键盘)、网络请求、页面渲染等。而这些任务不能是无序的,必须有个先来后到,浏览器内部需要一套预定的逻辑来有序处理这些任务,因此浏览器事件循环诞生了,再次强调,是浏览器事件循环,不是javascript事件循环,js只是浏览器事件循环的参与者。

    二、事件循环是什么

    浏览器把任务区分成了 宏任务 和 微任务 或者叫 外部任务 和 内部任务 ,内部任务可以理解为js内部处理的任务,外部任务可以认为是浏览器处理的任务。

    外部队列/宏任务队列(Task Queue)回调

    也可以叫宏任务队列,浏览器中的外部事件源包含以下几种:

    dom操作(页面渲染)、用户交互(鼠标、键盘)、网络请求(Ajax等)、History API操作(history.back、history.go…)、定时器(setTimeout)
    这些外部事件源可能很多,为了方便浏览器厂商优化,HTML标准中明确指出一个事件循环有一个或多个外部队列,而每一个外部事件源都有一个对应的外部队列。不同的时间源之间可以有不同的优先级(例如在网络时间和用户交互之间,浏览器可以优先处理鼠标行为,从而让用户感觉更加流畅)。

    内部队列/微任务队列(Microtask Queue)回调

    也可以叫微任务队列,指的就是javascript语言内部的事件队列,在HTML标准中,并没有明确规定这个队列的事件源,通常认为有以下几种:

    Promise的成功(.then)与失败(.catch)
    MutationObserver
    Object.observe(已废弃)
    以上三种除了第一个,其他两个可以认为没有,实际上我们js中能够使用的就只有promise。
    每一个事件循环,从外部任务队列中拿出一个来执行,执行完一个外部任务后立即执行内部任务队列中所有内部任务(清空),然后浏览器执行一次渲染,然后再次循环。
    下面展示两段代码
    1.

    // 以下代码会得到什么样的输出结果?
    console.log('1');
    setTimeout(function() { 
      console.log('2'); 
      Promise.resolve().then(function() {
    		console.log('3'); 
      });
    }, 0);
    Promise.resolve().then(function() { 
      console.log('4');
    }).then(function() { 
      console.log('5');
    });
    console.log('6');
    
    //执行顺序:164523
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.由于执行当前js代码这个任务是一个宏任务,因此首先输出的是"1",
    2.继续执行遇到setTimeou,由于setTimeout是一个外部事件源,它内部的代码会被push到TaskQueue中等待下一次事件循环再执行,
    3.当执行到promise的 then 或 catche的时候会将他们按顺序追加到本轮事件循环的末尾,
    再继续往下执行输出6,宏任务完成后清空微任务队列中的任务,继而输出4、5
    4.如果有的话,执行渲染任务后,本次事件循环结束
    5.开始执行下一个宏任务,也就是第一个setTimeout中的代码块,输出2,然后将promise.then添加到本轮循环末尾
    6.清空微任务,输出3

    2.

      console.log(1)
      setTimeout(() => console.log(2), 200)
      setTimeout(() => {
        console.log(3)
        setTimeout(() => console.log(4), 50)
      }, 100)
      new Promise(resolve => {
        console.log(5)
        resolve()
        }).then(() => {
        console.log(6)
      })
      setTimeout(() => {
        new Promise(resolve => {
          console.log(7)
          resolve()
        }).then(() => {
          console.log(8)
        })
      })
      console.log(9)
    //执行顺序:159678342
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    1、首先,这段代码会作为宏任务而被添加到宏任务队列里面。这时候,宏任务队列只有一个任务,主线程为空,执行这个任务。
    2、console.log(1)入栈,打印1,执行完毕,出栈
    3、setTimeout入栈,挂起,等待200ms后将回调函数添加到宏任务队列
    4、setTimeout入栈,挂起,等待100ms后将回调函数添加到宏任务队列
    5、promise入栈,打印5,然后将then添加到微任务队列
    6、setTimeout入栈,挂起,等待4ms后将回调函数加入到宏任务队列。这里提一嘴,HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒。
    7、console.log(9)入栈,打印9,执行完毕,出栈
    8、主线程为空,检查微任务队列是否有函数,发现有,入栈执行,打印6
    到这里,不考虑其他的渲染什么的,本次事件循环就结束了,这时候的打印结果就是1 5 9 6 。
    主线程又空了,事件轮询模块会一直轮询宏任务队列是否有任务可以执行。而很明显,在三个挂起的setTimeout里面,第六步的setTimeout是最快将
    回调函数添加到宏任务队列的。这时候就可以进行下一个事件循环了。
    9、循环开始,首先会打印7,然后将then添加到微任务队列,因为本次循环没有其他事做了,接着就执行微任务队列里的任务,打印8
    10、继续轮询,而第四步比第三步更快,先打印3,又发现了setTimeout,继续挂起
    而这时候第十步和地三步的哪个更快呢?这就要考虑在第三步到第十步之前有没有耗时的任务了。我们这里并没有什么耗时任务,所以第十步依然会比
    第三步先执行。打印4,最后打印2 。整个代码就执行完毕了。

    总结:微任务队列会在当前事件循环结束之前清空,而宏任务只有在下一次事件循环的时候才会被执行。自然微任务就会比宏任务优先了。这里说的优先也只是在一个事件循环内。

    下面是关于宏观队列和微观队列的一些理解

    每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行,也就是优先级比宏任务高,且与微任务所处的代码位置无关

    setTimeout(()=>{
        console.log('0');
    },0);
    new Promise((resolve,reject)=>{
        console.log('1');
        resolve();
    }).then(()=>{
        console.log('2');
        new Promise((resolve,reject)=>{
            console.log('3');
            resolve();
        }).then(()=>{
            console.log('4');
        }).then(()=>{
            console.log('5');
        })
    }).then(()=>{
        console.log('6');
    })
    new Promise((resolve,reject)=>{
        console.log('7');
        resolve();
    }).then(()=>{
        console.log('8');
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    先看同步,再看回调
    开头一个定时器,将0压入宏队列
    new Promise中executor同步执行,直接输出1
    接着执行rresolve(),状态改变,调用then(),将成功回调函数压入微队列(2)
    由于输出2这里还没执行,可以先不管其下的new Promise,此时第一个then()还没执行结束因此第二个then()也还没开始执行
    然后执行下一个new Promise,直接将7输出,然后立马执行resolve(),改变状态后将then()里面成功的回调压入微队列(8)
    此时初始化代码全部执行完毕

    输出:1 7
    宏队列:[0]
    微队列:[2,8]
    
    • 1
    • 2
    • 3

    执行微队列中的回调
    输出2,console.log(‘2’);执行完后new Promise,直接输出3,立马执行resolve()改变状态后then()成功回调压入微队列(4)。由于此时4还未执行,因此其后输出5的回调不能压入微队列,而是放入缓存,由于状态改变,其后的then()已经执行完了,因此将外层下一个then的回调压入微队列(6)

    输出:1 7 2 3
    宏队列:[0]
    微队列:[8,4,6]
    
    • 1
    • 2
    • 3

    执行输出8,此步不影响其他操作,接着输出4,将其后的回调压入微队列(5)

    输出:1 7 2 3 8 4
    宏队列:[0]
    微队列:[65]
    
    • 1
    • 2
    • 3

    最后就是:

    输出:1 7 2 3 8 4 6 5 0
    宏队列:[]
    微队列:[]
    
    • 1
    • 2
    • 3
  • 相关阅读:
    2023.11.15-hivesql之炸裂函数explode练习
    数据库锁及批量更新死锁处理
    Jenkins 中配置 LDAP
    预制菜行业数据分析(京东数据挖掘)
    抖音直播招聘报白企业人力资源公司多渠道展示职位
    面向对象设计模式
    Web网页实现多路播放RTSP视频流(使用WebRTC)
    Linux网络-HTTPS协议
    什么是API网关?——驱动数字化转型的“隐形冠军”
    【修复版】2023新版塔罗 算八字测运易理风水 取名 源码平台 搭建教程
  • 原文地址:https://blog.csdn.net/qq_44741441/article/details/125423924