浏览器工作原理与实践
李兵
前盛大创新院高级研究员
56402 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 46 讲
浏览器工作原理与实践
15
15
1.0x
00:00/00:00
登录|注册

18 | 宏任务和微任务:不是所有任务都是一个待遇

微任务机制
异步调用
MutationObserver
Mutation Event
轮询检测
微任务早于宏任务执行
微任务执行时长影响宏任务时长
微任务和宏任务绑定
执行时机
微任务产生时机
微任务队列
时间粒度问题
任务执行流程
网络请求完成、文件读写完成事件
JavaScript脚本执行事件
用户交互事件
渲染事件
应用和重要性
实时性和效率权衡
XMLHttpRequest
setTimeout
代码输出结果解释
Promise工作原理
改进
演变历史
监听DOM变化需求
结论
V8引擎
事件循环机制
任务包括
主线程执行
微任务
WebAPI
思考时间
MutationObserver
微任务
宏任务
消息队列
宏任务和微任务

该思维导图由 AI 生成,仅供参考

在前面几篇文章中,我们介绍了消息队列,并结合消息队列介绍了两种典型的 WebAPI——setTimeoutXMLHttpRequest,通过这两个 WebAPI 我们搞清楚了浏览器的消息循环系统是怎么工作的。不过随着浏览器的应用领域越来越广泛,消息队列中这种粗时间颗粒度的任务已经不能胜任部分领域的需求,所以又出现了一种新的技术——微任务微任务可以在实时性和效率之间做一个有效的权衡
从目前的情况来看,微任务已经被广泛地应用,基于微任务的技术有 MutationObserver、Promise 以及以 Promise 为基础开发出来的很多其他的技术。所以微任务的重要性也与日俱增,了解其底层的工作原理对于你读懂别人的代码,以及写出更高效、更具现代的代码有着决定性的作用。
有微任务,也就有宏任务,那这二者到底有什么区别?它们又是如何相互取长补短的?

宏任务

前面我们已经介绍过了,页面中的大部分任务都是在主线程上执行的,这些任务包括了:
渲染事件(如解析 DOM、计算布局、绘制);
用户交互事件(如鼠标点击、滚动页面、放大缩小等);
JavaScript 脚本执行事件;
网络请求完成、文件读写完成事件。
为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制,渲染进程内部会维护多个消息队列,比如延迟执行队列和普通的消息队列。然后主线程采用一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任务称为宏任务
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

