javascript 进阶知识点及运用之事件循环机制(event loop)

448 阅读4分钟

前言

以下为部分javascript的知识点整理,我将通过知识点+使用场景进行描述。

事件循环机制(Event Loop)

JavaScript的特点就是单线程,这就意味着当某一个模块需要运行较长时间时,会导致后面的代码处于长时间排队中而无法执行。为了解决这个问题,便有了事件循环机制(event loop),这是javascript基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务

image.png

基本的执行机制

主线运行时,会产生堆(heap)和栈(stack),函数调用会生成若干帧组成的栈,当执行完一帧,这一帧出栈,直至所有帧都弹出,栈清空。而在每帧执行当中,JavaScript会遇到一些待处理的事件消息,待处理的消息会被进入消息队列中,在栈清空后,则开始执行队列中的消息,被处理的消息,会被移除队列调用与之关联的函数,此时,函数的调用如前所讲,会形成一个帧并压入栈中,重复如上步骤。直至栈跟队列全部清空。

在消息队列中,setTimeout 具有延迟性,基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间,setInterval同理。即便延迟时间为0,setTimeout也不会立即执行。从这个层面来说,即便promise也是异步,也会先于setTimeoutsetInterval执行回调函数。

参考地址:并发模型与事件循环

注:栈的执行规则:先进后出; 队列执行规则:先进先出;

微任务和宏任务

深入来说,我们通常也把js的执行分为同步任务异步任务,所有同步任务会被压入执行栈中执行,异步任务则放入任务队列,任务队列也分为了微任务队列宏任务队列

宏任务

  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 <script> 元素中运行代码)。
  • 触发了一个事件,将其回调函数添加到任务队列时。
  • 执行到一个由 setTimeout() 或 setInterval()创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。

常见的函数: setTimeoutsetIntervalsetImmediaterequestAnimationFrame

微任务

JavaScript 中的 promises和 Mutation Observer API 都使用微任务队列去运行它们的回调函数,我们可以理解为promises和 Mutation Observer API即将运行的回调函数即为一个个微任务

常见到的函数: process.nextTickPromisesObject.observeMutationObserver

image.png

执行机制

  1. 执行栈中的同步任务,如果存在微任务,则该任务进入微任务队列中;如果存在宏任务,则该任务进入宏任务队列中,直至执行栈清空
  2. 在执行栈清空后,检查微任务队列中的微任务,有则出列调用与之关联的函数,形成一个帧入栈,跳至步骤1,直至微任务队列为空,跳至步骤3;
  3. 检查宏任务队列中的宏任务,有则出列调用与之关联的函数,形成一个帧入栈,跳至步骤1,直至宏任务队列为空

举例

<script>
    console.log('script1')
    $('.btn').on('click', function () {
        console.log('script1-click')
    })

    function func3() {
        console.log('script1-func3-1')
        setTimeout(() => {
            console.log('script1-settimeout1')
        })
        setTimeout(() => {
            console.log('script1-settimeout2')
        }, 100)
        new Promise(res => {
            console.log('script1-promise')
            res()
        }).then(() => {
            console.log('script1-promise.then1')
        }).then(() => {
            console.log('script1-promise.then2')
        })
        $('.btn').click()
        console.log('script1-func3-2')
    }

    function func2() {
        console.log('script1-func2')
        func3()
    }

    function func1() {
        console.log('script1-func1')
    }
    func1()
    func2()
</script>
<script>
    console.log('script2')
    new Promise(res => {
        res()
    }).then(() => {
        console.log('script2-promise')
    })
    setTimeout(() => {
        console.log('script2-settimeout')
    })
</script>

如上例子

  1. <script> 元素中运行代码分别进入任务队列,开始执行第一个<script>元素中的代码,当func1()被调用时,func1会被压入栈中;执行func1函数时,调用func2(),func2进栈,此时func2中调用func3(),func3进栈;在执行func3时,遇到0延迟即将执行的setTimeout,添加一条消息进入消息队列;继续执行,遇到了延迟100毫秒的setTimeout,未触发事件的发生,不入队;继续执行,遇到了即将执行的promise的状态回调函数,添加一条消息进入消息队列。继续执行,调用click(),click进栈,click执行完成,click出栈;func3执行完成,func3出栈;func2执行完成,func2出栈;func1执行完成,func1出栈。
  2. 此时执行栈为空,检查微任务队列,依次出列执行promise的回调函数,直至微任务队列清空
  3. 检查宏任务队列,第二个<script> 元素中的代码开始执行,与上同理

由上可知运行结果:

image.png

参考地址:

在 JavaScript 中通过 queueMicrotask() 使用微任务

深入:微任务与Javascript运行时环境

JavaScript 运行机制详解:再谈Event Loop

日常可见的运用:

利用setTimeout做防抖与节流,控制请求的发送

vue中节点的异步渲染,以及vue中封装的nextTick回调函数执行属于微任务,先于setTimeout执行

问题

以下问题为我从文档中了解后的个人理解,如有误,欢迎指正

  1. 如点击事件click入不入任务队列?

    如果事件本身有绑定回调函数,用户在点击时,即表示这是即将需要执行的任务,会入队列中。但在上面的例子中,我们可以知道,但js代码中直接调用click(),此时不会进入任务队列,因为此时是函数的直接调用,直接形成一个帧入栈

  2. promise为什么有部分代码是同步执行的,如下例子,script1-promise是同步打印的?

    我的理解是,new 一个函数,本身就是函数的构造调用,从上面我们可以知道,函数的调用会入栈,但promise的状态处理回调函数是异步的,且属于微任务

new Promise(res => { 
    console.log('script1-promise') 
    res() 
})

小结

  1. 执行栈用来执行同步任务,函数的调用会被压入执行栈(先进后出)
  2. 执行栈清空,微任务队列里的微任务依次出列执行(先进先出),直至队列清空
  3. 微任务队列清空后,宏任务队列取出一个任务执行,进入1,开始一次新的事件循环

如有错误缺漏,欢迎留言指正