“探索 JavaScript 事件循环机制:从宏任务到微任务的执行顺序”

前言

当我们在编写 JavaScript 代码时,经常会涉及到异步操作,比如网络请求、定时器等。为了处理这些异步任务,并保证代码的执行顺序和正确性,JavaScript 引入了事件循环(Event Loop)机制。 事件循环是 JavaScript 异步编程的核心机制,它负责协调和调度异步任务的执行。简单来说,事件循环就像一个循环在后台不断运行,它会从任务队列中取出任务并执行,直到任务队列为空。

进程 & 线程

进程和线程是计算机操作系统中的两个重要概念。

进程

进程是指正在运行中的一个程序,它是计算机资源分配的基本单位。每个进程都有独立的内存空间、代码和数据段,并且可以拥有自己的文件句柄、网络连接等系统资源。

线程

线程则是进程中的独立执行单元,它是操作系统进行调度的基本单位。每个线程都共享进程的内存空间和系统资源,但拥有自己的堆栈和寄存器,可以独立地执行特定的代码段。

总的来说,进程是操作系统资源分配的基本单位,线程是 CPU 调度的基本单位


可以将公司比喻为一个进程,而员工则是该进程中的线程。

公司作为一个进程,拥有独立的资源,如办公空间、设备、资金等。它有自己的目标和任务,比如提供产品或服务、实现盈利等。公司管理层相当于操作系统,负责调度和分配资源,制定规则和策略来实现公司的目标。

而员工则是公司进程中的线程,他们共享公司的资源,如办公设备、网络连接等。每个员工都有自己的专长和职责,可以独立地执行特定的任务。员工之间可以通过协作来完成更复杂的工作,也可以通过交流和共享知识来提高整个团队的效率。

类似于进程和线程之间的关系,公司中的员工可以并行执行不同的任务,从而提高工作的效率。不同的线程(员工)可以独立地执行特定的工作,并通过共享资源和协作来实现公司的目标。

异步编程

异步编程是一种处理任务的方式,它允许程序在进行耗时操作时不会被阻塞,而是继续执行其他任务。在异步编程中,任务可以分为宏任务和微任务两种类型。

宏任务(macrotask)

宏任务是一种在异步编程中用于处理耗时操作的任务类型。它们通常被放入事件队列中,按照顺序执行。

以下是一些常见的宏任务:

  1. 整体的 script 代码:整个脚本代码作为一个宏任务执行,即初始的全局代码。
  2. 定时器:使用 setTimeout 或 setInterval 注册的定时器回调函数会作为宏任务执行。它们在指定的延迟时间后被添加到宏任务队列,并按照顺序执行。
  3. I/O 操作:包括文件读写、网络请求等涉及输入输出的操作,它们通常是异步的,执行完成后将作为宏任务添加到队列中执行。
  4. UI 渲染:当浏览器需要重绘或重新布局页面时,会将相应的渲染任务作为宏任务执行。这些任务通常与用户交互和界面更新相关。
  5. setImmediate:在 Node.js 环境中,setImmediate 函数可以注册的回调函数也属于宏任务。它会在当前执行栈的末尾执行。

微任务(microtask)

微任务是一种在异步编程中用于处理轻量级任务的任务类型。与宏任务不同,微任务通常直接在已经运行的任务(如函数)执行完毕后立即执行,而不需要像宏任务那样等待一段时间。

以下是一些常见的微任务:

  1. Promise.then() 方法:Promise 对象的 then 方法返回的回调函数会被作为微任务执行。
  2. MutationObserver:当 DOM 树发生变化时,MutationObserver 监听器的回调函数也会被添加到微任务队列中执行。
  3. process.nextTick():在 Node.js 环境中,process.nextTick 函数注册的回调函数也属于微任务。它会在当前执行栈的末尾执行。
  4. Object.observe():当对象的属性发生变化时,Object.observe 方法注册的回调函数会被作为微任务执行。但这个方法已经被废弃。

微任务的执行优先级高于宏任务,因此在事件循环的每一轮中,当当前宏任务执行完毕后,会依次执行所有微任务,直到微任务队列为空,然后才会继续执行下一个宏任务。

事件循环(event-loop)

为了处理异步任务,JavaScript 引入了事件循环机制。事件循环会不断地从宏任务队列和微任务队列中取出任务,并按照一定的规则执行它们。

事件循环可以描述为以下步骤:

  1. 执行同步代码:从上到下按顺序执行当前执行栈中的同步代码,这些同步代码属于宏任务。
  2. 查询是否有异步任务需要执行:检查异步任务队列中是否有任务需要执行,如果有,则进入下一步。这些异步任务包括定时器回调、网络请求、事件回调等。
  3. 执行微任务:执行所有微任务队列中的任务。微任务会优先于宏任务执行,确保它们在下一个宏任务执行之前完成。
  4. 渲染页面:如果需要,浏览器会进行页面的渲染或布局操作,以更新用户界面。
  5. 执行宏任务:从宏任务队列中取出排在最前面的任务执行。宏任务队列中的任务可能是定时器回调、事件回调等。执行完当前宏任务后,返回第2步,开始下一轮事件循环。

