Heim  >  Artikel  >  Web-Frontend  >  Eine eingehende Analyse der asynchronen Ereignisabfrage in JavaScript

Eine eingehende Analyse der asynchronen Ereignisabfrage in JavaScript

不言
不言nach vorne
2018-11-24 14:18:412562Durchsuche

Dieser Artikel bietet Ihnen eine ausführliche Analyse der asynchronen Ereignisabfrage in JavaScript. Ich hoffe, dass er Ihnen als Referenz dienen wird.

JavsScript ist eine Single-Threaded-Programmiersprache, was bedeutet, dass es jeweils nur eine Sache verarbeiten kann. Das heißt, die JavaScript-Engine kann nur eine Sache in einem Thread verarbeiten auf einmal.

Während Single-Threading den Programmiercode vereinfacht, da Sie sich nicht zu viele Gedanken über durch Parallelität verursachte Probleme machen müssen, bedeutet dies auch, dass Sie langfristige Vorgänge wie Netzwerkanforderungen ausführen müssen. während der Hauptthread blockiert wird.

Stellen Sie sich vor, Sie fordern einige Daten von einer API an. Abhängig von den Umständen benötigt der Server einige Zeit, um die Anfrage zu verarbeiten, während er den Hauptthread blockiert, sodass die Webseite lange Zeit nicht reagiert.

Aus diesem Grund wurde asynchrones JavaScript eingeführt. Mit asynchronem JavaScript (z. B. Callback-Funktionen, Versprechen, Async/Await) können Sie Netzwerkanfragen über einen langen Zeitraum ausführen, ohne den Hauptthread zu blockieren:)

Vielleicht wissen Sie, wie asynchrones JavaScript funktioniert, aber das ist nicht der Fall Aber wenn man weiß, wie es funktioniert, ist ein tieferes Verständnis von JavaScript Async hilfreich.

Also ohne weitere Umschweife, fangen wir an :)

Wie funktioniert synchrones JavaScript?

Bevor wir uns mit asynchronem JavaScript befassen, wollen wir zunächst verstehen, wie synchroner JavaScript-Code in der JavaScript-Engine ausgeführt wird. Zum Beispiel:

    const second = () => {
      console.log('Hello there!');
    }
    
    const first = () => {
      console.log('Hi there!');
      second();
      console.log('The End');
    }
    
    first();

Um zu verstehen, wie der obige Code in der JavaScript-Engine ausgeführt wird, müssen wir die Konzepte des Ausführungskontexts und des Aufrufstapels (auch als Ausführungsstapel bezeichnet) verstehen.

Funktionscode wird im Funktionsausführungskontext ausgeführt, und globaler Code wird im globalen Ausführungskontext ausgeführt. Jede Funktion hat ihren eigenen Ausführungskontext.

Aufrufstapel

Der Aufrufstapel ist, wie der Name schon sagt, ein Stapel mit einer LIFO-Struktur (Last In First Out), die zum Speichern aller während der Codeausführung erstellten Ausführungskontexte verwendet wird.

JavaScript hat nur einen Aufrufstapel, da es eine Single-Threaded-Programmiersprache ist. Der Aufrufstapel hat eine LIFO-Struktur, was bedeutet, dass Elemente nur oben im Stapel hinzugefügt oder entfernt werden können.

Kehren wir zum obigen Codeausschnitt zurück und versuchen zu verstehen, wie der Code in der JavaScript-Engine ausgeführt wird.

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

Eine eingehende Analyse der asynchronen Ereignisabfrage in JavaScript

Was ist hier los?

Wenn dieser Code ausgeführt wird, wird ein globaler Ausführungskontext erstellt (von main( ) darstellt) und schiebt es an die Spitze des Aufrufstapels. Wenn ein Aufruf von first() auftritt, wird dieser an die Spitze des Stapels verschoben.

