js- 宏微任务和事件loop

2024-09-05 11:58
文章标签 js 事件 任务 loop 宏微

本文主要是介绍js- 宏微任务和事件loop,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

宏微任务和事件loop 目录

文章目录

  • 前言
  • 推荐阅读
  • 宏微任务的定义
  • 宏微任务的区别
    • 常见面试代码
  • 宏任务
  • 微任务
  • `Event-Loop`
  • 在浏览器中的表现
  • `Node`中的表现
  • setImmediate与setTimeout的区别
  • `process.nextTick`
  • `async/await`函数
  • 小节


前言

  • 面试常问三问题
  • 宏微任务面对异步事件
  • 宏微事件、Event-Loop

推荐阅读

  • jiasm

宏微任务的定义

  • JavaScript是一个单线程的脚本语言。

也就是说在一行代码执行的过程中,必然不会存在同时执行的另一行代码,就像使用alert()以后进行疯狂console.log,如果没有关闭弹框,控制台是不会显示出一条log信息的。
亦或者有些代码执行了大量计算,比方说在前端暴力破解密码之类的鬼操作,这就会导致后续代码一直在等待,页面处于假死状态,因为前边的代码并没有执行完。

如果全部代码都是同步执行的,这会引发很严重的问题,比方说我们要从远端获取一些数据,难道要一直循环代码去判断是否拿到了返回结果么?

就像去饭店点餐,肯定不能说点完了以后就去后厨催着人炒菜的,会被揍的。

于是就有了异步事件的概念,注册一个回调函数,比如说发一个网络请求,我们告诉主程序等到接收到数据后通知我,然后我们就可以去做其他的事情了。

然后在异步完成后,会通知到我们,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,可以去执行。

比如说打了个车,如果司机先到了,但是你手头还有点儿事情要处理,这时司机是不可能自己先开着车走的,一定要等到你处理完事情上了车才能走。

  • 宏微任务面对异步事件

img

宏微任务的区别

例子:

这个就像去银行办业务一样,先要取号进行排号。

一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。

因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。

所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。

  • 任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中

就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号

  • 一个宏任务在执行的过程中,是可以添加一些微任务的

而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。

所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。

也许老大爷在办完理财以后还想 再办一个信用卡?或者 再买点儿纪念币

无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。

  • 在当前的微任务没有执行完成时,是不会执行下一个宏任务的。

常见面试代码

setTimeout(_ => console.log(4))new Promise(resolve => {resolve()console.log(1)
}).then(_ => {console.log(3)
})console.log(2)

在这里插入图片描述

  • setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务

  • 上述代码的执行顺序就是按照序号来输出的

  • 所有会进入的异步都是指的事件回调中的那部分代码

  • 也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的

  • 在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。

所以就得到了上述的输出结论1、2、3、4

本来setTimeout已经先设置了定时器(相当于取号),然后在当前进程中又添加了一些Promise的处理(临时添加业务)。

即便我们继续在Promise中实例化Promise,其输出依然会早于setTimeout的宏任务

setTimeout(_ => console.log(4))new Promise(resolve => {resolve()console.log(1)
}).then(_ => {console.log(3)Promise.resolve().then(_ => {console.log('before timeout')}).then(_ => {Promise.resolve().then(_ => {console.log('also before timeout')})})
})console.log(2)
  • 实际情况下很少会有简单的这么调用Promise的,一般都会在里边有其他的异步操作,比如fetchfs.readFile之类的操作

  • 而这些其实就相当于注册了一个宏任务,而非是微任务

  • 在Promise/A+的规范中,Promise的实现可以是微任务,也可以是宏任务,但是普遍的共识表示(至少Chrome是这么做的),Promise应该是属于微任务阵营的

哪些操作是宏任务、哪些是微任务就变得很关键,这是目前业界比较流行的说法:

宏任务

#浏览器Node
I/O
setTimeout
setInterval
setImmediate×
requestAnimationFrame×
  • 有些地方会列出来UI Rendering,说这个也是宏任务,可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤
  • requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行

微任务

#浏览器Node
process.nextTick×
MutationObserver×
Promise.then catch finally

Event-Loop

JavaScript是一个单进程的语言,同一时间不能处理多个任务,所以何时执行宏任务,何时执行微任务?我们需要有这样的一个判断逻辑存在。

每办理完一个业务,柜员就会问当前的客户,是否还有其他需要办理的业务。***(检查还有没有微任务需要处理)***
而客户明确告知说没有事情以后,柜员就去查看后边还有没有等着办理业务的人。***(结束本次宏任务、检查还有没有宏任务需要处理)***
这个检查的过程是持续进行的,每完成一个任务都会进行一次,而这样的操作就被称为Event Loop。(这是个非常简易的描述了,实际上会复杂很多)

而且就如同上边所说的,一个柜员同一时间只能处理一件事情,即便这些事情是一个客户所提出的,所以可以认为微任务也存在一个队列,大致是这样的一个逻辑:

const macroTaskList = [['task1'],['task2', 'task3'],['task4'],
]for (let macroIndex = 0; macroIndex < macroTaskList.length; macroIndex++) {const microTaskList = macroTaskList[macroIndex]for (let microIndex = 0; microIndex < microTaskList.length; microIndex++) {const microTask = microTaskList[microIndex]// 添加一个微任务if (microIndex === 1) microTaskList.push('special micro task')// 执行任务console.log(microTask)}// 添加一个宏任务if (macroIndex === 2) macroTaskList.push(['special macro task'])
}// > task1
// > task2
// > task3
// > special micro task
// > task4
// > special macro task

之所以使用两个for循环来表示,是因为在循环内部可以很方便的进行push之类的操作(添加一些任务),从而使迭代的次数动态的增加

以及还要明确的是,Event Loop只是负责告诉你该执行那些任务,或者说哪些回调被触发了,真正的逻辑还是在进程中执行的。

在浏览器中的表现

在上边简单的说明了两种任务的差别,以及Event Loop的作用,那么在真实的浏览器中是什么表现呢?
首先要明确的一点是,宏任务必然是在微任务之后才执行的(因为微任务实际上是宏任务的其中一个步骤)

I/O这一项感觉有点儿笼统,有太多的东西都可以称之为I/O,点击一次button,上传一个文件,与程序产生交互的这些都可以称之为I/O

假设有这样的一些DOM结构:

<style>#outer {padding: 20px;background: #616161;}#inner {width: 100px;height: 100px;background: #757575;}
</style>
<div id="outer"><div id="inner"></div>
</div>
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')function handler () {console.log('click') // 直接输出Promise.resolve().then(_ => console.log('promise')) // 注册微任务setTimeout(_ => console.log('timeout')) // 注册宏任务requestAnimationFrame(_ => console.log('animationFrame')) // 注册宏任务$outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务
}new MutationObserver(_ => {console.log('observer')
}).observe($outer, {attributes: true
})$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)

如果点击#inner,其执行顺序一定是:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout

因为一次I/O创建了一个宏任务,也就是说在这次任务中会去触发handler

按照代码中的注释,在同步的代码已经执行完以后,这时就会去查看是否有微任务可以执行,然后发现了PromiseMutationObserver两个微任务,遂执行之。

因为click事件会冒泡,所以对应的这次I/O会触发两次handler函数(一次在inner、一次在outer),所以会优先执行冒泡的事件(早于其他的宏任务),也就是说会重复上述的逻辑。
在执行完同步代码与微任务以后,这时继续向后查找有木有宏任务。

需要注意的一点是,因为我们触发了setAttribute,实际上修改了DOM的属性,这会导致页面的重绘,而这个set的操作是同步执行的,也就是说requestAnimationFrame的回调会早于setTimeout所执行。

使用上述的示例代码,如果将手动点击DOM元素的触发方式变为$inner.click(),那么会得到不一样的结果。
Chrome下的输出顺序大致是这样的:
click -> click -> promise -> observer -> promise -> animationFrame -> animationFrame -> timeout -> timeout

与我们手动触发click的执行顺序不一样的原因是这样的,因为并不是用户通过点击元素实现的触发事件,而是类似dispatchEvent这样的方式,我个人觉得并不能算是一个有效的I/O,在执行了一次handler回调注册了微任务、注册了宏任务以后,实际上外边的$inner.click()并没有执行完。

所以在微任务执行之前,还要继续冒泡执行下一次事件,也就是说触发了第二次的handler

所以输出了第二次click,等到这两次handler都执行完毕后才会去检查有没有微任务、有没有宏任务。

  1. .click()的这种触发事件的方式个人认为是类似dispatchEvent,可以理解为同步执行的代码
document.body.addEventListener('click', _ => console.log('click'))document.body.click()
document.body.dispatchEvent(new Event('click'))
console.log('done')// > click
// > click
// > done
  1. MutationObserver的监听不会说同时触发多次,多次修改只会有一次回调被触发。
new MutationObserver(_ => {console.log('observer')// 如果在这输出DOM的data-random属性,必然是最后一次的值,不解释了
}).observe(document.body, {attributes: true
})document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())// 只会输出一次 ovserver

Node中的表现

Node也是单线程,但是在处理Event Loop上与浏览器稍微有些不同,这里是Node官方文档的地址。

就单从API层面上来理解,Node新增了两个方法可以用来使用:微任务的process.nextTick以及宏任务的setImmediate

setImmediate与setTimeout的区别

在官方文档中的定义,setImmediate为一次Event Loop执行完毕后调用。
setTimeout则是通过计算一个延迟时间后进行执行。

但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。
因为如果主进程中先注册了两个任务,然后执行的代码耗时超过XXs,而这时定时器已经处于可执行回调的状态了。

所以会先执行定时器,而执行完定时器以后才是结束了一次Event Loop,这时才会执行setImmediate

setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))

img

如果后续添加一些代码以后,就可以保证setTimeout一定会在setImmediate之前触发了:

setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))let countdown = 1e9while(countdonn--) { } 
// 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行`setTimeout`再结束这一轮循环,也就是说开始执行`setImmediate`

如果在另一个宏任务中,必然是setImmediate先执行:

require('fs').readFile(__dirname, _ => {setTimeout(_ => console.log('timeout'))setImmediate(_ => console.log('immediate'))
})// 如果使用一个设置了延迟的setTimeout也可以实现相同的效果

process.nextTick

就像上边说的,这个可以认为是一个类似于PromiseMutationObserver的微任务实现,在代码执行的过程中可以随时插入nextTick,并且会保证在下一个宏任务开始之前所执行。

在使用方面的一个最常见的例子就是一些事件绑定类的操作:

class Lib extends require('events').EventEmitter {constructor () {super()this.emit('init')}
}const lib = new Lib()lib.on('init', _ => {// 这里将永远不会执行console.log('init!')
})

因为上述的代码在实例化Lib对象时是同步执行的,在实例化完成以后就立马发送了init事件。
而这时在外层的主程序还没有开始执行到lib.on('init')监听事件的这一步。
所以会导致发送事件时没有回调,回调注册后事件不会再次发送。

我们可以很轻松的使用process.nextTick来解决这个问题:

class Lib extends require('events').EventEmitter {constructor () {super()process.nextTick(_ => {this.emit('init')})// 同理使用其他的微任务// 比如Promise.resolve().then(_ => this.emit('init'))// 也可以实现相同的效果}
}

这样会在主进程的代码执行完毕后,程序空闲时触发Event Loop流程查找有没有微任务,然后再发送init事件。

关于有些文章中提到的,循环调用process.nextTick会导致报警,后续的代码永远不会被执行,这是对的,参见上边使用的双重循环实现的loop即可,相当于在每次for循环执行中都对数组进行了push操作,这样循环永远也不会结束

async/await函数

因为,async/await本质上还是基于Promise的一些封装,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似:

setTimeout(_ => console.log(4))async function main() {console.log(1)await Promise.resolve()console.log(3)
}main()console.log(2)
  • async函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调

小节

推荐几篇参阅的文章:

    • tasks-microtasks-queues-and-schedules
    • understanding-js-the-event-loop
    • 理解Node.js里的process.nextTick()
    • 浏览器中的EventLoop说明文档
    • Node中的EventLoop说明文档
    • requestAnimationFrame | MDN
    • MutationObserver | MDN

这篇关于js- 宏微任务和事件loop的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1138901

相关文章

JS常用组件收集

收集了一些平时遇到的前端比较优秀的组件,方便以后开发的时候查找!!! 函数工具: Lodash 页面固定: stickUp、jQuery.Pin 轮播: unslider、swiper 开关: switch 复选框: icheck 气泡: grumble 隐藏元素: Headroom

禁止平板,iPad长按弹出默认菜单事件

通过监控按下抬起时间差来禁止弹出事件,把以下代码写在要禁止的页面的页面加载事件里面即可     var date;document.addEventListener('touchstart', event => {date = new Date().getTime();});document.addEventListener('touchend', event => {if (new

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、路由模块化 4、路由模块添加前缀 四、中间件

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求,要求做一款播放器,发现能力上跟EasyPlayer.js基本一致,满足要求: 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏(单屏/全屏) 多分屏(2*2) 多分屏(3*3) 多分屏(4*4) 播放控制 播放(单个或全部) 暂停(暂停时展示最后一帧画面) 停止(单个或全部) 声音控制(开关/音量调节) 主辅码流切换 辅助功能 屏

使用JS/Jquery获得父窗口的几个方法(笔记)

<pre name="code" class="javascript">取父窗口的元素方法:$(selector, window.parent.document);那么你取父窗口的父窗口的元素就可以用:$(selector, window.parent.parent.document);如题: $(selector, window.top.document);//获得顶级窗口里面的元素 $(

js异步提交form表单的解决方案

1.定义异步提交表单的方法 (通用方法) /*** 异步提交form表单* @param options {form:form表单元素,success:执行成功后处理函数}* <span style="color:#ff0000;"><strong>@注意 后台接收参数要解码否则中文会导致乱码 如:URLDecoder.decode(param,"UTF-8")</strong></span>

js react 笔记 2

起因, 目的: 记录一些 js, react, css 1. 生成一个随机的 uuid // 需要先安装 crypto 模块const { randomUUID } = require('crypto');const uuid = randomUUID();console.log(uuid); // 输出类似 '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

FreeRTOS内部机制学习03(事件组内部机制)

文章目录 事件组使用的场景事件组的核心以及Set事件API做的事情事件组的特殊之处事件组为什么不关闭中断xEventGroupSetBitsFromISR内部是怎么做的? 事件组使用的场景 学校组织秋游,组长在等待: 张三:我到了 李四:我到了 王五:我到了 组长说:好,大家都到齐了,出发! 秋游回来第二天就要提交一篇心得报告,组长在焦急等待:张三、李四、王五谁先写好就交谁的