浏览器消息队列中的宏任务和微任务对代码执行顺序和性能有重要影响。宏任务包括渲染事件、用户交互事件、JavaScript脚本执行事件和网络请求完成等任务,而微任务在实时性和效率之间取得平衡,被广泛应用于MutationObserver、Promise等技术。了解宏任务和微任务的区别对于理解和编写高效、现代化的代码至关重要。本文介绍了宏任务的执行过程和局限性,强调了微任务在满足时间精度要求较高的任务方面的优势。此外,文章还详细介绍了微任务的产生和执行时机,以及MutationObserver技术方案的演化历程,最终指出MutationObserver采用了“异步+微任务”的策略,有效地权衡了实时性和执行效率的问题。通过本文的总结,读者可以快速了解宏任务和微任务的概念、微任务的工作流程以及MutationObserver技术方案的优势,为他们理解现代化代码执行机制提供了重要参考。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《浏览器工作原理与实践》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(84)

  • 最新
  • 精选
  • 阿桐
    买过不少专栏,每一篇都紧跟并且会反复看的目前只有这一个。一方面懒另一方面是好的系统性的学习资料不多,所以以前很少关注偏底层原理性的东西,所以这个专栏学习起来是既收获满满有时也不乏一额头问号。 这里有 2 个问题想向老师请教,希望老师百忙之中能抽空解答一下,多谢多谢。 1、之前讲过,在循环系统的一个循环中,先从消息队列头部取出一个任务执行,该任务执行完后,再去延迟队列中找到所有的过期任务依次执行完。那前面这句话和本篇文章的这句话好像有矛盾:"先从多个消息队列中选出一个最老的任务,这个任务称为 oldestTask" 2、”通常情况下,在当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。“ 在页面生存周期内,全局执行上下文只有一份并且会一直存在调用栈中,只有当页面被关闭的时候全局执行上下文才会消失。页面都快关闭了,把全局执行上下文中的微任务队列中的任务都执行一遍,好像也没啥意义。系统应该不会做没有意义的事情,所以应该是我对全局执行上下文的某处理解有问题,但我又自查不到。

    作者回复: 非常高兴你能提出这些问题。 你有这两个疑问很正常,说明你看得很仔细,之所以你会感到疑惑,主要是我在写作过程中偷懒了。 我先来解答你的第一个问题: 第一段话是WHATWG标准定义的,在WHATWG规范,定义了在主线程的循环系统中,可以有多个消息队列,比如鼠标事件的队列,IO完成消息队列,渲染任务队列,并且可以给这些消息队列排优先级。 但是在浏览器实现的过程中,目前只有一个消息队列,和一个延迟执行队列。 一个是规范,一个是实现,主要我没有在文中强调这点,所以你会产生的这样的疑问。 关于第二个问题解释起来就比较复杂了,涉及到来了V8是怎么执行的了,专栏中的"全局执行上下文"我没有深入分析。所以我偷懒了,把两个稍微有点不同的概念都称为了“全局执行上下文”,要解释清楚这个问题还要牵涉到V8的一个底层逻辑,既然你提出来了,那我就打算在课程结束后,通过加餐的形式来开一讲,讲清楚了这个还能额外地理解 Realm 概念。

    2019-09-17
    11
    136
  • 花儿与少年
    提问: Mutation Event的回调 是同步的吗?如果是同步的,引擎是怎么做到的? 同步代码执行的时候,还能插入其他代码(mutation 回调)?

    作者回复: 要理解这个就得讲观察者模式了,不过展开又是一篇文章,我到时候加餐的时候再来结合观察者模式来讲这个。

    2019-09-17
    2
    9
  • coder
    对于文中一处有疑虑: “第一种是把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。这种比较好理解,我们前面介绍的 setTimeout 和 XMLHttpRequest 的回调函数都是通过这种方式来实现的。” 第16讲提到了,setTimeout里的延迟任务,是存在一个延迟队列中的。我看精选留言部分老师的回答,提到这个延迟队列实际上是一个hashmap,那么setTimeout的实现还是加到队尾,等到前面的都出队,才执行到这个任务的吗?

    作者回复: setTimeout因为是定时任务,设定的时间间隔没有到是不会执行,由此需要一个单独的模块来保存定时器的消息,你可以通过该模块取出到期的任务,我们把这个模块叫延时队列,Chrome内部用了个hashmap保存数据,然后又写了取出到期的任务的策略! 通常情况下,是当渲染主线程在执行完一个正常的任务之后,再判断该模块中是否有到期的任务,如果有取出来执行!

    2019-12-22
    3
    4
  • HoSalt
    老师,通过控制面板中修改的样式是不是不会触发MutationObserver?

    作者回复: 不会,因为没有出发js,这个微任务是v8触发的

    2020-04-10
    2
    3
  • 夏了夏天
    老师,我有个疑惑,主线程读取消息队列里的任务的时机是「系统调用栈」的任务执行完毕时还是「JS的调用栈」执行完毕时?

    作者回复: js只不是任务的一个过程,这里讲的都是C++层面的,所以也可以说是系统调用栈。 具体的你可以看看加餐5

    2019-12-18
    1
  • 小智
    反复读了几次,还是有很多疑问,不同于最初的几篇文章,这里的理论偏多,希望能有更多的案例结合理论分析,才能进一步验证心中理解的

    作者回复: 嗯。后面有疑问正常,理论偏多,而且篇幅之间的依赖性比较强。可以把你的问题列出来,答疑的时候我会结合实际列子来分析。

    2019-09-19
    2
    1
  • 凭实力写bug
    执行流程很清楚,也知道了几个队列的关系,看完唯一迷惑的是那里算一个任务,对着performance执行流程就很清晰了

    作者回复: 后续有补充内容

    2019-11-02
  • man-moonth
    错误和缺失之处烦请老师指正: 1. 执行`p0 = new Promise(executor)`,立即调用`executor()`。依次打印`1`和`rand`,根据`rand > 0.5`判断执行`resolve()`还是`reject()`,分别决定了p0的状态为fufilled(成功)还是rejected(失败)。 2. 继续往下执行`p1 = p0.then()`、`p3 = p1.then()`、`p4 = p3.then()`、`p4.catch()`,`p0.then()`、`p1.then()`、`p3.then()`、`p4.catch()`等依次推入微任务队列,p1、p3、p4的状态变为pending(初始状态)。此处p4添加了`catch()`方法,若p4也有`then()`方法,那么推入队列的就是`p4.then().catch()`。 3. 执行`console.log(2)`。宏任务执行完毕。 4. 从微任务队列中取出`p0.then()`。如果p0的状态为fufilled,那么执行`p0.then()`:打印`succeed-1`,然后执行`new Promise(executor)`,完毕后p1的状态转为fufilled/rejected;如果p0的状态为rejected,则不执行`p0.then()`,p1的状态置为rejected。 5. 继续从微任务队列取出`p1.then()`、`p3.then()`,他们的处理方式与第3步同理。 6. 取出`p4.catch()`,如果p4的状态为rejected,那么执行`p4.catch()`,否则啥也不做。结束。
    2019-09-14
    4
    11
  • splm
    前面的课程举过说过,正常任务会被放在消息队列中,延时任务会放在延时消息队列中,还举过一段代码,任务循环会不断的从消息队列中取任务,并执行,也会不断的判断延时任务是否到期需要执行。但在这节课里面却说延迟任务会追加到消息队列末尾,听说去就像普通任务和延迟任务都在一起,只是延迟任务被追加到末尾。究竟有几个消息队列,普通和延迟队列是真实存在还是只是概念区分,实际两种任务都保存在一块。
    2019-10-16
    5
    8
  • ytd
    执行过程: 从第10行开始: 1,创建promise赋值,打印1 rand 2,执行log语句,打印2 3,如果rand > 0.5,promise被resolve,打印success,并返回新的promse赋值 然后重复类似步骤1、3、4 4,否则如果rand <= 0.5 promise reject,然后p1、p2、p3、p4都分别被赋值为一个新的被reject的promise,最后在p4.catch中reject状态被捕获,打印error
    2019-09-14
    2
    7
收起评论
显示
设置
留言
84
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部