【JavaScript】13. Event Loop 事件循环

283 阅读6分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

Event Loop

(1)基本概念

  • Event Loop即事件循环

    • 是一种用于解决JavaScript单线程运行时不会阻塞的执行机制
  • 首先明确,JavaScript是单线程的,这意味着,所有任务都需要排队

    • 前一个任务结束,才会执行后一个任务

    • 这种由 主线程从 “任务队列” 中读取执行事件,不断循环重复的过程,就被称为 事件循环(Event Loop)

      JS引擎存在一个进程,会持续不断的检测主线程执行栈是否为空

      • 一旦为空,就会去任务队列中检查是否有等待的任务
  • 如果前一个任务耗时很长,那么后一个任务就不得不一直等待

    • 为了解决这个问题,所以JS的单线程任务有分为了同步任务和异步任务

(2)JS调用栈(执行栈)

  • Javascript 有一个 main thread 主线程和 call-stack 调用栈
  • 所有的任务都会被放到调用栈等待主线程执行
  • 调用栈也称为执行栈,采用的是后进先出的规则
    • 当函数执行的时候,会被添加到栈的顶部(push)
    • 当执行栈中执行完该函数,就会将其从栈顶移除(pop)
    • 直到栈内被清空

(3)任务队列(Task Queue)

  • 任务队列是一个先进先出类比排队)的数据结构
    • 排在前面的任务,优先被主线程读取
  • 任务队列会在执行栈被清空后,依次进入主线程
    • 注意任务队列上一些函数触发的条件
    • 有些事件,如定时器,只有在事件到了之后才会被触发

(4)同步任务和异步任务

  • 前面提到过,JS中的单线程任务分为了同步任务异步任务

  • 同步任务:

    • 主线程执行时,同步任务会被放入执行栈中,等待主线程依次执行
  • 异步任务:

    • 在执行栈之外,存在一个任务队列
    • 在主线程执行时,异步任务会被放入这个任务队列
      • 只有等待执行栈中的所有同步任务执行完毕后
      • 系统才会开始读取任务队列并将其放入执行栈中
      • 然后等待主线程执行

(5)宏任务和微任务

  • 而在JavaScript中,异步任务又被分为两种:宏任务(MacroTask)、微任务(MicroTask

  • 宏任务

    • script的全部代码、setTimeoutsetIntervalI/OUI渲染
  • 微任务

    • PromiseMutationObserver

    注意Promise本身是同步任务,但其回调函数.then()是异步任务的微任务

  • 宏任务和微任务都是异步任务,同属于一个队列

    • 其内部又划分为了宏任务队列和微任务队列

  • 关于async / await

    async function foo() {
      // await 前面的代码
      await bar(); // await的代码
      // await 后面的代码
    }
    
    
    • 其中 await 前面的代码 是同步的,调用此函数时会直接执行

    • await bar(); 这句可以被转换成 Promise.resolve(bar())

    • await 后面的代码 则会被放到 Promise 的 then() 方法里

    // 所以上面的代码可以转换成
    function foo() {
        // await 前面的代码
        Promise(resolve => {
                bar() // await的代码
        resolve()
    }).then(() => {
        // await 后面的代码
    });
    }
    

(6)执行过程

先看一个简单版的任务执行过程

a)简单版的
  • JS开始执行,执行过程中会将遇到的代码分为两类
    • 如果是同步任务,则放入执行栈中,等待主线程执行
    • 如果是异步任务,则放入任务队列中
  • 执行栈上的任务执行完毕后,即执行栈被清空了,系统就会去任务队列上读取一个任务
    • 并将这个任务放入执行栈中,等待主线程执行
  • 然后再清空执行栈,再从任务队列上读取一个任务,循环往复

现在我们来看一个具体一点的,图源网络,出处不明

eventloop.png

b)具体一点的:

通过图,发现同步任务没有什么变化,而异步任务更具体了,所以我们主要看异步任务

  • 异步任务会先进入Event Table
    • 然后等待其触发条件,(比如定时器时间到了、事件点击触发了),触发成功后,就会注册它的回调函数为一个任务
    • Event Table会将这个任务移入Event Queue任务队列
  • 主线程任务全部执行完毕后,就会去Event Queue读取一个任务队列
    • 并将这个任务放入执行栈中,由主线程来执行

我们知道,异步任务又会分为宏任务微任务

那么宏任务和微任务又是如何执行的呢?

我们再上一张图,图源网络,出处不明

task.png

c)再具体一点的
  • 根据遇到的任务不同,任务队列又可以分为:宏任务队列、微任务队列

  • 执行顺序:

    1. 最开始的时候,script标签内的代码将作为宏任务。向调用栈中压入全局上下文开始执行

    2. 执行过程:

      • 遇到宏任务,则加入下一次宏任务队列中;
      • 遇到微任务,则加入当前宏任务的微任务队列中
        • 注意,当前执行栈中是宏任务
        • 每个宏任务会创建自己的微任务队列
    3. JS同步代码执行完之后,宏任务即将结束之前,即执行栈被清空时

      • 会将该宏任务对应的微任务队列的任务全部取出依次执行
      • 若执行过程中又遇到微任务加入当前微任务队列队尾
      • 若执行过程中又遇到宏任务,则将其加入下一轮宏任务队列中
    4. 微任务队列清空后本轮event loop结束。

      • 然后从任务队列取出一个宏任务,将宏任务中的代码取出,初始化全局上下文开始执行
      • 有回到第2步的执行过程

(7)实例理解

// 位置1
console.log('script start(同)')
// 位置2 -- setTimeout1
setTimeout(function () {
  console.log('setTimeout1(宏)')
})
// 位置3 -- setTimeout2
setTimeout(function () {
  console.log('setTimeout2 —— 200s(宏)')
  // setTimeout2 -- setTimeout
  setTimeout(function () {
    console.log('setTimeout2 —— 200s —> setTimeout(宏)')
  })
  // setTimeout2 -- promise
  Promise.resolve().then(function () {
    console.log('setTimeout2 —— 200s —> promise(微)')
  })
}, 200)
// 位置4 -- promise1
new Promise(resolve => {
  console.log('promise1(同)')
  resolve()
}).then(function () {// 位置5
  console.log('promise1 -> then(微)')
  // promise1 -- setTimeout
  setTimeout(function () {
    console.log('promise1 -> then —> setTimeout(宏)')
  })
})
// 位置6 -- promise2
Promise.resolve()
  .then(function () { // 位置7
    console.log('promise2 -> then(微)')
    // promise2 -- then1 -- setTimeout
    setTimeout(function () {
      console.log('promise2 -> then —> setTimeout(宏)')
    })
  })
  .then(function () {
    console.log('promise2 -> then —> then(微)')
    // promise2 -- then2 -- setTimeout
    setTimeout(function () {
      console.log('promise2 -> then —> then —> setTimeout(宏)')
    })
  })
// 位置8
console.log('script end(同)')

输出结果为:(分析过程略)

# 输出结果
script start(同)
promise1(同)
script end(同)
promise1 -> then(微)
promise2 -> then(微)
promise2 -> then —> then(微)
setTimeout1(宏)
promise1 -> then —> setTimeout(宏)
promise2 -> then —> setTimeout(宏)
promise2 -> then —> then —> setTimeout(宏)
setTimeout2 —— 200s(宏)
setTimeout2 —— 200s —> promise(微)
setTimeout2 —— 200s —> setTimeout(宏)


本人前端小菜鸡,如有不对请谅解