精读Javascript系列(七)事件循环细则 I:微任务、宏任务

Node.js Express 应用 Kubernetes 生产部署避坑指南 Node.js 是基于 V8 的单线程 JavaScript 运行时,其内存管理、信号处理与进程生命周期特性,与 Kubernetes 声明式编排模型存在天然张力。理解 cgroup 资源隔离原理、probe 探针语义差异及容器优雅终止机制,是保障 Express 服务稳定运行的技术前提。关键价值在于避免 OOMKilled、CrashLoopBackOff 和流量接入失败等典型故障,适用于电商后台、IoT 管理平台等高并发 Web 场景。本文聚焦 Docker 镜像优化、YAML 字段精调与 kubect 阅读详情

前言

对于Javascript异步,我是从其他面向对象编程语言的并发编程层层向下介绍的,在一些细节上并没有多详细说明。此次算是补充所缺,在选择主题时,我茫然了好一阵,决定从微任务和宏任务开始入手,阅读下文时,尽可能有些Promise的基础。

开始吧。

事件循环

首先补充一下上次事件循环的更多细节:

  1. 事件循环开启
  2. 新消息序列设为当前消息序列
  3. 当前消息序列中取出任务
    消息序列是先入先出结构,也就是说它是按照顺序取出的。
  4. 处理任务
    自上而下运行JS代码
    如果发出异步请求,然后将消息保存到这个新消息序列(若无则新建)中
    新消息序列的任务全部被阻塞,等待下次事件循环迭代处理。
  5. 检查当前消息序列是否为空,是则继续,否则转至 (3)
  6. 是否触发UI Rendering事件,是则立即进行视图渲染。 否则继续
  7. 是否新增消息序列
    如果是,开始下一轮事件循环,回到(2)
    否则继续
  8. 确定再无事件,关闭事件循环。线程进入休眠;
    直至有事件发生,新建消息序列并保存消息,转至(1)。

从上面的过程中,可以得到下面的结论:

  1. 此次事件循环将消息序列进行了细分,即当前的新增的,两者并不同。
  2. 一次事件循环,处理一个消息序列,而不只是一个消息。
  3. 一个事件循环都只渲染一次。
  4. 此事件循环仍需改进

宏任务

我们常说的任务(task),都是宏任务(Macrotask),由宏任务组成的消息序列,称作宏任务序列,即 Macrotasks套娃嫌疑确定……),一般都是涉及到 IO操作(包括网络请求、页面渲染等)的任务,例如:

  1. scripts: 脚本代码
  2. Mouse/Key Events : click、onload、input等。
  3. Timers: 定时器,例如setTimeout setInterval等。
  4. 未完待续

注意:Timers工作过程是这样的:

  1. 调用setTimeout时,将消息(回调函数,即task)放到延迟消息队列
  2. 延迟消息队列中的task到期后,放入新Macrotasks中。
  3. 在下次事件循环迭代中等候处理。

示例:
首先搞一个用于生成定时器的函数。

 <div id="text"></div>
 var text = document.querySelector('#text')
   var genTimer = (string,delay=0,cb)=>{
       return ()=>{
           setTimeout(()=>{
           text.innerHTML+=string+'<br/>'
           !cb||cb();
       },delay*1000)
       }
   }    

现在请一直记住脚本代码和Timer是一个宏任务
并且这个函数会一直用到结束为止。
(……想必上面的代码极易理解的吧……)

事件循环与渲染

准备两个消息序列的任务,
算是模拟两次事件循环的消息序列。

 var macroTasks = []
 macroTasks[0] = [genTimer('A1: Be honest rather clever.',1),
                  genTimer('A2: Being on sea, sail; being on land, settle.',1)]
   // 两个队列的到期时间不一致!               
 macroTasks[1] = [genTimer('B1: Failure is the mother of success.',3),
                 genTimer('B2: The shortest answer is doing.', 3)]
 /*
   macroTask[0] 是 下次的消息序列的任务
   macroTask[1] 是 下下次的消息序列的任务
 */

