JavaScript事件循环之单线程异步编程的核心机制

 更新时间:2026年06月10日 09:29:53   作者:riuphan  
由于JS是单线程执行的,所以对于耗时的操作(如网络请求),需要通过异步编程来处理,这篇文章主要介绍了JavaScript事件循环之单线程异步编程的核心机制,需要的朋友可以参考下

一、JavaScript 单线程特性

JavaScript 采用单线程执行模式,这是由其设计初衷决定的:

  1. 避免 DOM 操作冲突:JavaScript 可以操作 DOM 结构,如果多个线程同时修改同一个 DOM,会导致渲染不一致和数据竞争问题。
  2. 简化编程模型:多线程需要引入锁机制来协调资源访问,单线程无需考虑锁机制,降低了编程复杂度,减少了设备性能开销。

二、js任务分类

JavaScript 中的任务分为两大类:同步任务异步任务

2.1 同步任务

同步任务在计算机性能足够时几乎不耗时,它在主线程上立即执行,前一个任务不完成,后一个任务不会开始。例如变量赋值、算术运算、console.log() 等都是同步任务。

2.2 异步任务

异步任务是指需要等待特定条件触发后才能执行的任务,它们会被存放到任务队列中,等待主线程执行完同步任务时再执行。异步任务按耗时长短又分为两大类:

宏任务

异步任务中耗时相对较长的任务,会被存放到宏任务队列中,主要有以下几种:

  • script(script标签也属于一个宏任务)
  • setTimeout() / setInterval()
  • I/O 操作(文件读写、网络请求)
  • UI 渲染

微任务

异步任务中耗时相对较短的任务,会被存放到微任务队列中,微任务相较于宏任务优先执行。主要有以下几种:

  • Promise.then() / Promise.catch() / Promise.finally()
  • process.nextTick()(Node.js)
  • MutationObserver

以下面这段代码为例,可以直观看到同步与异步的划分:

let a = 1
console.log(a);                 // 同步:立即输出 1

new Promise((resolve) => {
  a = 2
  console.log(a);               // 同步:Promise 构造函数内部同步执行,输出 2
  resolve()
}).then(() => {
  a = 3
  console.log(a);               // 异步微任务:等待同步代码全部执行完再执行,输出 3
})

setTimeout(() => {
  a = 4
  console.log(a);               // 异步宏任务:1 秒后才执行,输出 4
}, 1000)

执行顺序分析:

  1. a = 1console.log(a) 是同步任务,立即输出 1
  2. new Promise(...) 也是同步任务,a 变为 2,输出 2new Promise(...).then() 是微任务,进入微任务队列
  3. setTimeout(...) 进入宏任务队列,等待 1 秒
  4. 同步代码执行完毕,此时开始执行异步任务,从队列中取出微任务:.then() 执行,a 变为 3,输出 3
  5. 1 秒后,取出宏任务:setTimeout 执行,a 变为 4,输出 4

三、事件循环机制

事件循环(Event Loop)是 JavaScript 协调同步任务和异步任务的核心机制。JS 引擎执行代码的完整流程如下:

  1. 先执行同步任务(script标签属于宏任务),这个过程中,遇到异步就存入对应的队列中
  2. 去微任务队列中查找微任务,并将微任务全部取出来执行
  3. 有需要的情况下,就渲染页面(只发生在微任务清空后)
  4. 去宏任务队列中查找宏任务,并将宏任务取出一个来执行,此时宏任务内部可能产生新的宏任务或微任务 (也是下一次循环的开始)

关键要点:每执行完一个宏任务,都会清空微任务队列,再进行下一轮循环。微任务的优先级高于宏任务。

代码示例:

console.log(1);

new Promise((resolve) => {
  console.log(2);
  resolve()
}).then(() => {
  console.log(3);
  setTimeout(() => {
    console.log(4);
  }, 0);
})

setTimeout(() => {
  console.log(5);
}, 1000);
console.log(6);

