美文网首页
事件循环

事件循环

作者: 欢西西西 | 来源:发表于2022-10-27 18:29 被阅读0次

JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中,JavaScript只在一个线程上运行,同时只能执行一个任务,其他任务都必须在后面排队等待

同步与异步任务
在主线程上排队执行的任务是同步任务,这些任务一起形成执行栈
不进入主线程,而是在任务队列中等待进入主线程的,属于异步任务

一、js运行是单线程的

1.1 浏览器渲染进程的多个线程

事件监测和添加任务是独立于事件循环的,否则执行 js 时发生的事件将会被忽略;

- 线程 职能 - -
1 GUI渲染线程 解析html、css,构建DOM树、构建渲染树和绘制页面 重绘或回流 -
2 定时器触发线程 setTimeout和setInterval所在的线程,负责计时 定时器到期 →事件触发线程
3 异步http请求线程 XMLHttpRequest连接后新开一个线程 状态变更而且有回调 →事件触发线程
4 浏览器事件线程 监听浏览器事件(click,mouseover之类) 用户触发浏览器事件 →事件触发线程
5 事件触发线程 控制事件循环 接收到来自其他线程的事件触发 该线程会把事件添加到待处理队列的队尾
- - 宏任务队列维护在这个线程 当js引擎空闲时 取一个宏任务给js引擎执行
6 js引擎线程 解析执行js代码(微任务队列维护在这个线程,所以同步代码执行完时先执行所有的微任务,然后空闲了,事件触发线程会推任务过来) 当有宏任务来时 -

1.2 webworker

  • 创建worker时,js引擎向浏览器申请开一个子线程
  • 这个线程与主线程的上下文环境不一样,不能操作DOM,不能使用window对象(因为window是浏览器提供给js引擎的)
  • 但是可以读取一些浏览器属性例如navigator,location
  • JS引擎进程和worker进程之间通过浏览器提供的postMessage API互相通信

1.3 宏任务和微任务

宏任务 微任务
维护在事件触发线程 维护在js引擎线程
宿主发起的任务(最通用的JavaScript宿主环境是网页浏览器或Node) js引擎发起的任务
script整体代码块、setTimeout/setInterval注册的回调、http请求注册的回调 promise.then/catch/finally注册的回调 、 MutationObserver (DOM 发生变化) 、 node.js中的 process.nextTick

可以理解成js引擎自己能处理的异步就维护在自己的线程,属于微任务。
需要别人辅助的异步任务是在别人的线程里(定时器,http请求),这些任务属于宏任务

二、执行顺序

主线程执行栈为空 → 处理 所有 微任务队列中的任务 → 需要的话重新渲染页面 → 处理下一个宏任务 →
主线程执行栈为空 → 处理 所有 微任务队列中的任务 → ......

2.1 事件循环

<1> script代码块在主线程执行:所有同步代码立即执行,异步的进入Event Table并注册函数当指定的事情完成时,Event Table会将这个函数移入Event Queue。(比如定时器到期时或者Promise对象resolved时,这里会区分宏任务和微任务,进入各自对应的队列
<2> 主线程内的任务执行完毕,微任务队列中的每个任务依次进入主线程执行
<3> 需要的话重新渲染页面
<1> 下一个宏任务进入主线程执行
<2> 执行每个微任务
<3> 需要的话重新渲染页面
<1> 下一个宏任务进入主线程执行
......

微任务队列和宏任务队列中的任务不断进入主线程执行,这个过程是循环不断的,这种运行机制又称为Event Loop,事件循环是js实现异步的一种方法,也是js的执行机制

这种机制保证了:虽然JavaScript是单线程的,但是对于耗时任务,可以将它丢到任务队列中,不影响主线程代码执行。

2.2 代码示例:

<script>
    setTimeout(function () {
        console.log('setTimeout');
    })

    new Promise(function (resolve) {
        console.log('promise');
        resolve()
    }).then(function () {
        console.log('then');
    })

    console.log('console');
</script>

主线程

  • 遇到setTimeout,注册回调函数,并分发到宏任务Event Queue
  • new Promise立即执行,输出promise
  • then注册回调函数,并分发到微任务Event Queue
  • console.log立即执行,输出console

检查微任务队列

  • 执行then当时注册的回调函数,输出then

检查宏任务队列

  • 执行setTimeout当时注册的回调函数,输出setTimeout

检查微任务队列

  • 没有微任务

三、注意

  1. 代码不是任务,所以console.log()这句代码不是任务,更不是宏任务

  2. 单次事件循环最多处理一个宏任务,但所有微任务都会被处理;

  3. 当微任务队列执行完时,事件循环会检查是否需要更新 UI;如果一个宏任务执行完,微任务队列中有微任务,则微任务执行之前不允许重新渲染页面

  4. 单次事件循环不应该超过 16ms,因为浏览器试图每秒渲染 60 次,1000/60=16.66,因为任务执行时不能更新视图,所以超出这个时间会导致页面看起来卡顿;

  5. 使用定时器,可以将一个长时间的、阻塞页面的任务优化为多个任务,因为 2 个任务之间是可以渲染页面的;

四、setTimeout

  1. setTimeout 是经过指定时间后,把要执行的任务加入到 Event Queue 中,主线程执行完后来 Event Queue 中读取,但是如果此任务之前的任务执行时间太久,会导致真正执行 setTimeout 回调的时间点超过预期设置的时间。MDN上给出的最小延迟时间是4ms

  2. setTimeout(fn,0),我们想让fn在本次同步代码执行完之后尽可能早的执行。比如下面,我想让这个延时0的任务在延时500ms的任务之前执行,它一定能实现吗?

  • 2.1 【√】 延时0的定时器先到期,先进入队列,先执行
setTimeout(() => {
    console.log('执行500ms延迟任务');
}, 500);

console.log('同步');

setTimeout(() => {
    console.log('执行0ms延迟任务')
}, 0);
image.png
  • 2.2 【×】 延时500ms的定时器先到期,先进入队列,先执行。
    因为同步代码执行的太慢啦,500ms延时的定时器都到期啦,主线程甚至还没有执行到第二个定时器那里,所以这个延时0的任务排在了延时500ms的任务之后
setTimeout(() => {
    console.log('执行500ms延迟任务');
}, 500);

// 然后一段长时间执行的同步代码
console.time('test');
var arr = new Array(15000000).fill(10).map(a => {
    return { num: 19 + a };
});
console.timeEnd('test');

setTimeout(() => {
    console.log('执行0ms延迟任务')
}, 0);
image.png
  • 2.3 【√】 把想尽早执行的代码设置成微任务,那它永远都会在延时500ms那个任务之前执行啦。
    毕竟主线程执行完会先执行所有的微任务,然后才是宏任务。所以要插队还得是微任务
setTimeout(() => {
    console.log('执行500ms延迟任务');
}, 500);

// 然后一段长时间执行的同步代码
console.time('test');
var arr = new Array(15000000).fill(10).map(a => {
    return { num: 19 + a };
});
console.timeEnd('test');

// setTimeout(() => {
//     console.log('执行0ms延迟任务')
// }, 0);
Promise.resolve().then(() => {
    console.log('执行0ms延迟任务');
});
image.png

五、setInterval的2个问题

→ 如果主线程执行太久,期间也许有多次间隔定时器到期的行为,但如果到期时队列中有排队的此任务,该次到期就会被忽略,并不会重复添加。

→ 【但是】如果setInterval的回调函数fn执行时间超过了延迟时间。此时在浏览器的可能会表现为连续执行,也就是在浏览器中,如果到期时上个定时任务正在执行,并不会忽略本次到期,而是加入队列

由于setInterval的这两个问题,有时我们会使用setTimeout来模拟setInterval

问题1:到期被忽略

  • 条件:主线程执行期间,定时器到期多次

  • 设想:
    如果在主线程执行期间,定时器到期多次,并每次都加入队列,那么在主线程执行完后,队列中等待的多个定时任务会连续执行,就无法达到2次定时任务之间至少间隔指定时间的效果了

  • 测试:

    // 1秒钟到期1次的定时器
    var timer = setInterval(() => {
        console.log('每间隔1s定时执行:', Date.now())
    }, 1000);
    
    // 执行时间较长的任务
    console.time('test');
    var arr1 = new Array(5000000).fill(10).map(a => {
        return { num: 19 + a }
    })
    console.timeEnd('test')
    console.log('主线程执行完毕-start', Date.now());
image.png

循环执行了30s,定时器每秒到期一次,如果这30s内每次定时器到期都将任务加入队列,则主线程执行完毕后会立即执行多个任务。测试截图中:主线程执行完毕后仅立即执行了一个定时任务,之后的都是间隔1s,说明这30s内就只加入了一个定时任务,不会重复添加

问题2:连续执行

  • 条件:定时任务执行时间超过了间隔时间
  • 设想:
    假设定时任务每秒到期一次,执行每个定时任务需要3秒。也就是说,定时器到期时,可能有上个定时任务正在执行。如果此次也将任务加入队列,那么上个任务执行完也有可能会立刻执行,就无法达到2次任务之间的最小间隔时间了
  • 测试:
    【但是】在浏览器中测试了一下,确实是上次执行结束就立刻执行下次了,也就是说定时器到期时并未被忽略。一旦setInterval的回调函数fn执行时间超过了延迟时间,那么就完全看不出来有时间间隔了。
    Chrome
Firefox
Edge.png

相关文章

  • 浅谈JavaScript事件循环与Vue的批量异步更新策略

    在介绍事件循环之前,首先要明确以下几个关键概念。事件循环,同步和异步任务,宏任务,微任务。 一.事件循环 事件循环...

  • 并发:事件循环 & asyncio

    1. 事件循环机制 1.1. 什么是事件循环 事件循环(Event Loop),即通过轮询方法监控事件; asyn...

  • 探索未知种族之osg类生物---呼吸分解之事件循环一

    事件循环和更新循环 终于到了我们嘴里经常念叨的事件循环、更新循环以及渲染循环了。首先我们来区分一下事件循环和渲染循...

  • 事件循环

    先来一张图 下面上写的代码,在看浏览器的主线程的执行情况 再来上主线程页面初始化加载时的情况 setInterva...

  • 事件循环

    事件触发不会马上执行回调,会加入队列,队列中按照先进先出的顺序,逐个执行事件绑定的回调方法 新事件产生后会插在队尾...

  • 事件循环

    总结:第一次循环先执行宏任务中的队头任务,清空调用栈后执行微任务,然后第二次循环执行宏任务的队头任务,.........

  • 事件循环

    原文链接:https://zhuanlan.zhihu.com/p/26229293最近琢磨了好久的Javascr...

  • 事件循环

    单线程 .JavaScript是单线程javascript是单线程,无论后面加了什么标准,什么操作,都不能改变ja...

  • 事件循环。。

    Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高。 Node.js 的每一个 A...

  • 事件循环

网友评论

      本文标题:事件循环

      本文链接:https://www.haomeiwen.com/subject/wxeazrtx.html