请注意, macroTasks[0]中的task都在1s后到期。因此下轮事件循环中会处理这些tasks。同理,macroTasks[1]中的tasks都在3s后到期,因此会在下下次事件循环中处理这些tasks。

为什么要注意这些区别呢?
因为它们分两次事件循环的处理的!

  text.innerHTML +='start......<br>'
  macroTasks[0].forEach(f=>f())
  macroTasks[1].forEach(f=>f())
  
  text.innerHTML +='end......<br>'

输出如下:
在这里插入图片描述
答案很容易就被猜出来的。但是输出结果并不重要,重要的是现象。
A1A2 以及B1B2仿佛是分成了两次渲染出来的! 这才是关键

说明如下:

  • 一次事件循环,只会处理一个消息序列。由于A1A2Timer是同时到期的,因此会被划分到同一个消息序列中,而一次事件循环只渲染一次,所以A1A2同时被渲染。
  • 同理,B1B2也是同样的情形;但是要注意:B1B2的到期时间与A1A2的并不同,因此它们分为两次事件循环处理的。

基本过程如下:
在这里插入图片描述注意:事实上这次也渲染了,不过这不是我要讲的内容无关,因此略过。

在这里插入图片描述处理完毕后,渲染一次。

在这里插入图片描述最后一次事件循环, 渲染完毕后结束。

综上所述可以知道:

同一个消息序列中的task会共享同一次事件循环,并且会等待所有task处理完成后才会渲染

为什么我们要得到这个结论?
继续吧。

阻塞问题

大多数情况下,我们是不会感知到阻塞的,这一方面是CPU计算能力强悍,另一方面也是JS引擎高性能的原因。

不过偶尔也会出现例外,事实上,我们所说的宏任务基本上都是工作量较大的任务,例如我们的JS代码文件(少说也要有2000行代码吧),如果处理不好,就很容易阻塞(即响应时间超长)。

现在模拟一个阻塞任务,例如:

   var macroTasks =[]
 // 在下次事件循环中,新增一个普通任务,
 macroTasks.push(genTimer('01:Better to light one candle \
                           than to curse the darkness.',0))
// 新增一个超长事件的阻塞任务。
// 阻塞时长3s
 macroTasks.push(genTimer('XXXXXblocking long time!', 
           0,
            ()=>{
               console.log('i am running!');
               let c = Date.now();
               while((Date.now()-c)<=3000);
            }))
 text.innerHTML +='hello<br>'
 macroTasks.forEach(f=>f())
 
 text.innerHTML +='byebye!<br>'
   

注意:macroTasks中所有任务都是同时到期的,因此可知它们会被划分到同一个事件循环中;

然后输出如下(请耐心看下去):
在这里插入图片描述在上面的示例中,尽管将 microtasks中的所有内容都分到了统一个时间循环中,但它们并没有如我们所想的那般在 0s后输出。而是同时阻塞了3s。 这是又为何?

说明:

  • 因为microtask中的所有任务共享一次事件循环,并且只有事件循环的所有任务都处理完毕时才会发生渲染事件
  • 所以可以知道,microtask[0]microtask[1]只有都被处理完成后才能够渲染!但是由于microtask[1]产生了阻塞,最终导致了卡顿

所以可以得到下面的结论:

  • 如果消息序列中有一个task陷入阻塞,那么就会导致整个事件循环陷入阻塞,最终导致卡顿
    事实上,一旦事件循环陷入阻塞,也会影响到下次事件循环的运行。

接下来,当做我们不知道 macrotasks[1] 是阻塞任务。

