任务与微任务

embedded/2024/10/17 18:33:25/

JavaScript 本质上是一门单线程语言。自从定时器(setTimeout()setInterval())加入到 Web API 后,浏览器提供的 JavaScript 环境就已经逐渐发展到包含 任务调度多线程应用开发 等强大的特性。

JavaScript 执行上下文

JavaScript 代码在运行的时候,实际上是运行在 执行上下文 当中。以下三种类型的代码会创建一个新的执行上下文:

  • 全局上下文:为运行代码主体而创建的执行上下文,是为了那些存在于 JavaScript 函数之外的代码而创建的。
  • 本地上下文:每个函数在执行的时候都会创建自己的执行上下文。
  • 使用 eval() 函数也会创建一个新的执行上下文。

每个上下文本质上都是一种作用域层级。每个代码段开始执行的时候都会创建一个新的上下文来运行它,并在代码退出的时候销毁。

javascript">let outputElem = document.getElementById("output");let userLanguages = {Mike: "en",Teresa: "es",
};function greetUser(user) {function localGreeting(user) {let greeting;let language = userLanguages[user];switch (language) {case "es":greeting = `¡Hola, ${user}!`;break;case "en":default:greeting = `Hello, ${user}!`;break;}return greeting;}outputElem.innerHTML += `${localGreeting(user)}<br>\r`;
}greetUser("Mike");
greetUser("Teresa");
greetUser("Veronica");

这段程序代码包含了三个执行上下文,其中有些会在程序运行的过程中多次创建和销毁。每个上下文创建的时候,都会被推入 执行上下文栈 。当退出的时候,又会从上下文栈中移除。

  • 程序开始运行时,全局上下文就会被创建好。
    • 当执行到 greetUser("Mike") 时,会为 greetUser() 函数创建一个上下文,并将这个上下文推入到执行上下文栈中。
      • greetUser() 调用 localGreeting() 的时候,会为该方法创建一个新的上下文。当 localGreeting() 返回的时候,它的上下文也会从执行栈中弹出并销毁。程序会从栈中获取下一个上下文并恢复执行,也就是从 greetUser() 剩下的部分开始执行。
      • greetUser() 执行完毕并退出,其上下文也从栈中弹出并销毁。
    • 当执行到 greetUser("Teresa") 的时候,程序又会为它创建一个上下文并推入栈顶。
      • greetUser() 调用 localGreeting() 的时候,会为该方法创建一个新的上下文。当 localGreeting() 返回的时候,它的上下文也会从执行栈中弹出并销毁。程序会从栈中获取下一个上下文并恢复执行,也就是从 greetUser() 剩下的部分开始执行。
      • greetUser() 执行完毕并退出,其上下文也从栈中弹出并销毁。
    • 当执行到 greetUser("Veronica") 的时候,程序又会为它创建一个上下文并推入栈顶。
      • greetUser() 调用 localGreeting() 的时候,会为该方法创建一个新的上下文。当 localGreeting() 返回的时候,它的上下文也会从执行栈中弹出并销毁。程序会从栈中获取下一个上下文并恢复执行,也就是从 greetUser() 剩下的部分开始执行。
      • greetUser() 执行完毕并退出,其上下文也从栈中弹出并销毁。
  • 主程序退出,全局执行上下文从执行栈中弹出。此时栈中所有的上下文都已经弹出,程序执行完毕

通过这种方式来使用执行上下文,每个程序和函数都能够拥有自己的变量和其他对象。每个上下文还能够额外的跟踪程序中下一行需要执行的代码以及一些对上下文非常重要的信息。通过这种方式来使用上下文和上下文栈,我们可以对程序运行的一些基础部分进行管理,包括局部和全局变量、函数的调用与返回等。

关于递归函数(即多次调用自身的函数),需要特别注意:每次递归调用自身都会创建一个新的上下文。这使得 JavaScript 运行时能够追踪递归的层级以及从递归中得到的返回值,但这也意味着每次递归都会消耗内存来创建新的上下文。

JavaScript 运行时

在执行 JavaScript 代码的时候,JavaScript 运行时实际上维护了一组用于执行 JavaScript 代码的 代理 。每个代理由一组执行上下文的集合、执行上下文栈、主线程、一组可能创建用于执行 worker 的额外的线程集合、一个任务队列以及一个微任务队列构成。除了主线程(某些浏览器在多个代理之间共享的主线程)之外,其他组成部分对该代理都是唯一的。

事件循环

每个代理都是由 事件循环 (Event loop)驱动的,事件循环负责收集事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务,然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操作。

网页或者 app 的代码和浏览器本身的用户界面程序运行在相同的 线程 中,共享相同的 事件循环 。该线程就是 主线程 ,它除了运行网页本身的代码之外,还负责收集和派发用户和其他事件,以及渲染和绘制网页内容等。

