Javascript--Event Loops

本文介绍了JavaScript的事件循环机制,详细阐述了单线程、任务队列、宏任务与微任务的区别,以及它们在事件循环中的执行顺序。通过解析规范和实践案例,帮助读者掌握Promise、MutationObserver等在事件循环中的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

Javascript是单线程的。而在主线程之外,为了协调事件、用户交互、脚本、渲染、网络等,用户代理(user agents,一般指浏览器)必须使用一种机制来保证程序的正确运行。这种机制就是事件循环(event loops)。当主线程(栈)执行完毕后,就开始事件循环。事件循环有两种形式,分别是browsing contextsworkers(Web Worker)

Browsing contexts是一个浏览器展示Document的环境(一般来说是一个浏览器标签页,但也有可能是一个页面内的window或者frame)。

Web Works是H5的一个API,用来开辟一个新的线程。在Worker内部不能访问主程序的任何资源。与主程序的交互通过postMessagemessage事件。但是Worker内部仍可以执行网络操作(Ajax、WebSocket)以及定时器等。所以这里也有独立的事件循环。

关键点

有几个概念需要知道。

  1. 一个事件循环有一个或者多个task queues。
  2. 事件循环中的每一个task和Document相关联。如果这个任务在一个元素的上下文排队,那么这个任务是该元素的node document
  3. 每一个任务来自于某个特定的任务源(task source)。
    • DOM操作事件源(添加删除dom元素)
    • 用户交互事件源
    • 网络事件源(响应网络活动)
    • history事件源(history.back()等)
  4. 当用户代理添加一个任务的时候,根据任务源不同,会被添加到不同的task queues。
  5. 每个task queues保证任务是先进先出的。
  6. 对于不同的task queues,用户代理可能分配的时间不同。比如对于鼠标和键盘事件所在的task queues,用户代理可能分配3/4的时间,同时也不会让其他的task queues得不到执行。
  7. 每一个事件循环有一个currently running task(当前运行的任务)。开始,它是null。每当执行一个任务的时候,currently running task即这个正在执行的任务。执行完成后,被重置为null。

Jobs and Job Queues and promise

在讲macro-task和micro-task之前,这部分内容起到承上启下的作用。该部分内容在es6(ecma-262)规范

Job只能在没有running execution contextexecution context stack为空的时候执行(我的猜测是当没有正在执行的任务并且主线程栈没有任务的时候)。一个PendingJob是一个对于未来执行的Job的请求。

一个Job Queue是一个FIFO(先进先出)的关于PendingJob records的队列。每一个Job Queue都有一个ECMAScript实现(implementation)的名字。每一个ECMAScript实现至少有以下两个Job Queue。

NamePurpose
ScriptJobsJobs that validate and evaluate ECMAScript Script and Module source text. See clauses 10 and 15.
PromiseJobsJobs that are responses to the settlement of a Promise (see 25.4).

这里强调以下,PromiseJobs是用来处理Promise的。

ecma-262规范25.4.5.3.1可以看到,对于一个promise.then的操作,不管结果是被fulfilled(resolve)还是rejected(reject),最终都会被添加到PromiseJobs。promise.catch会调用promise.then。

这里写图片描述

Promise/A+规范2.2.4也提到:

onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].

也就是说只有当execution context stack只包含平台代码不包含项目代码的时候,resolve和reject才会执行。那么什么是平台代码呢?

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

平台代码指引擎、环境和promise实现代码。实际上,这要求确保resolve和reject是在事件循环执行到then函数后异步执行。这种异步执行的机制可以通过macro-task机制(比如setTimeoutsetImmediate)实现,也可以通过micro-task机制(比如MutationObserver或者process.nextTick)实现。因为promise实现考虑到平台代码,promise自己可能包括一个task-scheduling队列或者trampoline

但是根据上文ecma-262规范,promise.then中执行的函数会被添加到PromiseJobs队列。

micro-task & macro-task

实际上,ecma-262规范中并没有提到macro-task。但是根据某些大神的博文和某些相关概念推测,规范中的task queue即macro-task queue。job queue即micro-task queue。所以promise.then中的函数会被添加到micro-task。结合上述的Promise/A+规范,我们可以得到一些概念:

macro-task: setTimeout、setImmediate、传递事件对象Event、HTML解析、任务的回调、请求网络资源完成、操作DOM(除了前面两个,后面的来自于whatwg规范
micro-task: mutationObserver、process.nextTick(和宿主环境有关)、promise.then

有了上述基础,下面来谈谈事件循环机制。

事件循环

如果用一段伪代码来表示事件循环:

var someTaskQueue = [];
var task;

while(true) {
  if(someTaskQueue.length > 0) {
    task = someTaskQueue.shift(); // 先进先出

    try {
      task();
    } catch (err) {
      handleError(err);
    }
  }
}

每一轮循环称作一次tick。每一次tick都会执行事件循环队列中的一个任务。这里的任务即macro-task。所以每次tick都执行一个macro-task。那什么时候执行micro-task呢?这确实是个问题。在《你不知道的JavaScript中卷》看到:

我认为对于任务队列(这里指micro-task,原文是job queue)最好的理解方式就是,它是挂在事件循环队列的每个tick(每个macro-task)之后的一个队列。在事件循环的每个tick中,可能出现的异步动作(process.then等)不会导致一个完成的新事件添加到事件循环队列中(不会成为一个新的tick,节约资源),而会在当前tick的任务队列(micro-task)末尾添加一个任务。

也就是说,micro-task队列会挂载当前tick(某个macro-task)的尾部。当当前这个macro-task执行完毕后,就会执行micro-task队列。理论上来说,micro-task会导致无限循环(比如process.nextTick中一直调用process.nextTick),导致无法进行下一个macro-task。

whatwg规范8.1.4.2中提到了event loop具体是怎么执行的。大致如下:

  1. 找到一个oldest Task(macro-task)执行,如果找不到,就进入micro-task。
  2. 设置事件循环的currently running task为这个任务。
  3. 运行这个任务(原子操作)。
  4. 设置事件循环的currently running task为null。
  5. 从macro-task队列中移除该oldest Task。
  6. 运行micro-task(步骤同上)。
  7. 更新rendering(涉及到一系列操作)。
  8. 处理Web Worker开辟的事件循环(如果有的话)。

好吧,一个简单的问题

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);

作者:何幻
链接:http://www.jianshu.com/p/3ed992529cfc
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

如果还有不明白的,可以去上面网址看看解析。new Promise是一个构造函数,其中的代码是同步的。所以会在主线程执行。所以3 4会首先打印。

参考

Browsing contexts from https://whatwg-cn.github.io
Browsing contexts from https://developer.mozilla.org
Event loops from https://whatwg-cn.github.io
[JavaScript] Macrotask Queue和Microtask Queue from http://www.jianshu.com
从Promise来看JavaScript中的Event Loop、Tasks和Microtasks from https://github.com/creeperyang
Promise/A+ from https://promiseaplus.com
MutationObserver from https://developer.mozilla.org
Vue 中如何使用 MutationObserver 做批量处理? from https://www.zhihu.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值