深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环深入理解 JavaScript 的事件循环 EventLoop 一 什么是事件循环 JS 的代码执行是基于一种事件循环的机制 之所以称作事件循环 MDN 给出的解释为因为它经常被用于类似如下的方式来实现 while queue waitForMessa queue processNextM 如果当前没有任何消息 queue waitForMes

深入理解JavaScript的事件循环(Event Loop)

 

一、什么是事件循环

JS的代码执行是基于一种事件循环的机制,之所以称作事件循环,MDN给出的解释为因为它经常被用于类似如下的方式来实现

while (queue.waitForMessage()) { queue.processNextMessage(); } 

如果当前没有任何消息queue.waitForMessage 会等待同步消息到达

我们可以把它当成一种程序结构的模型,处理的方案。更详细的描述可以查看 这篇文章

而JS的运行环境主要有两个:浏览器Node

在两个环境下的Event Loop实现是不一样的,在浏览器中基于 规范 来实现,不同浏览器可能有小小区别。在Node中基于 libuv 这个库来实现

JS是单线程执行的,而基于事件循环模型,形成了基本没有阻塞(除了alert或同步XHR等操作)的状态

二、Macrotask 与 Microtask

根据 规范,每个线程都有一个事件循环(Event Loop),在浏览器中除了主要的页面执行线程 外,Web worker是在一个新的线程中运行的,所以可以将其独立看待。

每个事件循环有至少一个任务队列(Task Queue,也可以称作Macrotask宏任务),各个任务队列中放置着不同来源(或者不同分类)的任务,可以让浏览器根据自己的实现来进行优先级排序

以及一个微任务队列(Microtask Queue),主要用于处理一些状态的改变,UI渲染工作之前的一些必要操作(可以防止多次无意义的UI渲染)

主线程的代码执行时,会将执行程序置入执行栈(Stack)中,执行完毕后出栈,另外有个堆空间(Heap),主要用于存储对象及一些非结构化的数据

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

一开始

宏任务与微任务队列里的任务随着:任务进栈、出栈、任务出队、进队之间交替着进行

从macrotask队列中取出一个任务处理,处理完成之后(此时执行栈应该是空的),从microtask队列中一个个按顺序取出所有任务进行处理,处理完成之后进入UI渲染后续工作

需要注意的是:microtask并不是在macrotask完成之后才会触发,在回调函数之后,只要执行栈是空的,就会执行microtask。也就是说,macrotask执行期间,执行栈可能是空的(比如在冒泡事件的处理时)

然后循环继续

常见的macrotask有:

  • run <script>(同步的代码执行)
  • setTimeout
  • setInterval
  • setImmediate (Node环境中)
  • requestAnimationFrame
  • I/O
  • UI rendering

常见的microtask有:

  • process.nextTick (Node环境中)
  • Promise callback
  • Object.observe (基本上已经废弃)
  • MutationObserver

macrotask种类很多,还有 dispatch event事件派发等

run <script>这个可能看起来比较奇怪,可以把它看成一段代码(针对单个<script>标签)的同步顺序执行,主要用来描述执行程序的第一步执行

dispatch event主要用来描述事件触发之后的执行任务,比如用户点击一个按钮,触发的onClick回调函数。需要注意的是,事件的触发是同步的,这在下文有例子说明

注:

当然,也可认为 run <script>不属于macrotask,毕竟规范也没有这样的说明,也可以将其视为主线程上的同步任务,不在主线程上的其他部分为异步任务

三、在浏览器中的实现

先来看看这段蛮复杂的代码,思考一下会输出什么

 console.log('start'); var intervalA = setInterval(() => { console.log('intervalA'); }, 0); setTimeout(() => { console.log('timeout'); clearInterval(intervalA); }, 0); var intervalB = setInterval(() => { console.log('intervalB'); }, 0); var intervalC = setInterval(() => { console.log('intervalC'); }, 0); new Promise((resolve, reject) => { console.log('promise'); for (var i = 0; i < 10000; ++i) { i === 9999 && resolve(); } console.log('promise after for-loop'); }).then(() => { console.log('promise1'); }).then(() => { console.log('promise2'); clearInterval(intervalB); }); new Promise((resolve, reject) => { setTimeout(() => { console.log('promise in timeout'); resolve(); }); console.log('promise after timeout'); }).then(() => { console.log('promise4'); }).then(() => { console.log('promise5'); clearInterval(intervalC); }); Promise.resolve().then(() => { console.log('promise3'); }); console.log('end'); 

上述代码结合了常规执行代码,setTimeout,setInterval,Promise

答案为

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

在解释为什么之前,先看一个更简单的例子

 console.log('start'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); console.log('end'); 

大概的步骤,文字有点多

1. 运行时(runtime)识别到log方法为一般的函数方法,将其入栈,然后执行输出 start 再出栈

2. 识别到setTimeout为特殊的异步方法(macrotask),将其交由其他内核模块处理,setTimeout的匿名回调函数被放入macrotask队列中,并设置了一个 0ms的立即执行标识(提供后续模块的检查)

3. 识别到Promise的resolve方法为一般的方法,将其入栈,然后执行 再出栈

4. 识别到then为Promise的异步方法(microtask),将其交由其他内核模块处理,匿名回调函数被放入microtask队列中

5. 识别到log方法为一般的函数方法,将其入栈,然后执行输出 end 再出栈

6. 主线程执行完毕,栈为空,随即从microtask队列中取出队首的项,

这里队首为匿名函数,匿名函数里面有 console的log方法,也将其入栈(如果执行过程中识别到特殊的方法,就在这时交给其他模块处理到对应队列尾部),

输出 promise后出栈,并将这一项从队列中移除

7. 继续检查microtask队列,当前队列为空,则将当前macrotask出队,进入下一步(如果不为空,就继续取下一个microtask执行)

8.检查是否需要进行UI重新渲染等,进行渲染…

9. 进入下一轮事件循环,检查macrotask队列,取出一项进行处理

所以最终的结果是

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

再看上面那个例子,对比起来只是代码多了点,混入了setInterval,多个setTimeout与promise的函数部分,按照上面的思路,应该不难理解

需要注意的三点:

而输出promise1之后,为什么没有马上输出promise2呢?因为此时promise1所在任务之后是promise3的任务,1和3在promise函数内部返回后就添加至队列中,2在1执行之后才添加

再来看个例子,就有点微妙了

 <script> console.log('start'); setTimeout(() => { console.log('timeout1'); }, 0); Promise.resolve().then(() => { console.log('promise1'); }); </script> <script> setTimeout(() => { console.log('timeout2'); }, 0); requestAnimationFrame(() => { console.log('requestAnimationFrame'); }); Promise.resolve().then(() => { console.log('promise2'); }); console.log('end'); </script> 

输出结果

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

requestAnimationFrame是在setTimeout之前执行的,start之后并不是直接输出end,也许这两个<script>标签被独立处理了

来看一个关于DOM操作的例子,Tasks, microtasks, queues and schedules

<style type="text/css"> .outer { width: 100px; background: #eee; height: 100px; margin-left: 300px; margin-top: 150px; display: flex; align-items: center; justify-content: center; } .inner { width: 50px; height: 50px; background: #ddd; } </style> <script> var outer = document.querySelector('.outer'), inner = document.querySelector('.inner'), clickTimes = 0; new MutationObserver(() => { console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { console.log('click'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); outer.setAttribute('data-click', clickTimes++); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); // inner.click(); // console.log('done'); </script>

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

点击内部的inner块,会输出什么呢?

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

MutationObserver优先级比promise高,虽然在一开始就被定义,但实际上是触发之后才会被添加到microtask队列中,所以先输出了promise

两个timeout回调都在最后才触发,因为click事件冒泡了,事件派发这个macrotask任务包括了前后两个onClick回调,两个回调函数都执行完之后,才会执行接下来的 setTimeout任务

期间第一个onClick回调完成后执行栈为空,就马上接着执行microtask队列中的任务

如果把代码的注释去掉,使用代码自动 click(),思考一下,会输出什么?

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

可以看到,事件处理是同步的,done在连续输出两个click之后才输出

而mutate只有一个,是因为当前执行第二个onClick回调的时候,microtask队列中已经有一个MutationObserver,它是第一个回调的,因为事件同步的原因没有被及时执行。浏览器会对MutationObserver进行优化,不会重复添加监听回调

四、在Node中的实现

在Node环境中,macrotask部分主要多了setImmediate,microtask部分主要多了process.nextTick,而这个nextTick是独立出来自成队列的,优先级高于其他microtask

不过事件循环的的实现就不太一样了,可以参考 Node事件文档 libuv事件文档

Node中的事件循环有6个阶段

  • timers:执行setTimeout() 和 setInterval()中到期的callback
  • I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
  • idle, prepare:仅内部使用
  • poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
  • check:执行setImmediate的callback
  • close callbacks:执行close事件的callback,例如socket.on("close",func)

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

每一轮事件循环都会经过六个阶段,在每个阶段后,都会执行microtask

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

比较特殊的是在poll阶段,执行程序同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限

接下来再检查有无预设的setImmediate,如果有就转入check阶段,没有就先查询最近的timer的距离,以其作为poll阶段的阻塞时间,如果timer队列是空的,它就一直阻塞下去

而nextTick并不在这些阶段中执行,它在每个阶段之后都会执行

看一个例子

setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); console.log(5); 

根据以上知识,应该很快就能知道输出结果是 5 3 4 1 2

修改一下

process.nextTick(() => console.log(1)); Promise.resolve().then(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => { process.nextTick(() => console.log(0)); console.log(4); }); 

输出为 1 3 2 4 0,因为nextTick队列优先级高于同一轮事件循环中其他microtask队列

修改一下

