任务与微任务

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

JavaScript 执行上下文

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

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

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

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 接口上。

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

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

使用微任务的时机

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

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

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

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

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 子句中,我们面临了操作顺序可能不同的局势;比方说,像下面看起来的这样:

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 子句里使用一个微任务来确保操作顺序的一致性,以达到平衡两个子句的目的:

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 事件,平衡了两个子句。

批量操作

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

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

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() 方法来调度一个微任务并使其运行。

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,而后入列了一个微任务。

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() 调度一个微任务。此例的重要之处是微任务不在其所处的函数退出时,而是在主程序退出时被执行。因为那才是任务退出而执行栈上为空的时刻。

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.mzph.cn/news/881750.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

幸运7游戏模拟 python

题目&#xff1a; 幸运"7"游戏,用计算机模拟掷骰子的过程&#xff0c;测算两个骰子点数之和为7的概率。 游戏规则是你丢两个骰子&#xff0c;如果其点数之和为7你就赢4元&#xff0c;不是7你就输1元。 假设你刚开始有10元&#xff0c;当全部输掉为0元的时候游戏结…

【网络安全】1,600$:Auth0 错误配置

未经许可,不得转载。 文章目录 前言正文漏洞案例修复建议前言 Auth0 是一个广泛用于网站和应用程序的身份验证平台,负责管理用户身份并确保其服务的安全访问。该平台提供了多种工作流程,以无缝集成登录和注册流程。 在 Auth0 中创建新应用时,注册选项默认启用。当系统禁用…

Android 无Bug版 多语言设计方案!

出海业务为什么要做多语言&#xff1f; 1.市场扩大与本地化需求&#xff1a; 通过支持多种语言&#xff0c;出海项目可以触及更广泛的国际用户群体&#xff0c;进而扩大其市场份额。 本地化是吸引国际用户的重要策略之一&#xff0c;而语言本地化是其中的核心。使用用户的母语…

c语言字符函数

1&#xff0c;字符分类函数&#xff1a; 例如&#xff1a;写一个代码将字符串中的小写字母转化成大写字母 就可以用到上述islower函数判断字符是否是小写 2.字符转换函数 c语言提供了两个字符转换函数 1.int tolower (int c); //将输入进去的大写字母转化成小写 2,int …

IRP默认最小流程

IRP是Windows内核中的一种非常重要的数据结构。上层应用程序与底层驱动程序通信时&#xff0c;应用程序会发出I/O请求&#xff0c;操作系统将相应的I/O请求转换成相应的IRP&#xff0c;不同的IRP会根据类型被分派到不同的派遣例程中进行处理。 irp相当于R3下的消息&#xff0c…

rtsp协议:rtsp协议参数介绍

目的&#xff1a; 实时流协议&#xff08;RTSP&#xff09;用于建立和控制单个或多个时间同步的连续媒体流&#xff0c;例如音频和视频。RTSP 通常不负责实际传输这些连续的媒体流&#xff0c;但可以将连续媒体流与控制流进行交错传输&#xff08;参见第 10.12 节&#xff09;。…

学习Redisson实现分布式锁

官网&#xff1a;https://redisson.org/ 官方文档&#xff1a;https://redisson.org/docs/getting-started/ 官方中文文档&#xff1a;https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95 1、引入依赖 <!--redisson--> <dependency><groupId>or…

金融大数据平台总体技术

目录 金融大数据平台应用场景风险管理 场景描述解决方案​​​​​​​市场营销 ​​​​​​​场景描述解决方案​​​​​​​金融大数据信息价值链​​​​​​​金融大数据平台总体目标金融大数据平台功能技术要求​​​​​​​ ​​​​​​​概述数据接入功能要求 ​​…

吐槽kotlin之垃圾设计

本文重点在于吐槽垃圾设计&#xff0c;基本直只说缺点。 一.没有static关键字 static其实不是很面向对象&#xff0c;但是是很有必要和方便的。 kotlin为了实现java的static功能&#xff0c;必须使用伴生类&#xff0c;一般情况下没啥问题&#xff0c;但是反编译之后的class多…

Github学生包的JetBrains认证过期/idea认证过期如何解决?

官网通过Github更新状态即可JetBrains Account 注意要到邮箱走流程

