JavaScript-Grundprinzipien, Parallelitätsmodell, EventLoop, Zusammenfassung der asynchronen Programmierung

JavaScript ist ein auf Ereignisschleifen basierendes Parallelitätsmodell. Im Gegensatz zu anderen Multithread-Sprachen verwaltete frühes JavaScript nur eine Single-Threaded-EventLoop. Mit der Weiterentwicklung der Hardware haben sich Computer zu leistungsstarken Multi-Core-Systemen entwickelt und JavaScript ist zu einer der am häufigsten verwendeten Sprachen in der Computerwelt geworden. Viele der beliebtesten Anwendungen basieren zumindest teilweise auf JavaScript-Code. Um dies zu unterstützen, mussten Wege gefunden werden, Projekte von den Einschränkungen von Single-Thread-Sprachen zu befreien. Auch die entsprechende Multithread-Programmierung von JavaScript wurde auf die Tagesordnung gesetzt.

Um die Vorteile von JavaScripts EventLoop aus dem frühen setTimeout() setInterval()asynchronen Aufgabenmodell zu verbessern, wird es auch auf Promise(), queueMicrotask(), requestAnimationFrame(), erweitert requestIdleCallback(). Gleichzeitig werden auch Mikrotask-Warteschlangen und Sub-Thread-Worker-Implementierungen eingeführt.

1. Parallelitätsmodell und Ereignisschleife

Ausgehend vom grundlegendsten Java-Ereignismodell implementieren und optimieren moderne JavaScript-Engines die unten beschriebene Semantik.

Bild.png

der Stapel

Funktionsaufrufe bilden einen Stapel von Frames.

function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42

Beim Aufruf  bar wird der erste Frame erstellt und auf den Stapel gelegt, der  bar die Parameter und lokalen Variablen enthält. Beim  bar Aufruf  foo wird ein zweiter Frame erstellt und über dem ersten Frame auf den Stapel gelegt, der die  foo Parameter und lokalen Variablen enthält. Wenn  foo die Ausführung abgeschlossen ist und zurückkehrt, wird der zweite Frame vom Stapel entfernt (wobei  bar der Aufrufframe der Funktion verbleibt). Wenn  bar die Ausführung beendet ist und zurückkehrt, wird auch der erste Frame gelöscht und der Stapel geleert.

Haufen

Objekte werden auf dem Heap zugewiesen, ein Computerbegriff, der einen großen (normalerweise unstrukturierten) Speicherbereich bezeichnet.

Warteschlange

Eine JavaScript-Laufzeitumgebung enthält eine Nachrichtenwarteschlange mit ausstehenden Nachrichten. Jeder Nachricht ist eine Rückruffunktion zugeordnet, die die Nachricht verarbeitet.

在 事件循环 期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。

函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。

事件循环

之所以称之为 事件循环,是因为它经常按照类似如下的方式来被实现:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

queue.waitForMessage() 会同步地等待消息到达 (如果当前没有任何消息等待被处理)。

执行至完成

每一个消息完整地执行后,其他消息才会被执行。这为程序的分析提供了一些优秀的特性,包括:当一个函数执行时,它不会被抢占,只有在它运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据。这与 C 语言不同,例如,如果函数在线程中运行,它可能在任何位置被终止,然后在另一个线程中运行其他代码。

这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web 应用程序就无法处理与用户的交互,例如点击或滚动。为了缓解这个问题,浏览器一般会弹出一个“这个脚本运行时间过长”的对话框。一个良好的习惯是缩短单个消息处理时间,并在可能的情况下将一个消息裁剪成多个消息。

添加消息

在浏览器里,每当一个事件发生并且有一个事件监听器绑定在该事件上时,一个消息就会被添加进消息队列。如果没有事件监听器,这个事件将会丢失。所以当一个带有点击事件处理器的元素被点击时,就会像其他事件一样产生一个类似的消息。

函数 setTimeout 接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。

下面的例子演示了这个概念(setTimeout 并不会在计时器到期之后直接执行):

const s = new Date().getSeconds();

setTimeout(function() {
  // 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
  console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
  if(new Date().getSeconds() - s >= 2) {
    console.log("Good, looped for 2 seconds");
    break;
  }
}

零延迟

零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。

其等待的时间取决于队列里待处理的消息数量。在下面的例子中,"这是一条消息" 将会在回调获得处理之前输出到控制台,这是因为延迟参数是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。

基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

(function() {

  console.log('这是开始');

  setTimeout(function cb() {
    console.log('这是来自第一个回调的消息');
  });

  console.log('这是一条消息');

  setTimeout(function cb1() {
    console.log('这是来自第二个回调的消息');
  }, 0);

  console.log('这是结束');

})();

// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"

永不阻塞

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其他事情,比如用户输入。

由于历史原因有一些例外,如 alert 或者同步 XHR,但应该尽量避免使用它们。注意,例外的例外也是存在的(但通常是实现错误而非其他原因)。

二、运行时环境

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

现在我们来更加详细的了解一下运行时是如何工作的。

事件循环

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

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

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

有如下三种事件循环:

Window 事件循环

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

Worker 事件循环

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

Worklet 事件循环

worklet (en-US) 事件循环驱动运行 worklet 的代理。这包含了 Worklet (en-US)、AudioWorklet (en-US) 以及 PaintWorklet (en-US)。

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

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

如果一个窗口打开了另一个窗口,它们可能会共享一个事件循环。 如果窗口是包含在 中的容器,则它可能会和包含它的窗口共享一个事件循环。 在多进程浏览器中多个窗口碰巧共享了同一个进程。 这种特定情况依赖于浏览器的具体实现,各个浏览器可能并不一样。

三、异步编程

JavaScript本质上只是一个单线程的语言,如果一个任务占用时间过长,就会造成浏览器的假死,并阻塞后面的任务,所以异步编程是JavaScript处理任务核心特性。

1.回调

事件处理程序是一种特殊类型的回调函数。而回调函数则是一个被传递到另一个函数中的会在适当的时候被调用的函数。正如我们刚刚所看到的:回调函数曾经是 JavaScript 中实现异步函数的主要方式。

然而,当回调函数本身需要调用其他同样接受回调函数的函数时,基于回调的代码会变得难以理解。当你需要执行一些分解成一系列异步函数的操作时,这将变得十分常见。例如下面这种情况:

    function doStep1(init) {
      return init + 1;
    }
    function doStep2(init) {
      return init + 2;
    }
    function doStep3(init) {
      return init + 3;
    }
    function doOperation() {
      let result = 0;
      result = doStep1(result);
      result = doStep2(result);
      result = doStep3(result);
      console.log(`结果:${result}`);
    }
    doOperation();

回调有明显的缺陷,很容易写成“回调炼狱”形式的函数,给阅读和理解造成了麻烦。

2.事件

JavaScript是基于事件编程实现异步操作的,回调和事件组成了JavaScript的基石。JavaScript提供了addEventListener语法处理事件。

   
<button id="btn"> 点我哦 </button>
<script>
  const btn = document.getElementById('btn');

  // 单击时触发
  btn.addEventListener('click', event => console.log('click!'));

  // 鼠标移入触发
  btn.addEventListener('mouseover', event => console.log('mouseover!'));

  // 鼠标移出触发
  btn.addEventListener('mouseout', event => console.log('mouseout!'));
</script>

2.基于契约

Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在 Promise 返回给调用者的时候,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。

    const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

    fetchPromise
      .then( response => {
        return response.json();
      })
      .then( json => {
        console.log(json[0].name);
      });

4.使用queueMicrotask()

Window 或 Worker 接口的 queueMicrotask() 方法,将微任务加入队列以在控制返回浏览器的事件循环之前的安全时间执行。

微任务是一个简短的函数,它将在当前任务完成其工作后运行,并且在执行上下文的控制权返回到浏览器的事件循环之前没有其他代码等待运行时运行。

  MyElement.prototype.loadData = function (url) {
  if (this._cache[url]) {
    queueMicrotask(() => {
      this._setData(this._cache[url]);
      this.dispatchEvent(new Event("load"));
    });
  } else {
    fetch(url)
      .then((res) => res.arrayBuffer())
      .then((data) => {
        this._cache[url] = data;
        this._setData(data);
        this.dispatchEvent(new Event("load"));
      });
  }
};

5.使用setTime()、 setInterval()、 requestAnimationFrame()

window对象下这三个函数都能实现异常操作

6.requestIdleCallback()

window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

你可以在空闲回调函数中调用 requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。

6.并发编程worker线程

一个 worker 是使用一个构造函数创建的一个对象(例如 Worker())运行一个命名的 JavaScript 文件——这个文件包含将在 worker 线程中运行的代码; worker 运行在另一个全局上下文中,不同于当前的window。因此,在 Worker 内通过 window 获取全局作用域(而不是self)将返回错误。

在专用 worker 的情况下,DedicatedWorkerGlobalScope 对象代表了 worker 的上下文(专用 worker 是指标准 worker 仅在单一脚本中被使用;共享 worker 的上下文是 SharedWorkerGlobalScope (en-US) 对象)。一个专用 worker 仅能被首次生成它的脚本使用,而共享 worker 可以同时被多个脚本使用。

四、任务队列与微任务队列

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

任务队列

  1. setTimeOut()setInterval()函数添加
  2. 各种事件回调,浏览器addEventListener,node on
  3. XHR

微任务队列

  1. promse()
  2. queueMicrotask()
  3. Mutation observer()

image.png

image.png

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

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行
  • Jedes Mal, wenn eine Aufgabe beendet wird und der Ausführungskontextstapel leer ist, wird jede Mikrotask in der Mikrotask-Warteschlange der Reihe nach ausgeführt. Der Unterschied besteht darin, dass es wartet, bis die Mikrotask-Warteschlange leer ist, bevor die Ausführung gestoppt wird – selbst wenn in der Mitte Mikrotasks hinzugefügt werden. Mit anderen Worten: Mikrotasks können der Warteschlange neue Mikrotasks hinzufügen, und diese neuen Mikrotasks werden ausgeführt, bevor die nächste Aufgabe ausgeführt wird, bevor die aktuelle Iteration der Ereignisschleife endet.

Durch die Verwendung  asynchroner JavaScript- Techniken  wie Versprechen  , die es dem Hauptthread ermöglichen, die Ausführung fortzusetzen, während er auf die Rückgabe der Anforderung wartet, kann die oben genannte Situation weiter entschärft werden. Bei einigen Codes, die näher an der Grundfunktion liegen, z. B. einigen Framework-Codes, muss der Code jedoch möglicherweise so geplant werden, dass er zu einem sicheren Zeitpunkt im Hauptthread ausgeführt wird, was nichts mit den Ergebnissen von Anforderungen oder Aufgaben zu tun hat.

Mikrotasks sind eine weitere Lösung für dieses Problem. Sie bieten eine bessere Zugriffsebene, indem sie den Code so planen, dass er ausgeführt wird, bevor die nächste Ereignisschleife beginnt, anstatt warten zu müssen, bis die nächste beginnt.

Die Mikrotask-Warteschlange gibt es schon seit einiger Zeit, aber zuvor wurde sie nur intern verwendet, um Aufgaben wie Versprechen voranzutreiben. queueMicrotask() Durch die Hinzufügung von können Entwickler eine einheitliche Mikrotask-Warteschlange erstellen, die überall dort verwendet werden kann, wo die sichere Ausführung von Code ohne einen Ausführungskontext im JavaScript-Ausführungskontextstapel geplant werden muss. Über mehrere Instanzen, Browser und JavaScript-Laufzeiten hinweg bedeutet der standardisierte Mikrowarteschlangenmechanismus, dass diese Mikrotasks zuverlässig in der gleichen Reihenfolge ausgeführt werden, wodurch potenziell schwer zu findende Fehler vermieden werden.

5. Anwendungsfälle

Ausgabereihenfolge von Aufgaben und Mikroaufgaben

  1. Geben Sie für allgemeine schriftliche Testfragen die folgende Ausführungsreihenfolge aus:
console.log('start')
 
setTimeout(() => {
  console.log('setTimeout')
}, 0)
 
new Promise((resolve) => {
  console.log('promise')
  resolve()
})
  .then(() => {
    console.log('then1')
  })
  .then(() => {
    console.log('then2')
  })
 
console.log('end')

Beispiel: start Promise end then1 then2 setTimeout

Insbesondere wird der interne Code der neuen Promise()-Funktion synchron ausgeführt und erst dann zur Mikrotask-Opposition hinzugefügt

Supongo que te gusta

Origin juejin.im/post/7253437782333472826
Recomendado
Clasificación