分析:

  1. console.log(1) —— 同步,输出 1
  2. 创建 Promise,同步执行 —— console.log(2) 输出 2,将 .then(...) 推入微任务队列
  3. setTimeout(..., 1000) —— 将宏任务 A(1 秒后执行)加入宏任务队列
  4. console.log(6) —— 同步,输出 6
  5. 「同步代码执行完毕」—— 开始清空微任务队列
  6. 执行 .then() —— 输出 3setTimeout(..., 0) 将宏任务 B 加入宏任务队列
  7. 「微任务队列已空」—— 等待宏任务
  8. 宏任务 B(0 毫秒)计时器先到 —— 输出 4
  9. 宏任务 A(1000 毫秒)后到 —— 输出 5

从这个例子可以清楚看出:同步代码先跑完 → 微任务清空 → 宏任务按时间顺序依次执行。即使 setTimeout(..., 0) 写在了 .then() 里面,也要等外层宏任务完了、微任务完了,才会轮到它。

注意: 所有 setTimeout 共用同一个计时器体系,先进队列的不一定先执行,执行顺序取决于各自的延迟时间长短。例如上面的例子中,当定时器延迟分别为 0ms 和 1000ms 时,0ms 的宏任务先执行,1000ms 的后执行。

进阶示例:宏任务内嵌套微任务

当宏任务内部又产生了新的微任务和宏任务,事件循环会如何处理?

console.log(1);

setTimeout(() => {
  console.log(2);
  new Promise((resolve) => {
    console.log(3);
    resolve()
  }).then(() => {
    console.log(4);
  })
  setTimeout(() => {
    console.log(5);
  }, 0)
}, 1000)

console.log(6);

逐步骤分析:

  1. console.log(1) —— 同步,输出 1
  2. setTimeout(..., 1000) —— 宏任务 A 加入队列
  3. console.log(6) —— 同步,输出 6
  4. 「同步代码执行完毕,微任务队列为空」—— 等待宏任务
  5. 1 秒后,取出宏任务 A 执行:
    • console.log(2) —— 输出 2
    • 创建 Promise,同步执行 —— console.log(3) 输出 3resolve().then() 加入微任务队列
    • setTimeout(..., 0) —— 宏任务 B 加入队列
  6. 「宏任务 A 执行完毕」—— 立即清空微任务队列
  7. .then() 执行 —— 输出 4
  8. 「微任务队列已空」—— 取出下一个宏任务 B
  9. 宏任务 B 执行 —— 输出 5

注意第 5、6、7 步:在宏任务 A 内部,console.log(3) 是同步代码、setTimeout(..., 0) 是新宏任务、.then() 是微任务。按照事件循环规则,宏任务 A 结束后,必须先清空微任务(执行 .then() 输出 4),才会去执行下一个宏任务 B(输出 5)。所以 4 出现在 5 之前,而不是 2, 3, 5, 4

四、async/await 原理

async/await 是 Promise 的语法糖,让异步代码写起来像同步代码,但底层依然遵循事件循环规则。

核心规则

  • 函数前加 async,等同于该函数返回一个 Promise 对象
  • await 后面的表达式看作同步,然后将 await 之后的代码推入微任务队列

使用async/await 可以代替.then().then()的方式执行代码

function A() {
  return new Promise(() => {
    setTimeout((resolve) => {
      console.log('a');
      resolve()
    }, 1000);
  })
}

function B() {
  console.log('b');
}

async function fn() {
  await A()   // 等待 A() 的 Promise resolve
  B()         // await 之后的代码,等价于 .then(() => B())
}
fn()

fn() 内部,await A() 会暂停 fn 的执行,等 A() 返回的 Promise 在 1 秒后 resolve,再继续执行 B()。这个效果等价于 A().then(() => B())

进阶示例:

console.log('script start');

async function async1() {
  await async2()
  console.log('async1 end');    // 微任务
}
async function async2() {
  console.log('async2 end');
}
async1()

setTimeout(() => {
  console.log('setTimeout');
}, 0)

new Promise((resolve, reject) => {
  console.log('promise');
  resolve()
})
  .then(() => {
    console.log('then1');
  })
  .then(() => {
    console.log('then2');
  });