process.nextTick(() => console.log(1)); console.log(0); setTimeout(()=> { console.log('timer1'); Promise.resolve().then(() => { console.log('promise1'); }); }, 0); process.nextTick(() => console.log(2)); setTimeout(()=> { console.log('timer2'); process.nextTick(() => console.log(3)); Promise.resolve().then(() => { console.log('promise2'); }); }, 0); 

输出为

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

与在浏览器中不同,这里promise1并不是在timer1之后输出,因为在setTimeout执行的时候是出于timer阶段,会先一并处理timer回调

setTimeout是优先于setImmediate的,但接下来这个例子却不一定是先执行setTimeout的回调

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

 

深入理解JavaScript的事件循环(Event Loop) 一、什么是事件循环

 

因为在Node中识别不了0ms的setTimeout,至少也得1ms.

所以,如果在进入该轮事件循环的时候,耗时不到1ms,则setTimeout会被跳过,进入check阶段执行setImmediate回调,先输出 immediate

如果超过1ms,timer阶段中就可以马上处理这个setTimeout回调,先输出 timeout

修改一下代码,读取一个文件让事件循环进入IO文件读取的poll阶段

let fs = require('fs');

    fs.readFile('./event.html', () => {
        setTimeout(() => {
            console.log('timeout');
        }, 0);

        setImmediate(() => {
            console.log('immediate');
        });
    });

这么一来,输出结果肯定就是 先 immediate 后 timeout

五、用好事件循环

知道JS的事件循环是怎么样的了,就需要知道怎么才能把它用好

1. 在microtask中不要放置复杂的处理程序,防止阻塞UI的渲染

2. 可以使用process.nextTick处理一些比较紧急的事情

3. 可以在setTimeout回调中处理上轮事件循环中UI渲染的结果

4. 注意不要滥用setInterval和setTimeout,它们并不是可以保证能够按时处理的,setInterval甚至还会出现丢帧的情况,可考虑使用 requestAnimationFrame

5. 一些可能会影响到UI的异步操作,可放在promise回调中处理,防止多一轮事件循环导致重复执行UI的渲染

6. 在Node中使用immediate来可能会得到更多的保证

7. 不要纠结

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/232192.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • js layui 弹出子窗体_Layui弹出层 加载 做编辑页面的方法[通俗易懂]

    js layui 弹出子窗体_Layui弹出层 加载 做编辑页面的方法[通俗易懂]layui是一款优秀的模块化前端框架。利用layui弹出层做编辑页面先上效果图基本准备,引入layui的layui.css,layui.js文件Js方法/***页面内弹出编辑窗口//需要引入layui.jslayui.css文件*@param{}title标题不显示为false*@param{}area大小[“400px”,”500px”]或者”400px”—&…

    2022年5月16日
    114
  • adb命令修改手机分辨率_adb命令查看安卓版本

    adb命令修改手机分辨率_adb命令查看安卓版本打开所要查看的应用包名:$adbshelldumpsysactivitytop|head-n10TASKcom.ss.android.article.newsid=5ACTIVITYcom.ss.android.article.news/com.ss.android.article.base.activity.DetailActivity4407b468pid=2714…

    2022年8月13日
    1
  • 网络流媒体协议之——RTSP协议

    网络流媒体协议之——RTSP协议RTSP(Real-TimeStreamProtocol)协议是一个基于文本的多媒体播放控制协议,属于应用层。RTSP以客户端方式工作,对流媒体提供播放、暂停、后退、前进等操作。该标准由IETF指定,对应的协议是RFC2326。RTSP作为一个应用层协议,提供了一个可供扩展的框架,使得流媒体的受控和点播变得可能,它主要用来控制具有实时特性的数据的发送,但其本身并不用于传送流媒体数据,而

    2022年7月16日
    19
  • 百度网盘下载提速,推荐3种亲测有效的方法

    百度网盘下载提速,推荐3种亲测有效的方法凉透的下载工具自从PanDownload事件之后,陆续出了很多第三方的度盘不限速下载神器,但是最后都凉了,这些第三方下载神器,都是个人开发者,即便有盈利也承受不起巨大的风险。甚至有款下载神器,用爱发电!流程是这样的,1.用户提交下载链接,2.然后开发者先下载好资源(当然开发者开的是超级会员),3.最后下载完成后保存到阿里云,然后再发送给用户。当然最后还是凉凉了!比如PDown、Dupan、忆寻,最终还是都死掉了,很可惜!现在还有没有百度网盘加速下载的方法呢?这里从解决实际问题的角度上,给大家

    2022年4月28日
    53
  • LPCTSTR 用法

    LPCTSTR 用法LPCTSTR用法

    2025年6月26日
    2
  • Android layout_Android源码

    Android layout_Android源码LayoutParams源码分析LayoutParams是布局参数的意思,我们在XML布局文件里的layout_xxx等属性都是对LayoutParams的描述。LayoutParams不属于View,是ViewGroup控制View的具体显示在哪里。

    2022年9月21日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号