上面的代码总是Hello之后就ByeBye!!了,内容完全没输出,这是没道理的。所以姑且为了用户体验着想,代码改成这样:

   var macroTasks =[]
   macroTasks.push(genTimer('XXXXXblocking long time!', 
           0.5,
            ()=>{
               console.log('i am running!');
               let c = Date.now();
               while((Date.now()-c)<=3000);
            }))
   macroTasks.push(genTimer('01:Be honest rather clever',1))
   macroTasks.push(genTimer('ByeBye',1))
   text.innerHTML +='hello<br>'
   macroTasks.forEach(f=>f())
 

注意:上面的代码中,macrotasks[0]macrotasks[1]以及macrotasks[2]的事件循环不同,它们已经被错开了。但是仍然被硬生生卡到3s后才输出。原因很简单,因为当前事件循环仍在处理中,所以就推迟了进入下次事件循环的时间

因此总结一条:永远不要阻塞事件循环,它是所有异步模型的黄金铁律。因为它不仅导致严重的卡顿,而且极其影响用户体验,更重要的是:事件循环阻塞就意味着更大的性能开销

因此我们只能在阻塞任务之前处理所有任务,但通常情况下仍不可避免的受其影响,例如阻塞任务的延迟时间为0s时,那么任何宏任务都会受阻塞影响, 惹怒用户第一步,循环阻塞想呕吐

微任务便是上面的一种解决方案(当然最直接的处理办法就把阻塞任务给Pass掉,但是大多数情况下,这种任务偏偏就很重要。)

使用微任务

微任务Microtask简单来说是能够快速完成的任务,并且它保证所有的tasks处理完成后(但仍然在UI Rendering前)进行处理完成。在ES8规范中,微任务用 Job 表示,嘛,不过喜欢 microtask的人更多些,两个术语表达的意思都是相同的。

最经常使用的微任务是Promise

例如下面代码:

    var macroTasks =[]
   macroTasks.push(genTimer('XXXXXblocking long time!', 
           0 ,
            ()=>{
               console.log('i am running!');
               let c = Date.now();
               while((Date.now()-c)<=3000);
            }))
   macroTasks.push(()=>{
       return new Promise(res=>{
           
          text.innerHTML+='lark in the clear air<br/>'
          res('success!');
       })
   })
   macroTasks.push(()=>{
       return new Promise(res=>{
          text.innerHTML+='ByeByeBye!<br/>'
       })
   })
   text.innerHTML +='hello<br>'
   macroTasks.forEach(f=>f()) 
  

输出:
在这里插入图片描述

