# 事件循环系统

不管是浏览器还是 Node 环境,都采用了单线程的执行模型(未来 JS 可能会有多线程,但不是现在),因此引入了事件队列/事件循环的概念。和多线程模型不同,在 JS 中如果某段代码执行时间过长是会阻塞线程的,在浏览器端体现为用户卡顿。

# 事件循环 & 消息队列

想要理解事件循环系统是必须要看源码的,它本质上就是死循环加上消息队列。

while (1) {
    // 执行消息队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);
}

# setTimeout

在 JS 中的 setTimeout() 函数是如何实现的呢?

需要延迟的任务会放到单独的延迟队列。

while (1) {
    // 执行消息队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);

    // 执行延迟队列中的任务
    ProcessDelayTask();
}

通过上面的代码可以看到,延迟队列的优先级是最高的,别的任务都要排队,而延迟队列是插队的。ProcessDelayTask()会将延迟队列中所有已经到期的任务拿出来执行。

# setTimeout 为何不准时?

先介绍一个专业基础知识。多线程模型中,一个任务执行到一半是可以被高优先级任务打断的。首先会保存当前的上下文信息,然后切换到高优先级任务执行。执行完毕后,恢复上下文信息,并继续执行低优先级任务。

但是事件循环系统不一样,它是单线程的,任务在执行的过程中是无法被打断的。因此如果当前任务执行时间太久,会导致定时器任务晚于预期时间才能得到执行。

# setTimeout 嵌套调用

如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。

可以拿这段代码做实验,当嵌套调用次数超过 5 次,Chrome 会将最小时间间隔设置为 4 毫秒。

let n = 10;
function cb() {
    if (n > 0) setTimeout(cb, 0);
    n--;
    console.log((new Date()).getMilliseconds());
}
setTimeout(cb, 0);

因此 setTimeout() 不适合用于做动画,可以用 window.requestAnimationFrame()

# XMLHttpRequest

在 Ajax 编程中,我们通过 XHR 异步请求一个资源,JS 代码如下:

let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () { }
xhr.send();

浏览器内部也是通过事件循环系统实现的,渲染进程通过 IPC 通知网络进程加载资源。等资源加载完毕后,网络进程通过 IPC 往消息队列里面添加一个任务。

# 宏任务 & 微任务

只有 Promise 和 MutationObserver 可以创建微任务。

一个宏任务内对应一个微任务队列,只有所有微任务执行完毕这个宏任务才算完成。