Als nächstes wird console.log('Hallo!') an die Spitze des Stapels verschoben und nach Abschluss des Vorgangs vom Stapel entfernt. Danach rufen wir second() auf, sodass die Funktion second() an die Spitze des Stapels verschoben wird.

console.log('Hello there!') wird an die Spitze des Stapels verschoben und nach Abschluss vom Stapel entfernt. Die Funktion „second()“ endet und wird vom Stapel entfernt.

console.log("the End") wird an die Spitze des Stapels verschoben und nach Abschluss gelöscht. Danach wird die Funktion first() abgeschlossen und vom Stapel entfernt.

Das Programm hat seine Ausführung zu diesem Zeitpunkt abgeschlossen, sodass der globale Ausführungskontext (main()) vom Stapel entfernt wird.

Wie funktioniert asynchrones JavaScript?

Da wir nun ein grundlegendes Verständnis des Aufrufstapels und der Funktionsweise von synchronem JavaScript haben, kehren wir zum asynchronen JavaScript zurück.

Was ist Blockieren?

Nehmen wir an, dass wir Bildverarbeitungs- oder Netzwerkanfragen synchron durchführen. Zum Beispiel:

const processImage = (image) => {
  /**
  * doing some operations on image
  **/
  console.log('Image processed');
}
const networkRequest = (url) => {
  /**
  * requesting network resource
  **/
  return someData;
}
const greeting = () => {
  console.log('Hello World');
}
processImage(logo.jpg);
networkRequest('www.somerandomurl.com');
greeting();

Die Bildverarbeitung und Netzwerkanfragen nehmen Zeit in Anspruch. Wenn die Funktion „processImage()“ aufgerufen wird, dauert es je nach Größe des Bildes einige Zeit.

Nachdem die Funktion „processImage()“ abgeschlossen ist, wird sie vom Stapel entfernt. Anschließend wird die Funktion networkRequest() aufgerufen und auf den Stack geschoben. Ebenso dauert es einige Zeit, bis die Ausführung abgeschlossen ist.

Wenn schließlich die Funktion networkRequest() abgeschlossen ist, wird die Funktion Greeting() aufgerufen, da sie nur eine Konsole enthält. Protokollanweisungen und Konsole. Protokollanweisungen sind normalerweise schnell, daher wird die Funktion Greeting() sofort ausgeführt und kehrt zurück.

Daher müssen wir warten, bis die Funktion (wie ProcessImage() oder NetworkRequest()) abgeschlossen ist. Dies bedeutet, dass diese Funktionen den Aufrufstapel oder den Hauptthread blockieren. Während der obige Code ausgeführt wird, können wir daher keine anderen Vorgänge ausführen, was nicht ideal ist.

Was ist also die Lösung?

Die einfachste Lösung ist ein asynchroner Rückruf. Wir verwenden asynchrone Rückrufe, um den Code nicht blockierend zu machen. Zum Beispiel:

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};
console.log('Hello World');
networkRequest();

Hier habe ich die setTimeout-Methode verwendet, um eine Netzwerkanfrage zu simulieren. Denken Sie daran, dass setTimeout nicht Teil der JavaScript-Engine ist, sondern Teil der Web-API (im Browser) und der C/c++-API (in node.js).

Um zu verstehen, wie dieser Code ausgeführt wird, müssen wir weitere Konzepte verstehen, wie z. B. Ereignisabfragen und Rückrufwarteschlangen (oder Nachrichtenwarteschlangen).

Eine eingehende Analyse der asynchronen Ereignisabfrage in JavaScript

事件轮询、web api和消息队列不是JavaScript引擎的一部分,而是浏览器的JavaScript运行时环境或Nodejs JavaScript运行时环境的一部分(对于Nodejs)。在Nodejs中,web api被c/c++ api所替代。

现在让我们回到上面的代码,看看它是如何异步执行的。

const networkRequest = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
};

console.log('Hello World');

networkRequest();

console.log('The End');

Eine eingehende Analyse der asynchronen Ereignisabfrage in JavaScript