过程图如下:
在这里插入图片描述
(注意: 上面的macroTasks中混入了两个微任务

虽然现在仍然还是受阻塞影响,但是至少表面上没什么卡顿。当然这只是一种实验;生产环境下无论如何也不要这样做。自此不再赘述。

微任务在浏览器环境下有三个:

  1. queueMicrotask: 微任务回调函数。
  2. Promise: Promise,最常用的
  3. MutationObserver: DOM树监听

这里面除了Promise其他都不怎么常用,有兴趣的可以去了解一下。不过微任务给人的感觉,就像是一个可以追加到宏任务后面的同步代码,微任务定义不重要,重要的是,微任务尽可能是体积较小的任务代码,不要尝试阻塞微任务,否则就失去了微任务的本来含义

将上面的代码再进一步改写:

 var text = document.querySelector('#text')
 function genMicrotask(str){
     return async ()=>{
         text.innerHTML += str + '<br>'
         return str ;
     }
 }
 
 var microTasks = [genMicrotask('01: For man is man and master of his fate.'),
                   genMicrotask('ByeBye!!!')]
     
 text.innerHTML='hello!!!<br>'
 microTasks.forEach(f=>f())

输出:

hello!!!
01: For man is man and master of his fate.
ByeBye!!!

很完美,至少比上次的看起来清爽了许多。
注意:async函数最终也返回一个settled的Promise

好了,微任务和宏任务就先到这里。
(? Promise放后面吧,相信看的人也不是零基础,总知道用法吧……)


更新事件循环模型

根据上面所有内容,加入macroTasksmicroTasks等元素,就是:

  1. 事件循环开启
  2. 新增Macrotasks设为当前Macrotasks
  3. 当前Macrotasks中按顺序取出任务
  4. 处理任务
    自上而下运行JS代码
    如果发出异步请求,然后将消息保存到新Macrotasks(若无则新建)中;
    如果存在Microtasks,那么:
    1. Microtasks中取出任务
    2. 运行代码
    3. 如果发出异步请求,然后将消息保存到新Macrotasks(若无则新建)中
    4. 如果存在Microtask,仍然将其添加到当前macrotask的Microtasks
  5. 检查当前Macrotasks是否为空,是则继续,否则转至 (3)
  6. 是否触发UI Rendering事件,是则立即进行视图渲染。 否则继续
  7. 是否有新增MacroTasks
    如果是,开始下一轮事件循环, 回到(2);否则继续。
  8. 确定再无事件,关闭事件循环。线程进入休眠;
    直至有事件发生,新建消息序列并保存消息,转至(1)。

其实关于事件循环可以简单记作为:

  1. 任何事件循环都是以 Macrotasks --> MicroTasks --> UI Rendering 顺序进行的。
  2. 新增的任何 Microtask 只会保存在 当前事件循环的 Microtasks中。
  3. 新增的任何 Macrotask 只会保存在 新增Macrotasks 中,它是下一轮事件循环的 Macrotasks

最后

原来我心想能带入 NodeJS 的东西, 但是未曾想 NodeJS的底层细节如此复杂,远不是Javascript事件循环模型能概括得了的。对于不明白的原理,小生不敢自以为是,因此只能稍作安排。libuv好难啊。。。

限于篇幅,只能说这么多……但是关于这部分内容涉及知识量极大,有谬误之处,还请慷慨指正,不胜感激。

同步转发:
https://juejin.im/post/5ecfa657f265da76ed484fd9

碳纳米管NRAM:突破5纳米制程的下一代存储技术 在半导体存储领域,非易失性存储器和随机存取存储器是两大基础技术方向。传统DRAM速度快但断电易失,NAND Flash非易失但写入慢、寿命有限,两者在先进制程下都面临物理极限挑战。碳纳米管作为一种新型纳米材料,凭借其优异的导电性、机械强度和尺寸缩潜力,为存储技术革新提供了物理基础。基于碳纳米管的NRAM技术,通过控制纳米管间的可逆接触实现电阻状态切换,兼具DRAM级速度和Flash的非易失性,同时具备近乎无限的擦写寿命和出色的CMOS工艺兼容性。这项技术不仅能满足5纳米以下制程的存储需求,在数据中心缓存、 阅读详情

相关推荐

相机标定原理与张氏标定法

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 欢迎使用Markdown编辑器 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Mar

heeweii的博客 2243

JS——事件循环机制

两点重要: 先执行任务 每次任务结束,执行任务序列中所有任务 ,接着执行下一次任务 *任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境) 任务主要包含:**Promise、*MutaionObserver、process.nextTick(Node.js 环境) console.log('script start'); setTimeout(function() { conso

weixin_38271901的博客 211

linux菜鸟进阶系列之Linux常用命令(新手必看)分享.pdf

linux菜鸟进阶系列之Linux常用命令(新手必看)分享.pdf

精读Javascript系列(三) 执行上下文、 执行栈、初识事件循环

前言 这时可以接触真正实用的东西了,毕竟变量也不能代表整个Javascript语言,虽然有些不可思议,但变量的确是Javascript必经之路之一,关于变量的奇特行为数不胜数(真的是这样),不过这些我想高阶Javascript都努力回避这些,新手也不懂,所以我就跳过了。 下面的这些概念,无论是执行上下文、 还是执行栈,它在规范中的概念都很抽象,很多内容的理解实际靠的都是想象力,若有错误之处,还请指正。 执行上下文 简而言之,执行上下文(Execution Context)是正在运行的可执行代码所处环境的抽象

krfwill的博客 5056

前端精读】彻底吃透 JavaScript 执行机制

关注并将「趣谈前端」设为星标每早08:30按时推送技术干货/优秀开源/技术思维前言 The last time, I have learned【THE LAST TIME】一直是我想写的一...

趣谈前端 540

前端精读周刊:现代JavaScript异步编程模型详解

你是否曾被JavaScript中的回调地狱折磨得晕头转向?或者在使用async/await时不经意间写出了性能低下的串行代码?本文将深入剖析现代JavaScript异步编程模型,从Event Loop机制到Promise、async/await的最佳实践,帮你彻底理解异步编程的本质,写出高效、可维护的异步代码。 读完本文你将掌握: - JavaScript事件循环(Event Loop)的工作原...

gitblog_00314的博客 793

[书籍精读]《JavaScript异步编程》精读笔记分享

写在前面 书籍介绍:本书讲述基本的异步处理技巧,包括PubSub、事件模式、Promises等,通过这些技巧,可以更好的应对大型Web应用程序的复杂性,交互快速响应的代码。理解了JavaScript的异步模式可以让读者写出结构更合理、性能更出色、维护更方便的JavaScript程序。 第一章 深入理解JavaScript事件 1.1.事件的调度 JavaScript代码用于不会被中断,因为代...

小磊哥er @ 资深研发工程师 486

前端精读周刊:前端异步编程模式

你是否还在为回调地狱而烦恼?是否在Promise链式调用中迷失方向?是否对async/await的性能陷阱感到困惑?本文将带你深入探索前端异步编程的演进历程,从回调函数到Promise,再到async/await,解析每种模式的优缺点及适用场景,助你写出高效、优雅的异步代码。 ## 异步编程的前世今生 JavaScript作为一门单线程语言,异步编程一直是其核心特性。从最原始的回调函数,到Pr...

gitblog_00223的博客 419

Node.js:面试官喜欢问什么

八股精网站对Node.js面试题关键词统计的深度解读与学习指引,结合数据趋势为面试者提供高效备考策略和核心能力聚焦方向

ocean2103的专栏 740

精读《深入浅出Node.js》

前言 如果你想要深入学习Node,那你不能错过《深入浅出Node.js》这本书,它从不同的视角介绍了 Node 内在的特点和结构。由首章Node 介绍为索引,涉及Node 的各个方面,主要内容包含模块机制的揭示、异步I/O 实现原理的展现、异步编程的探讨、内存控制的介绍、二进制数据Buffer 的细节、Node 中的网络编程基础、Node...

weixin_34151004的博客 220

前端精读周刊

原文https://github.com/dt-fe/weekly Type Name Latest commit message Commit time   assets 68 4 months ago   01.精读 js 模块化发展.md fix: sort by file name a year ago   02.精...

大灰狼的小绵羊哥哥的博客 538

你不知道的JavaScript三部曲阅读计划

好的!以下是,精确到,帮助你系统性攻破《你不知道的JavaScript》三卷。:打通作用域、闭包、this、原型链四大核心概念。

以码平川的博客 994

Python 多线程 / 多进程 / 异步 IO 选型指南:高并发场景下的 8 组性能实测

技术没有银弹。是大力出奇迹的举重运动员。Threading是灵活的杂技演员。AsyncIO是处理海量信息的指挥家。在 2026 年的今天,随着 Python 3.14+ 即将带来的 No-GIL(PEP 703)正式落地,多线程的地位可能会迎来史诗级加强。但在那之前,掌握这三把斧头,足够你解决 99% 的 Python 性能难题。

BUG猿的博客 757

web前端年刊-2019年(90篇精选文章)

2019年web前端年刊,收录了包括JavaScript、框架和小程序、Node.js、浏览器和网络及性能优化、前端和Serverless、实践经验和开发思想、前端工程、数据结构与算法等方面的好文。 JavaScriptJavaScript 代码简洁之道 来自Github项目clean-code-javascript的翻译文章,文章作者根据自己的理解对原文进行了一些修改。 JavaScr...

枫星辰的博客 1631

为什么nodejs是单进程的_Nodejs内存限制与解决方案

什么是node内存,为什么需要内存限制在32位系统限制 0.7GB64位系统限制 1.4GB导致的问题: Node 无法直接操作大文件对象。例如我想读取一个 4g 的文件来处理,即使物理内存有 32GB,在单个 Node 进程中也是不能完全的使用的。我们平常在声明一些对象的时候,要是没有Node垃圾回收机制回收 ,就会占用V8限制的内存为什么有内存限制内存限制主要原因是v8的垃圾回收制度...

weixin_39641334的博客 760

2026前端面试真相:源码+文档+场景驱动的工程能力体检

JavaScript作为前端核心语言,其能力评估已从语法记忆跃迁至工程化实践。理解V8引擎优化机制、原型链污染防御、异步流控制等底层原理,是构建高性能与高安全应用的基础;而源码阅读能力、技术文档解析能力与真实业务场景建模能力,共同构成现代前端工程师的核心素养。这些能力直接支撑防抖Hook设计、Virtual DOM实现、分片上传架构、React Fiber调试等高频工程任务,并在大厂校招与社招终面中以‘压力测试’形式落地验证。

weixin_33858336的博客 452

2024年2024前端面试经常被问到的题(附答案)_前端面试题,面试web前端开发自我介绍怎么写

由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】由于篇幅限制,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

2301_82243710的博客 1096

C/C++工程师成长:从开源库深度研读到面试实战

在C/C++开发领域,深入理解系统底层原理和工程实践是工程师进阶的关键。从内存管理、并发编程到网络通信,掌握这些核心概念是构建高性能应用的基础。开源库作为工业级代码的集大成者,其设计思想、内存模型和性能优化策略,为开发者提供了绝佳的学习范本。通过研读如Muduo、libevent、Folly等经典库,不仅能深化对Reactor模型、无锁数据结构等技术的理解,更能将知识转化为解决实际问题的工程能力。这种从原理到实践的学习路径,尤其契合大厂面试中对系统设计、性能调优和复杂问题排查的考察要求,是C/C++工程师实

weixin_33824385的博客 238

最新2024前端面试经常被问到的题(附答案)_前端面试题(1),头条前端面试节奏

推荐一些系统学习的途径和方法。每个Web开发人员必备,很权威很齐全的Web开发文档。作为学习辞典使用,可以查询到每个概念、方法、属性的详细解释,注意使用英文关键字搜索。里面的一些 HTML,CSS,HTTP 技术教程也相当不错。开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】好了,这就是整理的前端从入门到放弃的学习笔记,还有很多没有整理到,我也算是边学边去整理,后续还会慢慢完善,这些相信够你学一阵子了。

2401_84419009的博客 350

腾讯前端暑期提前批面试全流程与技术要点解析

前端开发作为现代Web应用的核心技术领域,其技术栈涵盖HTML、CSS、JavaScript等基础语言,以及React、Vue等主流框架。理解浏览器渲染原理、性能优化策略和工程化实践是前端工程师的必备技能。在大型互联网企业如腾讯的面试中,面试官通常会深入考察闭包、事件循环JavaScript核心概念,以及Flex布局、CSS渲染优化等前端基础技术。从技术原理到工程实践,前端开发需要兼顾代码质量和性能指标,特别是在处理大文件上传、前端监控系统设计等高并发场景时。腾讯等一线大厂的前端面试特别注重候选人对Web

weixin_34195546的博客 363
上一篇: 精读Javascript系列(六)并发编程、 Javascript异步框架
下一篇: 精读Javascript系列(八)事件循环细则 II: NodeJS事件循环相关
krfwill
博客等级 码龄11年 38粉丝 16原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值