网站首页 > 技术文章 正文
JavaScript 是一门单线程的编程语言,这意味着它只有一个主执行线程来处理所有的任务。然而,JavaScript 可以利用异步编程的方式实现并发操作,从而提高性能和用户体验。为了更好地理解 JavaScript 中的事件循环,我们首先要了解进程、线程和异步编程的概念。
进程与线程
进程是指操作系统中运行的一个程序实例。在计算机上同时运行着多个进程,每个进程都有自己的独立内存空间和执行环境。进程之间相互独立,彼此不会干扰。而线程是进程中更小的单位,是操作系统调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和其他资源。
简单来说,进程指的是cpu在运行指令和保存上下文所需的时间,线程指的是进程中更小的单位,指的是一段指令执行所需要的时间。
在浏览器中,每个标签页通常都是一个独立的进程,这样可以隔离不同页面的运行环境,提高安全性和稳定性。当我们在浏览器中新开一个标签页时,就会创建一个新的进程来处理该标签页的内容。而在这个进程中,又可以有多个线程来并行处理不同的任务。
举个例子,当我们在浏览器中新开一个标签页并访问一个网站时,这个进程中可能会有以下几个线程同时工作:
- 渲染线程:负责将 HTML、CSS 和 JavaScript 转换为可视化页面,处理页面的渲染和绘制。
- HTTP 请求线程:负责发送网络请求和接收响应,处理与服务器的通信。
- JavaScript 引擎线程:负责解析 JavaScript 代码并执行,处理页面的交互和动态效果。
需要知道的是,线程之间是可以一起工作的,这些线程在进程中互相协作,完成各自的任务。但是,渲染线程和 JavaScript 引擎线程是互斥的,即同一时间只能有一个线程在执行。这是因为渲染线程需要访问 DOM 树和样式表来进行页面渲染,而 JavaScript 引擎线程可能会修改 DOM 树或样式表,所以需要互斥执行,以免出现冲突。
单线程的JavaScript
由于 JavaScript 是单线程的,它在执行时只能按照顺序逐条执行代码。这样做有两个优点:
- 节约内存开销:由于只需要一个线程来执行代码,不需要为每个线程分配独立的内存空间,从而节约了内存开销。
- 没有锁的概念:在多线程编程中,由于多个线程可能同时访问共享资源,需要引入锁机制来保证数据的一致性,但这会增加上下文切换的时间。而 JavaScript 的单线程模型不存在这个问题,可以避免锁的开销。
异步编程
JavaScript 通过异步编程的方式来实现并发操作。它将任务分为宏任务和微任务两种类型。
宏任务(Macrotask)包括以下几种:
- script:整体的 JavaScript 代码块。
- setTimeout 和 setInterval:定时器任务。
- setImmediate:在当前事件循环完成后立即执行的任务。
- I/O 操作:例如网络请求、文件读写等。
- UI 渲染:浏览器需要绘制页面时触发的任务。
微任务(Microtask)包括以下几种:
- Promise.then():Promise 的回调函数。
- MutationObserver:DOM 变动观察器。
- process.nextTick():Node.js 中的微任务。
事件循环(event-loop)
事件循环(Event Loop)是 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);
这段代码最终的输出顺序是什么样的呢?
第一步执行同步代码,看这段代码首先是一个console.log(1);之后是一个异步的定时器setTimeout(),它是宏任务,我们将它加入宏任务队列[ setTimeout() ],再就是console.log(3);所以执行同步代码输出的是1,3。
第二步查询是否有异步任务需要执行,此时宏任务队列里有一个定时器,微任务队列为空,第三步和第四步不用做。
到第五步执行宏任务,执行宏任务队列里的定时器。我们再来看setTimeout()里的代码,回到第一步重新开始。
执行同步代码,首先打印2,此时的输出顺序是1,3,2,之后是一个new Promise(),它是立即执行的,于是执行它里面的代码打印了4,输出顺序为1,3,2,4,之后是一个resolve的调用,再就是一个定时器setTimeout(),我们又把它加入到宏任务队列[ setTimeout() setTimeout() ],宏任务队列里的第一个定时器我们已经开始执行了,所以把它划掉。new Promise()之后还有个.then(),这个是微任务,于是把它加入微任务队列里[ Promise.then() ]。此时宏任务和微任务队列不为空,查询到有异步任务,于是开始执行微任务队列里的Promise.then(),输出5,此时输出顺序为1,3,2,4,5,然后执行宏任务队列里的定时器,里面是立即执行的console.log(6),此时全部代码都执行完毕,最终的输出顺序为1,3,2,4,5,6。
注意await
这里我们还要再讲一下await会造成的影响,当代码中有await时,浏览器会给await开小灶提速,让它立即执行,而把它之后的代码挤入微任务队列。
我们来看例子,顺便练习一下刚刚学的知识:
console.log('start');
async function async1() {
await async2() // 浏览器给await开小灶提速
console.log('async1'); // 被await挤入微任务队列
}
async function async2() {
console.log('async2');
}
async1()
setTimeout(function() {
console.log('setTimeout');
}, 0)
new Promise((resolve) => {
console.log('promise');
resolve()
})
.then(() => {
console.log('then1');
})
.then(() => {
console.log('then2');
})
console.log('end');
看完这段代码你得出的输出顺序是什么呢?
正确答案是start,async2,promise,end,async1,then1,then2,setTimeout,你答对了吗?
我们来看下过程,第一步执行同步代码,首先是console.log('start');,输出了start,然后是async1(),于是我们去执行函数async1,里面是await async2()于是立即执行调用async2,函数async2里面是console.log('async2'); 于是现在的输出是start,async2。await async2()之后的代码是 console.log('async1');它被挤入了微任务队列[ console.log('async1') ]。此时async1的调用完成,之后是一个定时器,把它放入宏任务队列[ setTimeout() ],之后是立即执行的new Promise(),里面是console.log('promise');,此时输出为start,async2,promise。后面有两个.then(),把它们加入微任务队列[ console.log('async1'),.then(),.then()],最后是一个打印end,此时输出为start,async2,promise,end。然后是执行异步任务,先是微任务,一个个执行微任务队列里的微任务,此时输出为start,async2,promise,end,async1,then1,then2。然后执行宏任务,就一个setTimeout()输出‘setTimeout’,所以最终的输出就是start,async2,promise,end,async1,then1,then2,setTimeout啦,你学会了吗?
作者:Guava205
链接:https://juejin.cn/post/7310786575214559282
- 上一篇: 高级前端进阶,你了解事件循环吗?
- 下一篇: autolog.js:一个小而美的toast插件。
猜你喜欢
- 2025-06-24 服务端性能测试实战3-性能测试脚本开发
- 2025-06-24 跨标签页通信(六):Cookie(cookie跨页面)
- 2025-06-24 崩溃!代码总掉链子?6 个 JavaScript 技巧助你稳操胜券
- 2025-06-24 如何取消一个已经开始的 JavaScript Promise
- 2025-06-24 Web页面如此耗电!到了某种程度,会是大损失
- 2025-06-24 setTimeout 出大 bug?为什么需要 setBigTimeout?
- 2025-06-24 悠然!午休十分钟分清 Vue 的 watch 和 computed,面试不发怵
- 2025-06-24 科普:CPU空闲时在忙什么?(cpu空闲温度)
- 2025-06-24 html5大神结合js带你研究古老读心术,你的心思早被猜透
- 2025-06-24 JWT + Refresh + SSO模版(jwt demo)
你 发表评论:
欢迎- 06-24发现一款开源宝藏级工作流低代码快速开发平台
- 06-24程序员危险了,这是一个 无代码平台+AI+code做项目的案例
- 06-24一款全新的工作流,低代码快速开发平台
- 06-24如何用好AI,改造自己的设计工作流?
- 06-24濮阳网站开发(濮阳网站建设)
- 06-24AI 如何重塑前端开发,我们该如何适应
- 06-24应届生靠这个Java简历模板拿下了5个offer
- 06-24服务端性能测试实战3-性能测试脚本开发
- 566℃Oracle分析函数之Lag和Lead()使用
- 564℃几个Oracle空值处理函数 oracle处理null值的函数
- 550℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 545℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 543℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 536℃【数据统计分析】详解Oracle分组函数之CUBE
- 526℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 518℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端富文本编辑器 (47)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle的函数 (57)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)