• js的事件循环(Eventloop) 机制/js的宏任务微任务执行顺序


    这篇借助于同事准备的技术分享,其他技术文章,书本知识,自己的理解梳理而成

    高级程序设计第三版:
    js 是一门单线程的语言,运行于单线程的环境中,例如定时器等并不是线程,定时器仅仅只是计划代码在未来的某个时间执行,浏览器负责排序,指派某段代码在某个时间点运行
    的优先级

    1.为什么规定浏览器必须是单线程?
    JS主要用途之一是操作DOM,如果JS同时有两个线程,同时对同一个dom进行操作,一个需要删除dom,一个需要添加dom,这时浏览器应该听哪个线程的,如何判断优先级,所以为了简化操作,规定js是一门单线程的语言。

    2.有关于js是单线程的理解
    所谓的"JS是单线程的"是指解释和执行JS代码的线程,只有一个,一般称之为“主线程”,而浏览器并不是单线程的,是多线程并且是多进程的,而对于前端最关心的还是渲染进程.

    1. GUI渲染线程
      ● 负责渲染浏览器界面,解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制
      ● 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
      ● GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中,等到JS引擎空闲时立即被执行。
    2. JS引擎线程
      ● 也称JS内核,负责处理JS脚本程序。例如V8引擎
      ● JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(Renderer进程)中无论什么时候都只有一个JS引擎线程在运行JS程序
      ● GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,页面渲染就不连贯。
    3. 定时触发器线程
      ● 传说中的setInterval和setTimeout所在的线程
      ● 定时器线程其实只是一个计时的作用,他并不会真正执行时间到了的回调,真正执行这个回调的还是JS主线程。当时间到了,定时器线程就通知事件触发线程,让事件触发线程将setTimeout的回调事件添加到待处理任务队列的尾部,等待JS引擎的处理。
      ● W3C在HTML5标准中规定,要求setTimeout中低于4ms的时间间隔算4ms
    4. 事件触发线程
      ● 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解为:JS引擎自己都忙不过来,需要浏览器另开线程协助)
      ● 当JS引擎执行setTimeout时(或者是来自浏览器内核的其他线程,如鼠标点击、ajax异步请求等),当这些事件满足触发条件被触发时,该线程就会将对应回调事件添加到添加到待处理任务队列的尾部,等待JS引擎的处理
      ● 由于JS是单线程关系,所以这些待处理任务队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
    5. 异步http请求线程
      ● 这个线程负责处理异步的ajax请求,当请求完成后如果设置有回调函数,他也会通知事件触发线程,然后事件触发线程将这个回调再放入任务队列中尾部,等待JS引擎执行

    3.单线程如何实现异步?
    大家都知道JS是单线程的脚本语言,在同一时间,只能做同一件事,为了协调事件、用户交互、脚本、UI渲染和网络处理等行为,防止主线程阻塞,设计者给JS加了一个事件循环(Event Loop)的机制

    3.1理解什么是执行上下文?
    可以看这篇文章https://amberzqx.com/2020/02/04/JavaScript%E7%B3%BB%E5%88%97%E4%B9%8B%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E5%92%8C%E6%89%A7%E8%A1%8C%E6%A0%88/

    当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。执行上下文(可执行代码段)总共有三种类型:
    全局执行上下文(全局代码):不在任何函数中的代码都位于全局执行上下文中,只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
    函数执行上下文(函数体):只有调用函数时,才会为该函数创建一个新的执行上下文,可以存在无数个,每当一个新的执行上下文被创-建,它都会按照特定的顺序执行一系列步骤。
    Eval 函数执行上下文(eval 代码): 指的是运行在 eval 函数中的代码,很少用而且不建议使用

    执行上下文又包括三个生命周期阶段:创建阶段 → 执行阶段 → 回收阶段
    JS引擎创建了执行上下文栈(执行栈)来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出,后进先出的原则,就像下面的汉诺塔,第一个最大的先进去,当拿出来的时候肯定是最后一个出来的,最小的那个后进去,拿出来的时候是最先拿出来的~因为JS执行中最先进入全局环境,所以处于"栈底的永远是全局执行上下文"。而处于"栈顶的是当前正在执行函数的执行上下文"

    举个例子:

     const firstFunction = () => {
                console.log('1');
                secondFunction();
                console.log('2');
            }
            const secondFunction = () => {
                console.log('3');
            }
            firstFunction();
            console.log(4)
    
            // 1324
            //从上到下的执行
            //图一:从上到下执行,先是全局作用域,那就是栈底第一个
            //图二: firstFunction的调用,打印出1,现在栈顶是secondFunction,因为函数里面还没有执行完,所以还没有被销毁
            //图三: secondFunction的调用,打印3,secondFunction,因为函数里面执行完,所以要被销毁到图四
            //再执行栈顶firstFunction里面的2到图5
    


    看上面的图是不是对应汉诺塔放进去,拿出来的一个过程

    3.2理解同步任务,异步任务,任务队列
    JavaScript 是一个单线程序的解释器,因此一定时间内只能执行一段代码。为了控制要执行的代码,就有一个 JavaScript 任务队列。这些任务会按照将它们添加到队列的顺序执行如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行.同步任务指的是,在主线程上,排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程,而进入“任务队列”(task queue)的任务,只有“任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

    3.3 js的事件循环机制
    具体来说,异步运行机制如下:
    (1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
    (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
    (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
    (4)述过程会不断重复,也就是常说的Event Loop(事件循环)

    3.4宏任务微任务
    js的任务又分为宏任务和微任务(btw,不要归类为同步任务异步任务和宏任务微任务扯上联系,抛开同步异步的概念去理解宏任务微任务)
    前端常见的宏任务,微任务分类:
    macro-task(宏任务):包括整体代码script,setTimeout,setInterval,setImmediate(node或者小众浏览器支持)
    micro-task(微任务):Promise,process.nextTick(node 环境支持)
    注意:
    Promise 新建后就会立即执行,也就是说new Promise构造函数是同步任务,但Promise的注册的then回调和catch回调才是微任务
    宏任务:可以理解是每次执行栈执行的代码就是一个宏任务,所有宏任务都是添加到任务队列,所以”任务队列又叫宏任务队列”,这个任务队列由事件触发线程来单独维护的

    微任务
    可以理解是在当前宏任务执行结束后立即执行的任务

    每一次事件循环,是先执行宏任务,再执行宏任务里面的微任务,看到里面两个字了吗?????每一次事件循环只执行一个宏任务

    注意:
    1.微任务队列里边的优先级process.nextTick()>Promise.then()
    2.setInterval,setImmediate的执行顺序后续补充,目前前端几乎用不到setImmediate,不要慌

    总结:
    每一次循环称为 tick, 每一次tick的任务如下:
    1、执行一个宏任务(执行栈中没有就从任务队列中获取)
    2、宏任务执行过程中如果遇到微任务,就将它添加到微任务队列中
    3、宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
    4、重复1到3步骤
    宏任务 > 所有微任务 > 宏任务>微任务,也就是每一次事件循环,是先执行宏任务,再执行宏任务里面的微任务,下一轮还是先执行宏任务,再执行下一轮的微任务

    练习题目:
    example1

          console.log('1')
            setTimeout(() => {
              console.log('2')
            }, 0)
            Promise.resolve().then(() => {
              console.log('3')
            }).then(() => {
              console.log('4')
            })
            console.log('5')
          // 1 5 3 4 2
          //第一轮循环,执行宏任务script里面的同步任务,遇到微任务挂起,先执行15
          // 执行完宏任务,执行微任务 3, 4 微任务执行完毕了就是下一轮事件循环的开始
          //  第二轮循环 执行 settimout里面的2
    

    example2
    es6那本书---Promise 新建后就会立即执行

      console.log('1')
        new Promise((resolve) => {
          console.log('2')
          setTimeout(()=>{
            console.log('4')
            resolve()
            }, 0)
        }).then(() => {
          console.log('3')
        })
    
        // 1,2,4,3
        // 第一轮: 宏 script 打印出:12 
        // 第二轮: 宏 setTimeout 打印出: 4,3
        // 这里要注意Promise的注册的then回调和catch回调才是微任务,所以resolve()是在setTimeout里面调的
        // 属于第二轮的微任务
    

    example3

      new Promise((resolve) => {
          console.log('1')
          setTimeout(() => {
            console.log('2')
          }, 1000)
          resolve()
        }).then(() => {
          console.log('3')
        })
        // 132
        // 基于example2,可以明白,resolve()在setTimeout之外调用,还是属于第一轮宏任务里面的微任务
    

    example4

    console.log('1')
       new Promise((resolve) => {
         console.log('2')
         resolve()
         console.log('3')
       }).then(() => {
         console.log('4')
       })
       console.log('5')
    
       // 第一轮  宏: script 打印: 1235 微:4
       // 12354
       // 要注意的是3不用等resolve回调完再执行哦,因为并没有还可以继续执行,await可以阻塞下面的执行
    

    example5

     console.log('1')
           setTimeout(() => {
               console.log('2')
               new Promise((resolve) => {
                   console.log('3')
                   resolve()
               }).then(() => {
                   console.log('4')
               })
           })
           new Promise((resolve) => {
               console.log('5')
               resolve()
           }).then(() => {
               console.log('6')
           })
           console.log('7')
           setTimeout(() => {
               console.log('8')
               new Promise((resolve) => {
                   console.log('9')
                   resolve()
               }).then(() => {
                   console.log('10')
               })
           })
    
       // 第一轮循环 宏:script  打印: 157  微:resolve() 打印:6
       // 第二轮循环 宏:第一个setTimeout 打印:23  微:resolve() 打印:4
       // 第三轮: 宏:第二个setTimeout 打印:89 微:resolve() 打印:10
       // 15762348910
    

    example6 在node下面执行,这个例子一般,不值得一看

    console.log('1');
        setTimeout(function() {
            console.log('2');
            process.nextTick(function() {
                console.log('3');
            })
            new Promise(function(resolve) {
                console.log('4');
                resolve();
            }).then(function() {
                console.log('5')
            })
        })
        process.nextTick(function() {
            console.log('6');
        })
        new Promise(function(resolve) {
            console.log('7');
            resolve();
        }).then(function() {
            console.log('8')
        })
    
        setTimeout(function() {
            console.log('9');
            process.nextTick(function() {
                console.log('10');
            })
            new Promise(function(resolve) {
                console.log('11');
                resolve();
            }).then(function() {
                console.log('12')
            })
        })
    
        // 第一轮 宏:script 打印:17 微: process.nextTick以及Promise.then 打印: 6 8
        // 第二轮:宏: 第一个settimeout  打印: 24 微:process.nextTick以及Promise.then 打印:35
        // 第三轮: 宏: 第二个settimeout 打印: 9 11 微: process.nextTick以及Promise.then 打印: 10 12
        // 1768 2435    9 11 10 12
    

    example6

     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') 
            },0)  
            async1();
            new Promise(function(resolve){
                console.log('6')
                resolve();
            }).then(function(){
                console.log('7')
            })
            console.log('8')
    
            // 第一轮事件循环 宏:script 打印:4 1 3 6 8 微: await之后的结果 以及resolve() 打印: 2 7
            // 第二轮事件循环 宏:setTimeout 打印: 5 
            // 4 1 3 6 8  2 7 5
            //注意 await xx的时候,相当于xx这里直接创建了一个new promise,所以async2函数是new promise,会立即执行, await的结果是promise.then的结果,并且没有成功finish会阻塞下面的执行,所以2会在微任务拿到结果之后执行
    

    还有一个例子,前端目前用不到哈,后续更新:

    console.log('1')
    setTimeout(() => {
      console.log('2')
      process.nextTick(() => {
        console.log('3')
      })
      new Promise((resolve) => {
        console.log('4')
        resolve()
      }).then(() => {
        console.log('5')
      })
    })
    new Promise((resolve) => {
      console.log('7')
      resolve()
    }).then(() => {
      console.log('8')
    })
    console.log('9')
    process.nextTick(() => {
      console.log('10')
    })
    setImmediate(() => {
      console.log('15')
      process.nextTick(() => {
        console.log('16')
      })
      new Promise((resolve) => {
        console.log('17')
        resolve()
      }).then(() => {
        console.log('18')
      })
    })
    setTimeout(() => {
      console.log('11')
      new Promise((resolve) => {
        console.log('12')
        resolve()
      }).then(() => {
        console.log('13')
      })
      process.nextTick(() => {
        console.log('14')
      })
    })
    1,7,9,10,8,
    2,4,3,5
     11  12  14  13  
     15  17  16  18 
    
    
    
  • 相关阅读:
    9.3 simulated match
    网络流模版大全
    Treblecross
    ENimEN
    求逆序对的两种方法(树状数组/归并排序)
    树状数组
    计算最短路和次短路条数
    Python3.7版库的安装以及常用方法(十分简单)
    二维线段树(hdu1823)
    流星雨(记忆化搜索)
  • 原文地址:https://www.cnblogs.com/antyhouse/p/13380264.html
Copyright © 2020-2023  润新知