事件循环与任务队列

Author: bugall
Wechat: bugallF
Email: 769088641@qq.com
Github: https://github.com/bugall

一: 事件循环

虽然我们用Javascript总是可以实现一些异步代码, 但是Javascript中真正的异步概念,但是直到ES6,Javascript才内建了直接的异步概念。

对于原有Javascript引擎来说, 它只关心如何去执行给定的代码块, 对于什么时候该执行哪些代码块这个是引擎不关心的。引擎是依赖于宿主环境的,这里的宿主环境并不是指操作系统环境,因为不同的平台提供的“可执行环境”不同。而宿主环境就是为了隔离代码、语言与具体的平台而提出的一个设计。比如web浏览器环境,Node.js这样的工具等,所有的这些环境都有一个共同点,它们都提供了一种机制来处理程序中多个代码块的执行,且执行每块时调用Javascript引擎,这种机制被成为事件循环

换句话说,Javascript引擎本身并没有时间的概念,只是一个按需执行Javascript任意代码片段的环境。“事件”的调度总是由包含它的环境进行。

注意!!!,ES6后事件的管理方式有所改变。ES6本身解决的事件在哪里管理的问题,现在ES6精确指定了事件循环的工作细节,这就 意味着技术上将其纳如了Javascript引擎的势力范围,而不再由宿主环境管理,这个改变的一个主要原因是ES6中Promise的引入,这个技术要求事件循环队列的调度运行能够直接进行精细的控制

我们看下面的这段代码:

function task() {
    console.log("Hello Word");
}
setTimeout(task, 1000);

如果你在代码中设置一个计时器, 当计时器到达指定的时间后执行函数task, 当Javascript引擎执行到定时器的时候会通知宿主环境:“我要去做别的事情, 等1秒后就调用task函数,注意:是调用而不是执行)

我们用一段伪代码实现一个简单的事件循环

var eventList = [event1, event2, event3];
var event;
while(true) {
    if (eventList.length > 0 ) {
        event = eventList.pop();
    }
    run(event);
}

可以看到有一个用while循环实现的持续运行的循环,当event1, event2, event3都取出被执行一次后称为一轮,循环的每一轮称为一个tick,对于每一个tick而言,如果在队列中有等待的事件,那么就会从队列中取出下一个事件并执行,这些事件就是我们代码中写的回调函数。

需要注意的是,定时器比较特殊,setTimeout(task, 1000)并没有把回调函数挂在事件循环队列中,它所做的就是设置一个定时器,当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的tick会被取出执行。

如果你的事件循环中已经有很多项目后,定时器的回调就要被放到队尾( 不支持抢占式 )等待被执行,这也就是定时器不准的原因。定时器的回调函数的执行要根据时间队列的状态而定。
那么该如何去降低定时器误差呢?

二:任务队列

严格来说,定时器并不直接把回调函数直接插到事件循环队列,定时器会在有机会的时候插入事件,对于连续的两个setTimeout(..., 0)调用不能保证会严格按照调用顺序处理,所以各种情况都会发生,比如定时器漂移,这类结果是不可预测的,在Node.js中可以用process.nextTick(...)。但是不能保证所有环境都能控制异步的顺序。

在ES6中,在事件循环队列上有个一个新的概念,那就是任务队列,这个概念给大家带来的最大影响可能就是Promise的异步特性

任务队列就是挂在时间循环队列的每个tick之后的一个队列,在事件循环的每个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个任务。
一个任务可能引起更多任务被添加到同一个队列末尾,所以理论上说,任务循环可能无限循环,无法转移到下一个事件循环tick.

任务队列的概念,那么怎么减少定时器的误差?看代码:

function task() {
    console.log("Hello Word");
}
setTimeout(task, 1000);

如果现在时间队列中有100个等待被执行的任务,这时候task任务准备插入到事件队列。

没有引入任务队列前:
task会被插入到当前事件循环队列的末端,等待下次的tick被执行,那么这就需要等到当前的tick被执行完,那么这时候的timer延时就决定于100个等待执行的任务耗时。

引入任务队列之后:
直接插入到当前tick的任务队列被执行

### JavaScript 事件循环任务队列详解 #### 宏观视角下的事件循环机制 JavaScript 是一种单线程编程语言,这意味着在同一时间只能执行一段代码。为了有效处理异步操作而不阻塞主线程,JavaScript 使用了事件循环机制[^1]。 #### 同步异步任务的区别 在 JavaScript 中存在两种主要类型的可执行单元——同步任务和异步任务: - **同步任务**:这类任务会在调用栈中按顺序立即执行。 - **异步任务**:它们不会立刻被执行而是被安排在未来某个时刻触发,比如定时器回调函数、`Promise.then()` 方法指定的回调等。 当所有同步脚本执行完毕后,如果当前没有任何正在等待处理的消息,则进入下一轮事件循环周期,在此期间会检查是否有任何已准备好待处理的新消息加入到消息队列里去[^4]。 #### 微任务 vs 宏任务 根据 WHATWG 的定义,宏任务指的是那些由外部输入(如用户交互、网络请求)引发的任务;而微任务则是指某些特定条件下自动创建的小型内部任务,例如 `process.nextTick()`, `MutationObserver` 或者 Promise 链接中的 `.then()` 调用所形成的回调函数。值得注意的是,在每一个宏任务完成后都会清空一次微任务队列并依次执行其中所有的项目直到为空为止][^[^23]。 #### 多个宏任务队列的存在形式 不同于传统意义上的单一全局任务队列模型,现代浏览器实现了更为复杂的架构设计—即不同种类的任务会被分配至各自独立的宏任务队列当中。这种做法不仅提高了系统的灵活性也使得开发者能够更加精细地控制程序行为模式。具体来说就是像计时器(`setTimeout`)、I/O 操作以及绘制更新这样的活动都被分别放入对应的专用通道内排队等候调度[^2]。 ```javascript // 示例代码展示如何利用 setTimeout 和 promise 实现简单的异步流程控制 console.log('Start'); setTimeout(() => { console.log('This is a macro-task'); }, 0); new Promise((resolve) => { resolve(); }).then(() => { console.log('Microtask after start, before end.'); }); console.log('End'); ``` 上述例子展示了在一个完整的宏任务生命周期内的微任务执行情况。由于微任务总是在每轮事件循环结束之前被执行,因此即使设置了延迟时间为零秒 (`setTimeout(fn, 0)`), 其实际效果仍然要晚于同一批次产生的其他微任务完成之后才能显现出来。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值