事件循环驱动着浏览器中发生的一切,因为它与用户的交互有关,但对于我们这里的目的来说,更重要的是它负责调度和执行在其线程中运行的每一段代码。

有如下三种事件循环:

Window 事件循环: 驱动所有共享同源的窗口(尽管这有进一步的限制,如下所述)。

Worker 事件循环: 循环驱动 worker 的事件循环。这包括所有形式的 worker,包括基本的 web worker、shared worker 和 service worker。Worker 被保存在一个或多个与“主”代码分开的代理中;浏览器可以对所有特定类型的工作者使用一个事件循环,也可以使用多个事件循环来处理它们。

Worklet 事件循环: 驱动运行 worklet 的代理。这包含了 Worklet、AudioWorklet 以及 PaintWorklet。

多个同源窗口可能运行在相同的事件循环中,每个队列任务进入到事件循环中以便处理器能够轮流对它们进行处理。记住这里的网络术语“window”实际上指的是“用于运行网页内容的浏览器级容器”,包括实际的 window、标签页或者一个 frame。

在特定情况下,同源窗口之间共享事件循环,例如:

  • 如果一个窗口打开了另一个窗口,它们可能会共享一个事件循环。
  • 如果窗口是包含在 <iframe> 中的容器,则它可能会和包含它的窗口共享一个事件循环。
  • 在多进程浏览器中多个窗口碰巧共享了同一个进程。

这种特定情况依赖于浏览器的具体实现,各个浏览器可能并不一样。

任务 vs 微任务

一个 任务 就是指计划由标准机制来执行的任何 JavaScript,如程序的初始化、事件触发的回调等。除了使用事件,你还可以使用 setTimeout() 或者 setInterval() 来添加任务。

任务队列和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在 下一次迭代开始之后才会被执行
  • 每次当一个任务退出且执行上下文栈为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,这些新的微任务将在下一个任务开始运行之前,在当前事件循环迭代结束之前执行。

在 JavaScript 中通过 queueMicrotask() 使用微任务

一个 微任务 (microtask)就是一个简短的函数,当创建该微任务的函数执行之后,并且只有 当 Javascript 调用栈为空 ,而控制权尚未返还给被用户代理用来驱动脚本执行环境的事件循环之前,该微任务才会被执行。事件循环既可能是浏览器的主事件循环也可能是被一个 web worker 所驱动的事件循环。这使得给定的函数在没有其他脚本执行干扰的情况下运行,也保证了微任务能在用户代理有机会对该微任务带来的行为做出反应之前运行。

JavaScript 中的 promise 和 Mutation Observer API 都使用微任务队列去运行它们的回调函数,但当能够推迟工作直到当前事件循环过程完结时,也是可以执行微任务的时机。为了允许第三方库、框架、polyfill 能使用微任务,在 Window 和 WorkerGlobalScope 接口上暴露了 queueMicrotask() 方法。

任务

一个 任务 (task)就是由执行诸如从头执行一段程序、执行一个事件回调或一个 interval/timeout 被触发之类的标准机制而被调度的任意 JavaScript 代码。这些都在 任务队列 (task queue)上被调度。

在以下时机,任务会被添加到任务队列:

  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 <script> 元素中运行代码)。
  • 触发了一个事件,将其回调函数添加到任务队列时。
  • 执行到一个由 setTimeout() 或 setInterval() 创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。

事件循环驱动你的代码按照这些任务排队的顺序,一个接一个地处理它们。在事件循环的单次迭代中,将执行任务队列中最旧的可运行任务。之后,微任务将被执行,直到微任务队列为空,然后浏览器可以选择更新渲染。然后浏览器继续进行事件循环的下一次迭代。

微任务

起初 微任务 (microtask)和任务之间的差异看起来不大。它们很相似;都由位于某个队列的 JavaScript 代码组成并在合适的时候运行。但是,只有在迭代开始时队列中存在的任务才会被事件循环一个接一个地运行,这和处理微任务队列是殊为不同的。

有两点关键的区别。

首先,每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如若不然,事件循环就会运行微任务队列中的所有微任务。接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。

其次,如果一个微任务通过调用 queueMicrotask(),向队列中加入了更多的微任务,则那些新加入的微任务会早于下一个任务运行。这是因为事件循环会持续调用微任务直至队列中没有留存的,即使是在有更多微任务持续被加入的情况下。

注意: 因为微任务自身可以入列更多的微任务,且事件循环会持续处理微任务直至队列为空,那么就存在一种使得事件循环无尽处理微任务的真实风险。如何处理递归增加微任务是要谨慎而行的。

入列微任务

应该使用微任务的典型情况,第一种是只有在没有其他办法的时候,第二种是当创建框架或库时需要使用微任务达成其功能的时候。已经存在一些入列微任务的方法(比如创建一个立即兑现的 promise),新加入的 queueMicrotask() 方法提供了一种标准的方式,可以安全的引入微任务而无需其他的复杂操作。

当使用 promise 创建微任务时,由回调抛出的异常被报告为 rejected promises 而不是标准异常。同时,创建和销毁 promise 带来了事件和内存方面的额外开销,这是正确入列微任务的函数应该避免的。

简单的传入一个 JavaScript Function,以在 queueMicrotask() 方法中处理微任务时供其上下文调用即可;取决于当前执行上下文,queueMicrotask() 以定义的形式被暴露在 Window 或 Worker 接口上。

javascript">queueMicrotask(() => {/* 微任务中将运行的代码 */
});

微任务函数本身没有参数,也不返回值。

使用微任务的时机

通常,微任务的使用场景关乎捕捉或检查结果、执行清理等;其时机晚于一段 JavaScript 执行上下文主体的退出,但早于任何事件处理函数、timeouts 或 intervals 及其他回调被执行。

使用微任务的最主要原因简单归纳为:确保任务顺序的一致性,即便当结果或数据是同步可用的,也要同时减少操作中用户可感知到的延迟而带来的风险。

保证条件性使用 promises 时的顺序

微任务可被用来确保执行顺序总是一致的一种情形,是当 promise 被用在一个 if…else 语句(或其他条件性语句)中、但并不在其他子句中的时候。考虑如下代码:

javascript">customElement.prototype.getData = url => {if (this.cache[url]) {this.data = this.cache[url];this.dispatchEvent(new Event("load"));} else {fetch(url).then(result => result.arrayBuffer()).then(data => {this.cache[url] = data;this.data = data;this.dispatchEvent(new Event("load"));)};}
};

这段代码带来的问题是,通过在 if…else 语句的其中一个分支(此例中为缓存中的图片地址可用时)中使用一个任务而 promise 包含在 else 子句中,我们面临了操作顺序可能不同的局势;比方说,像下面看起来的这样:

javascript">element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data...");
element.getData();
console.log("Data fetched");

连续执行两次这段代码会形成下表中的结果:

如果数据未缓存,输出结果如下:
Fetching data
Data fetched
Loaded data如果数据已缓存,输出结果如下:
Fetching data
Loaded data
Data fetched

甚至更糟的是,有时元素的 data 的属性会被设置,有时当这段代码结束运行时却不会被设置。

我们可以通过在 if 子句里使用一个微任务来确保操作顺序的一致性,以达到平衡两个子句的目的:

javascript">customElement.prototype.getData = url => {if (this.cache[url]) {queueMicrotask(() => {this.data = this.cache[url];this.dispatchEvent(new Event("load"));});} else {fetch(url).then(result => result.arrayBuffer()).then(data => {this.cache[url] = data;this.data = data;this.dispatchEvent(new Event("load"));)};}
};

通过在两种情况下各自都通过一个微任务( if 中用的是 queueMicrotask()else 子句中通过 fetch() 使用了 promise)处理了设置 data 和触发 load 事件,平衡了两个子句。

批量操作

也可以使用微任务从不同来源将多个请求收集到单一的批处理中,从而避免对处理同类工作的多次调用可能造成的开销。

下面的代码片段创建了一个函数,将多个消息放入一个数组中批处理,通过一个微任务在上下文退出时将这些消息作为单一的对象发送出去。

javascript">const messageQueue = [];let sendMessage = (message) => {messageQueue.push(message);if (messageQueue.length === 1) {queueMicrotask(() => {const json = JSON.stringify(messageQueue);messageQueue.length = 0;fetch("url-of-receiver", json);});}
};

sendMessage() 被调用时,指定的消息首先被推入消息队列数组。

如果我们刚加入数组的消息是第一条,就入列一个将会发送一个批处理的微任务。照旧,当 JavaScript 执行路径到达顶层,恰在运行回调之前,那个微任务将会执行。这意味着之后的间歇期内造成的对 sendMessage() 的任何调用都会将其各自的消息推入消息队列,但囿于入列微任务逻辑之前的数组长度检查,不会有新的微任务入列。

当微任务运行之时,等待它处理的可能是一个有若干条消息的数组。微任务函数先是通过 JSON.stringify() 方法将消息数组编码为 JSON。其后,数组中的内容就不再需要了,所以清空 messageQueue 数组。最后,使用 fetch() 方法将编码后的 JSON 发往服务器。

这使得同一次事件循环迭代期间发生的每次 sendMessage() 调用将其消息添加到同一个 fetch() 操作中,而不会让诸如 timeouts 等其他可能的定时任务推迟传递。

服务器将接到 JSON 字符串,然后大概会将其解码并处理其从结果数组中找到的消息。

举例

简单微任务

调用一次 queueMicrotask() 方法来调度一个微任务并使其运行。

