如何理解JS事件的轮询机制

目录

🔽 概念

🔽 任务队列

🔽 JS如何实现异步操作

🔽 异步执行机制

🔽 JS中事件队列的优先级

宏任务队列 👇

微任务队列 👇



🔽 概念

事件轮询 (eventloop) 是"一个解决和处理外部事件时将它们转换为回调函数的调用的实体(entity)"

JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。


🔽 任务队列

“ 任务队列 " 是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空," 任务队列 " 上第一位的事件就会自动进入主线程。

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务

异步任务指的是,不进入主线程、而进入 " 任务队列 "(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。


🔽 JS如何实现异步操作

JS 的异步是通过回调函数实现的,即通过任务队列,在主线程执行完当前的任务栈(所有的同步操作),主线程空闲后轮询任务队列,并将任务队列中的任务(回调函数)取出来执行。

" 回调函数 "(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

虽然 JS 是单线程的,但是浏览器的内核是多线程的,在浏览器的内核中不同的异步操作由不同的浏览器内核模块调度执行,异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含上图中的3种 webAPI,分别是 DOM Binding、network、timer模块。

onclick 由浏览器内核的 DOM Binding 模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。

setTimeout 会由浏览器内核的 timer 模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。

ajax 则会由浏览器内核的 network 模块来处理,在网络请求完成返回之后,才将回调函数添加到任务队列中。


🔽 异步执行机制

1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

2. 主线程之外,还存在一个 " 任务队列 "(task queue)。只要异步任务有了运行结果,就在 " 任务队列 " 之中放置一个事件。

3. 一旦 " 执行栈 " 中的所有同步任务执行完毕,系统就会读取 " 任务队列 ",看看里面有哪些事件。于是那些对应的异步任务结束等待状态,进入执行栈,开始执行。

4. 主线程不断重复上面的第三步(事件轮询)


🔽 JS中事件队列的优先级

在 JS 的ES6 中新增的任务队列(promise)优先级是在事件循环之上的,事件循环每次 tick 后会查看 ES6 的任务队列中是否有任务要执行,也就是 ES6 的任务队列比事件循环中的任务(事件)队列优先级更高。

先执行同步代码再执行异步代码,先执行微任务再执行宏任务

setTimeout( fn, 0 ),它在 " 任务队列 " 的尾部添加一个事件,因此要等到同步任务和 " 任务队列 " 现有的事件都处理完,才会得到执行。

console.log(1)
setTimeout(function () {
	console.log(6)
}, 1000)
new Promise(function (resolve, reject) {
    console.log(2)
    setTimeout(function () {
        console.log(3)
    }, 500)
    resolve()
}).then(function (res) {
    console.log(4)
})
console.log(5)
 
// 执行结果:1 2 5 4 3 6

宏任务队列 👇

宏任务:由宿主环境提供,比如setTimeoutsetInterval、网络请求Ajax、用户I/Oscript(整体代码)、UI renderingsetImmediate(node)

微任务队列 👇

微任务:语言标准(ECMAScript)提供,如process.nextTick(node)PromiseObject.observe(已废弃)MutationObserver(html5新特性)

了解了宏任务队列和微任务队列以后,我们也可以使用它们简单概括一下事件循环(event loop)

网上很多有用异步任务和同步任务来说明这个问题的,但是我觉得用宏任务和微任务更好点。在网上看到一些文章也把同步任务,即script(整体代码)视为宏任务,这时的执行就是宏任务 => 微任务 => 宏任务 => 微任务…..的循环

1. 执行宏任务队列中第一个任务,执行完后移除它

2. 执行所有的微任务,执行完后移除它们

3. 执行下一轮宏任务(重复步骤2)

如此循环就形成了event loop,其中,每轮执行一个宏任务所有的微任务

下图很形象的描述了事件循环(event loop)机制的过程:

console.log(1);

setTimeout(function(){
    console.log(2)
},10);

new Promise(function(resolve){
    console.log(3)
    for( var i=100000 ; i>0 ; i-- ){
        i==1 && resolve()
    }
    console.log(4)
}).then(function(){
    console.log(5)
}).then(function(){ 
console.log(6) 
})

console.log(7);

打印出来的结果是:1 3 4 7 5 6 2

我们分析一下整个过程:👇

1. 首先执行主线程这个宏任务,从上到下执行,遇到console.log(1); 打印1出来

2. 遇到setTimeout,把它丢给定时器线程处理,然后继续往下执行,并不会阻塞10毫秒,而此处定时器线程会在,主线程执行完后的10毫秒,把回调函数放入宏任务队列。

3. 遇到new Promise,直接执行,先打印 ‘3‘ 出来,然后执行for循环,达到条件之后,把promise的状态改为resolved,继续执行打印 ‘4’ 出来

4. 遇到promise的then, 属于微任务,则把回调函数放入微任务队列

5. 又遇到promise的then, 属于微任务,则把回调函数放入微任务队列

6. 遇到console.log(7) 打印 ‘7’ 出来

7. 宏任务执行完后会执行所有待执行的微任务,所以会相继打印 ‘6’, ‘7’ 出来。

至此第一轮循环已经结束了,第一轮循环里的宏任务和微任务都会被移除出任务队列,接下来开启第二轮循环,

1. 首先查找是否有宏任务,由于setTimeout 的回调被放入了宏任务队列,这里会执行回调函数的代码,打印了 ‘2’ 出来

2. 接着查找是否有微任务,发现没有微任务,则本轮循环结束

接下来会重复上面的步骤,这就是event loop 了。后续当我们触发点击事件,有回调函数的话,回调函数也会被放入宏任务队列,一旦队列里重新有了任务,就会被执行。

👋🏻 扩展题目

如果能把上面这道题的流程说清楚,那么恭喜你,对event loop理解的不错了。 下面我们再利用上面的题目扩展一下,加深理解。

下面的代码打印出来的结果是什么?

console.log(1);

setTimeout(function(){
    new Promise(function(resolve){
    console.log('promise in setTimeout1');
    resolve();
    }).then(function(){
        console.log('then in setTimeout1');
    })
},10);

new Promise(function(resolve){
    console.log(3);
    for( var i=100000 ; i>0 ; i-- ){
        i==1 && resolve();
    }
    console.log(4)
}).then(function(){
    console.log(5);
});

setTimeout(function(){
    console.log('setTimeout2');
},10);

console.log(7);

结果如下:

可以发现,第二个setTimeout 的回调函数,执行的比第一个setTimeout里面的promise.then()的回调要晚,这是因为每次循环只执行一个宏任务,但是却会执行所有待执行的微任务,而第二个setTimeout在宏任务队列的位置在第一个setTimeout后面。


🔽 参考资料:

  • 8
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
浏览器的事件轮询机制是指浏览器在等待事件发生时,采用轮询的方式来检查是否有事件发生。这个机制是浏览器实现异步编程的重要手段之一。在浏览器中,事件可以是用户交互、网络请求、定时器等等。浏览器通过事件队列来管理这些事件,当事件发生时,会将事件加入到事件队列中,然后等待 JavaScript 引擎执行。 事件轮询机制的实现方式是通过一个事件循环来实现的。事件循环会不断地从事件队列中取出事件,然后执行相应的回调函数。当事件队列为空时,事件循环会进入休眠状态,等待新的事件加入到事件队列中。这个过程是不断重复的,直到浏览器关闭。 在事件轮询机制中,有一个重要的概念叫做“任务队列”。任务队列是一个存放任务队列,每个任务都是一个回调函数。当事件发生时,会将相应的回调函数加入到任务队列中。任务队列分为两种类型:宏任务和微任务。宏任务包括用户交互、网络请求、定时器等等,而微任务则是指 Promise 的回调函数、MutationObserver 的回调函数等等。 在事件轮询机制中,宏任务和微任务的执行顺序是不同的。当一个宏任务执行完毕后,会立即执行所有的微任务,然后再执行下一个宏任务。这个过程是不断重复的,直到事件队列为空。 总的来说,浏览器的事件轮询机制是一种非常重要的机制,它可以帮助我们实现异步编程,提高程序的性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

儒雅的烤地瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值