js的事件循环(Eventloop) 机制

Posted 周小姐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了js的事件循环(Eventloop) 机制相关的知识,希望对你有一定的参考价值。

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

高级程序设计第三版:
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 


以上是关于js的事件循环(Eventloop) 机制的主要内容,如果未能解决你的问题,请参考以下文章

前端中的事件循环eventloop机制

js事件循环机制(Event Loop)

JS中EventLoop事件循环机制

对javascript EventLoop事件循环机制不一样的理解

浅析JS的Event Loop机制

简述JavaScript事件循环EventLoop