console.log('script end');

逐步骤分析:

  1. console.log('script start') —— 同步,输出 script start
  2. 调用 async1(),执行到 await async2() ,看作同步
  3. async2() 同步执行 —— console.log('async2 end') 输出 async2 end
  4. awaitasync1 中后续代码(console.log('async1 end'))推入微任务队列
  5. setTimeout(..., 0) —— 宏任务加入队列
  6. new Promise(...) 同步执行 —— console.log('promise') 输出 promiseresolve() 将第一个 .then() 推入微任务队列
  7. console.log('script end') —— 同步,输出 script end
  8. 「同步代码执行完毕」—— 清空微任务队列:
    • 取出第一个微任务:console.log('async1 end') 输出 async1 end
    • 取出第二个微任务:console.log('then1') 输出 then1,并将第二个 .then() 推入微任务队列
    • 取出第三个微任务:console.log('then2') 输出 then2
  9. 「微任务队列已空」—— 取出宏任务
  10. setTimeout 回调执行 —— 输出 setTimeout

输出结果: script start, async2 end, promise, script end, async1 end, then1, then2, setTimeout

关键点:

  1. await 之后的代码已经变成了微任务,所以 script end 会在 async1 end 之前输出。
  2. 多个微任务在同一次清空中按入队顺序依次执行(async1 endthen1then2)。
  3. 无论微任务有多少,都必须全部清空后才会执行宏任务 setTimeout

总结 

到此这篇关于JavaScript事件循环之单线程异步编程的核心机制的文章就介绍到这了,更多相关JS事件循环单线程异步编程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用javascript实现json数据以csv格式下载

    使用javascript实现json数据以csv格式下载

    这篇文章主要介绍了使用javascript实现json数据以csv格式下载,需要的朋友可以参考下
    2015-01-01
  • JavaScript实现带并发限制的异步调度器

    JavaScript实现带并发限制的异步调度器

    这篇文章主要为大家详细介绍了如何基于JS实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有N个,感兴趣的小伙伴可以了解下
    2024-03-03
  • 微信小程序实现上传视频功能

    微信小程序实现上传视频功能

    这篇文章主要为大家详细介绍了微信小程序实现上传视频功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • 详解JavaScript的另类写法

    详解JavaScript的另类写法

    这篇文章主要介绍了详解JavaScript的另类写法的相关资料,需要的朋友可以参考下
    2016-04-04
  • JS中的forEach、$.each、map方法推荐

    JS中的forEach、$.each、map方法推荐

    下面小编就为大家带来一篇JS中的forEach、$.each、map方法推荐。小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-04-04
  • JavaScript将相对地址转换为绝对地址示例代码

    JavaScript将相对地址转换为绝对地址示例代码

    本文为大家详细介绍下JavaScript怎么将相对地址转换为绝对地址,具体的示例如下,感兴趣的朋友可以参考下哈,希望对大家有所帮助
    2013-07-07
  • JS实现点星星消除小游戏

    JS实现点星星消除小游戏

    这篇文章主要为大家详细介绍了JS实现点星星消除小游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-03-03
  • 微信小程序云开发实现增删改查功能

    微信小程序云开发实现增删改查功能

    这篇文章主要为大家详细介绍了微信小程序云开发实现增删改查功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-05-05
  • ECMAScript 的 6 种简单数据类型

    ECMAScript 的 6 种简单数据类型

    这篇文章主要介绍了ECMAScript的 6 种简单数据类型,ECMAScript的数据类型很灵活,一种数据类型可以当作多种数据类型来使用,具体使用详情文章详细介绍需要的小伙伴可以参考一下
    2022-06-06
  • 页面使用密码保护代码

    页面使用密码保护代码

    这是一个由JS实现的网页密码保护代码,在进入网页前需要在弹出框中输入密码才可以,不过现在不怎么用了,一般情况下,目前都在后台处理这种功能,用户输入用户名和密码后交给服务器处理,然后再返回信息,若登录无误就可看到某些内容
    2013-04-04

最新评论