javascript">log("Before enqueueing the microtask");
queueMicrotask(() => {log("The microtask has run.");
});
log("After enqueueing the microtask");// 输出结果
// Before enqueueing the microtask
// After enqueueing the microtask
// The microtask has run.

timeout 和微任务的示例

一个 timeout 在 0 毫秒后被触发(或者 “尽可能快”),演示当调用一个新任务(如通过使用 setTimeout())时的“尽可能快”意味着什么。调度一个 0 毫秒后触发的 timeout,而后入列了一个微任务。

javascript">let callback = () => log("Regular timeout callback has run");let urgentCallback = () => log("*** Oh noes! An urgent callback has run!");log("Main program started");
setTimeout(callback, 0);
queueMicrotask(urgentCallback);
log("Main program exiting");// 输出
// Main program started
// Main program exiting
// *** Oh noes! An urgent callback has run!
// Regular timeout callback has run

从主程序体中输出的日志首先出现,接下来是微任务中的输出,其后是 timeout 的回调。这是因为当处理主程序运行的任务退出后,微任务队列先于 timeout 回调所在的任务队列被处理。任务和微任务是保持各自独立的队列的,且微任务先执行有助于保持这一点。

来自函数的微任务

增加一个完成同样工作的函数,略微地扩展了前一个例子。该函数使用 queueMicrotask() 调度一个微任务。此例的重要之处是微任务不在其所处的函数退出时,而是在主程序退出时被执行。因为那才是任务退出而执行栈上为空的时刻。

javascript">let callback = () => log("Regular timeout callback has run");let urgentCallback = () => log("*** Oh noes! An urgent callback has run!");let doWork = () => {let result = 1;queueMicrotask(urgentCallback);for (let i = 2; i <= 10; i++) {result *= i;}return result;
};log("Main program started");
setTimeout(callback, 0);
log(`10! equals ${doWork()}`);
log("Main program exiting");// 输出
// Main program started
// 10! equals 3628800
// Main program exiting
// *** Oh noes! An urgent callback has run!
// Regular timeout callback has run

http://www.ppmy.cn/embedded/127757.html

相关文章

Github 2024-10-14开源项目周报Top14

根据Github Trendings的统计,本周(2024-10-14统计)共有14个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目7C++项目2C项目2Swift项目1Jupyter Notebook项目1Java项目1Rust项目1Python中的算法实现集合 创建周期:2831 天开发语言:Python协议…

C++ AVL树

大家好呀&#xff0c;我是残念&#xff0c;希望在你看完之后&#xff0c;能对你有所帮助&#xff0c;有什么不足请指正&#xff01;共同学习交流哦&#xff08;不能私学&#xff0c;谁私学谁是&#xff09; 本文由&#xff1a;残念ing原创CSDN首发&#xff0c;如需要转载请通知…

探索 Jupyter 核心:nbformat 库的神秘力量

文章目录 探索 Jupyter 核心&#xff1a;nbformat 库的神秘力量1. 背景介绍&#xff1a;为何选择 nbformat&#xff1f;2. nbformat 是什么&#xff1f;3. 如何安装 nbformat&#xff1f;4. 简单的库函数使用方法4.1 读取 Notebook 文件4.2 修改 Notebook 中的单元格4.3 添加 M…

洗衣店订单管理:Spring Boot技术实现

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常适…

C++、Python 、JavaScript、Java 、Go 编程资源大全中文版

C、Python、JavaScript、Java 、Go 编程资源大全中文版 https://github.com/jobbole C 资源大全中文版 https://github.com/jobbole/awesome-cpp-cn/blob/master/README.md Python 资源大全中文版 https://github.com/jobbole/awesome-python-cn JavaScript 资源大全中文…

【HarmonyOS】HMRouter使用详解(三)生命周期

生命周期&#xff08;Lifecycle&#xff09; 使用HMRouter的页面跳转时&#xff0c;想实现和Navigation一样的生命周期时&#xff0c;需要通过新建生命周期类来实现对页面对某一个生命周期的监控。 新建Lifecycle类 通过继承IHMLifecycle接口实现生命周期接口的方法重写。 通过…

C++学习,容器类 <list>

C 标准库 <list> 是一个非常重要的容器类&#xff0c;用于存储元素集合&#xff0c;支持双向迭代器。<list>允许在容器的任意位置快速插入和删除元素。与数组或向量&#xff08;<vector>&#xff09;不同&#xff0c;<list> 不需要在创建时指定大小&am…

安卓14无法安装应用解决历程

客户手机基本情况&#xff1a; 安卓14&#xff0c;对应的 targetSdkVersion 34 前天遇到了安卓14适配问题&#xff0c;客户发来的截图是这样的 描述&#xff1a;无法安装我们公司的B应用。 型号&#xff1a;三星google美版 解决步骤&#xff1a; 1、寻找其他安卓14手机测试…