SQL Server LocalDB 表数据中文乱码问题

--查看数据库设置 SELECT name, collation_name FROM sys.databases;--出现了The database could not be exclusively locked to perform the operation这个错误&#xff0c; --无法修改字符集为Chinese_PRC_CI_AS&#xff1b;所以需要先设置为单用户模式 ALTER DATABASE MySma…

初试PostgreSQL数据库

文章目录 一、PostgreSQL数据库概述1.1 PostgreSQL的历史1.2 PostgreSQL安装1.3 安装PostgreSQL二、PostgreSQL起步2.1 连接数据库2.1.1 SQL Shell2.1.2 执行SQL语句2.2 pgAdmin 42.2.1 打开pgAdmin 42.2.2 查找数据库2.2.3 打开查询工具2.2.4 执行SQL语句三、实战小结文章目录…

大一计算机课程之线性代数

《大一计算机课程之线性代数》 在大一的计算机课程中&#xff0c;线性代数是一门极为重要的基础学科&#xff0c;它就像一把神奇的钥匙&#xff0c;为计算机科学领域的诸多方面开启了智慧之门。 线性代数主要研究线性方程组、向量空间、线性变换等内容。对于计算机专业的学生…

HCIP-HarmonyOS Application Developer 习题(九)

(多选) 1、HarmonyOS多窗口交互能力提供了以下哪几种交互方式&#xff1f; A. 全局消息通知 B.平行视界 C.悬浮窗 D.分屏 答案&#xff1a;BCD 分析&#xff1a;系统提供了悬浮窗、分屏、平行视界三种多窗口交互&#xff0c;为用户在大屏幕设备上的多任务并行、便捷的临时任务…

程序员必读:精通ER图设计,解锁数据库高效构建秘籍

在信息技术的浩瀚星空中&#xff0c;数据库如同星辰般璀璨&#xff0c;而ER图&#xff08;Entity-Relationship Diagram&#xff0c;实体-关系图&#xff09;则是那把引领我们穿越数据迷雾的钥匙。对于每一位程序员而言&#xff0c;掌握ER图设计不仅是数据库设计的基础&#xf…

Flutter 3.24 发布:GPU模块及多视图嵌入功能

Flutter 3.24 发布&#xff1a;GPU模块及多视图嵌入功能 Flutter 3.24 带来了许多新功能和改进&#xff0c;让开发应用程序变得更加容易和有趣。这个版本重点展示了 Flutter GPU 的预览功能&#xff0c;让应用程序可以直接使用高级图形和 3D 场景功能。 此外&#xff0c;网页…

Open WebUI | 自托管的类 ChatGPT 网站

Open WebUI 是一个扩展性强、功能丰富且用户友好的自托管 WebUI&#xff0c;支持 ChatGPT 网页端的大部分功能&#xff0c;支持各类模型服务&#xff0c;包括 Ollama 和 OpenAI 的 API。该项目在 GitHub 上已有 38k 星&#xff0c;非常受欢迎。 功能介绍 本篇介绍该项目的功能…

分布式 ID

背景 在复杂分布式系统中&#xff0c;往往需要对大量的数据和消息进行唯一标识。随着数据日渐增长&#xff0c;对数据分库分表后也需要有一个唯一ID来标识一条数据或消息&#xff0c;数据库的自增 ID 显然不能满足需求&#xff1b;此时一个能够生成全局唯一 ID 的系统是非常必…

Android中的Activity(案例+代码+效果图)

目录 1.Activity的生命周期 核心生命周期回调 1&#xff09;onCreate() 2&#xff09;onStart() 3&#xff09;onResume() 4&#xff09;onPause() 5&#xff09;onStop() 6&#xff09;onRestart() 7&#xff09;onDestroy() 8&#xff09;生命周期图示 10&#xff09;注意事项…

Android实现App内直接预览本地PDF文件

在App内实现直接预览pdf文件&#xff0c;而不是通过调用第三方软件&#xff0c;如WPS office等打开pdf。 主要思路&#xff1a;通过PhotoView将pdf读取为图片流进行展示。 一、首先&#xff0c;获取对本地文件读取的权限 在AndrooidManifest.xml中声明权限&#xff0c;以及页…