看下面代码:

javascript 复制代码
console.log(1);
setTimeout(() => {
    console.log(2);
    new Promise((resolve) => {
        console.log(4);
        resolve()
        setTimeout (() => {
            console.log(6);
        })
    }).then(() => {
        console.log(5);
    })
}, 1000)
console.log(3);

这段代码执行的过程如下:

  1. 首先同步执行 console.log(1),输出数字 1。
  2. 然后创建一个定时器,在 1000 毫秒后将回调函数添加到宏任务队列中。
  3. 继续同步执行 console.log(3),输出数字 3。
  4. 定时器时间未到,进入下一个事件循环。
  5. 时间到达后,将回调函数添加到宏任务队列中。
  6. 取出宏任务队列中的回调函数,同步执行 console.log(2),输出数字 2。
  7. 创建一个 Promise 对象,并将其中的回调函数添加到微任务队列中,跳过 setTimeout 中的第二个定时器。
  8. 在微任务队列中取出 Promise 对象的回调函数,执行 console.log(4),输出数字 4。
  9. 调用 Promise 对象的 resolve() 方法,将状态设置为 resolved,并将其中的回调函数添加到微任务队列中。
  10. 在微任务队列中取出 Promise 对象回调函数中的 console.log(5),输出数字 5。
  11. 再次查询是否有需要执行的宏任务和微任务,发现 setTimeout 中的第二个定时器需要执行,执行 console.log(6),输出数字 6。

再看下面代码:

javascript 复制代码
console.log('stard'); 
async function async1() {
    await async2()  // 浏览器给await开小灶提速
    console.log('saync1 end'); 
}
async function async2() {
    console.log('saync2 end');
}
async1()
setTimeout(function() {
    console.log(('setTimeout'));
}, 0)
new Promise((resolve) => {
    console.log('promise');
    resolve()
})
.then(() => {
    console.log('thn1');
})
.then(() => {
    console.log('then2');
})
console.log('end');

以下是代码执行的详细顺序:

  1. 同步代码执行阶段:

    • 执行 console.log('stard'),打印字符串 'stard'
    • 调用 async1() 函数
  2. 异步任务添加阶段:

  • 添加宏任务:setTimeout,等待 0 毫秒后执行
  • 添加微任务:Promise 的回调函数,即 resolve() 的执行
  • 添加微任务:async1() 的回调函数
  • 添加微任务:第一个 then() 的回调函数
  • 添加微任务:第二个 then() 的回调函数
  1. 继续同步代码执行阶段:
  • 执行 console.log('end'),打印字符串 'end'
  • 创建一个新的 Promise 对象,并立即执行传入的回调函数,打印字符串 'promise'
  1. 异步任务执行阶段:
  • 执行微任务队列中的任务:

    • 执行 Promise 的回调函数,打印字符串 'thn1'
    • 执行 async1() 的回调函数,打印字符串 'saync1 end'
    • 执行第二个 then() 的回调函数,打印字符串 'then2'
  • 执行宏任务队列中的任务:

    • 等待 0 毫秒后执行的 setTimeout 的回调函数,打印字符串 'setTimeout'

因此,最终的输出顺序为:

  1. stard
  2. saync2 end
  3. promise
  4. end
  5. saync1 end
  6. thn1
  7. then2
  8. setTimeout

总结

事件循环是 JavaScript 异步编程的核心机制之一,它负责协调和管理 JavaScript 运行时的异步任务。

事件循环分为两个阶段:同步代码执行阶段和异步代码执行阶段。在同步代码执行阶段,JavaScript 会顺序执行所有同步代码,直到遇到异步任务(如定时器、事件监听等)。遇到异步任务时,JavaScript 会将其添加到任务队列中,并继续执行后面的同步代码。异步任务包括宏任务和微任务,其中宏任务包括定时器和 I/O 操作等,而微任务包括 Promise 的回调函数、MutationObserver 等。

在同步代码执行完毕后,JavaScript 就会开始执行异步任务。具体来说,它会先检查微任务队列中是否有任务,如果有则依次执行完所有微任务。当微任务队列为空时,JavaScript 再从宏任务队列中取出一个任务执行。执行完该任务后,再次检查微任务队列并依次执行微任务,以此类推,直到所有任务执行完毕。

需要注意的是,由于 JavaScript 是单线程的,因此在执行任何任务时都不会被其他任务打断。只有在某个任务执行完毕后,才会检查队列中是否有待执行的任务。因此,在编写 JavaScript 异步代码时,要特别注意避免长时间的同步阻塞,以免影响整个程序的性能和响应速度。

总之,事件循环是 JavaScript 异步编程的核心机制,了解其原理和执行顺序对于编写高效的异步代码至关重要。

相关推荐
一颗花生米。20 分钟前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐0124 分钟前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio199525 分钟前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&1 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈1 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水2 小时前
简洁之道 - React Hook Form
前端
正小安4 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
小飞猪Jay6 小时前
C++面试速通宝典——13
jvm·c++·面试
_.Switch6 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光6 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js