当上述代码在浏览器中加载时,console.log(' Hello World ') 被推送到堆栈中,并在完成后弹出堆栈。接下来,将遇到对 networkRequest() 的调用,因此将它推到堆栈的顶部。

下一个 setTimeout() 函数被调用,因此它被推到堆栈的顶部。setTimeout()有两个参数:

  • 1) 回调和

  • 2) 以毫秒(ms)为单位的时间。

setTimeout() 方法在web api环境中启动一个2s的计时器。此时,setTimeout()已经完成,并从堆栈中弹出。cosole.log(“the end”) 被推送到堆栈中,在完成后执行并从堆栈中删除。

同时,计时器已经过期,现在回调被推送到消息队列。但是回调不会立即执行,这就是事件轮询开始的地方。

事件轮询

事件轮询的工作是监听调用堆栈,并确定调用堆栈是否为空。如果调用堆栈是空的,它将检查消息队列,看看是否有任何挂起的回调等待执行。

在这种情况下,消息队列包含一个回调,此时调用堆栈为空。因此,事件轮询将回调推到堆栈的顶部。

然后是 console.log(“Async Code”) 被推送到堆栈顶部,执行并从堆栈中弹出。此时,回调已经完成,因此从堆栈中删除它,程序最终完成。

消息队列还包含来自DOM事件(如单击事件和键盘事件)的回调。例如:

document.querySelector('.btn').addEventListener('click',(event) => {
  console.log('Button Clicked');
});

对于DOM事件,事件侦听器位于web api环境中,等待某个事件(在本例中单击event)发生,当该事件发生时,回调函数被放置在等待执行的消息队列中。

同样,事件轮询检查调用堆栈是否为空,并在调用堆栈为空并执行回调时将事件回调推送到堆栈。

延迟函数执行

我们还可以使用setTimeout来延迟函数的执行,直到堆栈清空为止。例如

const bar = () => {
  console.log('bar');
}
const baz = () => {
  console.log('baz');
}
const foo = () => {
  console.log('foo');
  setTimeout(bar, 0);
  baz();
}
foo();

打印结果:

foo
baz
bar

当这段代码运行时,第一个函数foo()被调用,在foo内部我们调用console.log('foo'),然后setTimeout()被调用,bar()作为回调函数和时0秒计时器。

现在,如果我们没有使用 setTimeout, bar() 函数将立即执行,但是使用 setTimeout 和0秒计时器,将bar的执行延迟到堆栈为空的时候。

0秒后,bar()回调被放入等待执行的消息队列中。但是它只会在堆栈完全空的时候执行,也就是在baz和foo函数完成之后。

ES6 任务队列

我们已经了解了异步回调和DOM事件是如何执行的,它们使用消息队列存储等待执行所有回调。

ES6引入了任务队列的概念,任务队列是 JavaScript 中的 promise 所使用的。消息队列和任务队列的区别在于,任务队列的优先级高于消息队列,这意味着任务队列中的promise 作业将在消息队列中的回调之前执行,例如:

const bar = () => {
  console.log('bar');
};

const baz = () => {
  console.log('baz');
};

const foo = () => {
  console.log('foo');
  setTimeout(bar, 0);
  new Promise((resolve, reject) => {
    resolve('Promise resolved');
  }).then(res => console.log(res))
    .catch(err => console.log(err));
  baz();
};

foo();

打印结果:

foo
baz
Promised resolved
bar

我们可以看到 promise 在 setTimeout 之前执行,因为 promise 响应存储在任务队列中,任务队列的优先级高于消息队列。

小结

因此,我们了解了异步 JavaScript 是如何工作的,以及调用堆栈、事件循环、消息队列和任务队列等概念,这些概念共同构成了 JavaScript 运行时环境。虽然成为一名出色的JavaScript开发人员并不需要学习所有这些概念,但是了解这些概念是有帮助的:)

Das obige ist der detaillierte Inhalt vonEine eingehende Analyse der asynchronen Ereignisabfrage in JavaScript. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen