Heim > Artikel > Web-Frontend > Detaillierte Erläuterung der Beispiele für js-Ausführungsmechanismen
Um den Ausführungsmechanismus von JavaScript zu verstehen, müssen Sie mehrere Punkte tiefgreifend verstehen: den Single-Thread-Mechanismus von JavaScript, die Aufgabenwarteschlange (synchrone Aufgaben und asynchrone Aufgaben), Ereignisse und Rückruffunktionen, Timer und die Ereignisschleife.
Eine der Sprachfunktionen von JavaScript (auch der Kern der Sprache) ist der Single-Threaded. Einfach ausgedrückt bedeutet ein einzelner Thread, dass jeweils nur eine Aufgabe ausführen kann . Wenn mehrere Aufgaben vorhanden sind, können diese nur in einer Reihenfolge abgeschlossen werden, bevor die nächste ausgeführt wird.
Das Single-Threading von JavaScript hängt mit seinem Sprachzweck zusammen. Als Browser-Skriptsprache besteht der Hauptzweck von JavaScript darin, die Benutzerinteraktion abzuschließen und das DOM zu betreiben. Dies legt fest, dass es nur Single-Threaded sein kann, da es sonst zu komplexen Synchronisationsproblemen kommt.
Stellen Sie sich vor, dass JavaScript zwei Threads gleichzeitig hat, um einem bestimmten DOM-Knoten Inhalte hinzuzufügen, und der andere Thread besteht darin, den Knoten zu löschen verwenden? ?
Um Komplexität zu vermeiden, ist JavaScript seit seiner Geburt Single-Threaded.
Um die CPU-Auslastung zu verbessern, schlägt HTML5 den Web Worker-Standard vor, der es JavaScript-Skripten ermöglicht, mehrere Threads zu erstellen, die untergeordneten Threads werden jedoch vollständig vom Hauptthread gesteuert und dürfen diese nicht bedienen DOM. Dieser Standard ändert also nichts an der Single-Threaded-Natur von JavaScript.
Das Erledigen von Aufgaben nacheinander bedeutet, dass die zu erledigenden Aufgaben in die Warteschlange gestellt werden müssen. Warum müssen sie also in die Warteschlange gestellt werden?
Normalerweise gibt es zwei Gründe für die Warteschlange:
Die Aufgabenberechnungslast ist zu groß und die CPU ist ausgelastet
Aufgabe Die erforderlichen Dinge sind nicht bereit, sodass die Ausführung nicht fortgesetzt werden kann, was dazu führt, dass die CPU im Leerlauf bleibt und auf Eingabe- und Ausgabegeräte (E/A-Geräte) wartet.
Bei einigen Aufgaben muss Ajax beispielsweise Daten abrufen, bevor sie ausgeführt werden können
Daraus erkannten die Entwickler von JavaScript auch, dass es zu diesem Zeitpunkt durchaus möglich ist, die Aufgaben auszuführen, die später bereit sind, um die Betriebseffizienz zu verbessern, d. h. die wartenden Aufgaben anzuhalten und beiseite zu legen und sie anschließend auszuführen bekommen, was benötigt wird. Es ist so, als ob der andere Teilnehmer für einen Moment weggeht, während Sie den Anruf entgegennehmen, ein weiterer Anruf eingeht, Sie also den aktuellen Anruf auflegen, warten, bis dieser Anruf beendet ist, und dann wieder mit dem vorherigen Anruf verbinden.
Daher tauchten die Konzepte Synchronisation und Asynchronität auf und Aufgaben wurden in zwei Typen unterteilt: eine ist eine synchrone Aufgabe (synchron) und die andere ist eine asynchrone Aufgabe (asynchron).
Synchronisierte Aufgaben: Aufgaben, die ausgeführt werden müssen, werden nacheinander im Hauptthread in die Warteschlange gestellt. Nachdem die vorherige abgeschlossen ist, wird die nächste ausgeführt
Asynchrone Aufgabe: Aufgaben, die nicht sofort ausgeführt werden, aber ausgeführt werden müssen, werden in der „Aufgabenwarteschlange“ gespeichert, wann welche asynchrone Aufgabe ausgeführt werden kann Die Aufgabe wird in den Hauptthread aufgenommen und ausgeführt.
Alle synchronen Ausführungen können als asynchrone Ausführungen ohne asynchrone Aufgaben angesehen werden
Konkret sieht die asynchrone Ausführung wie folgt aus:
(1) Alle synchronen Aufgaben werden im Hauptthread ausgeführt und bilden einen Ausführungsstapel (Ausführungskontextstapel).
Das heißt, alle Aufgaben, die sofort ausgeführt werden können, werden im Hauptthread in die Warteschlange gestellt und nacheinander ausgeführt.
(2) Zusätzlich zum Hauptthread gibt es auch eine „Aufgabenwarteschlange“. Solange die asynchrone Aufgabe laufende Ergebnisse hat, wird ein Ereignis in die „Aufgabenwarteschlange“ gestellt.
Das heißt, jede asynchrone Aufgabe setzt ein eindeutiges Flag, wenn sie bereit ist. Dieses Flag wird verwendet, um die entsprechende asynchrone Aufgabe zu identifizieren.
(3) Sobald alle Synchronisierungsaufgaben im „Ausführungsstapel“ abgeschlossen sind, liest das System die „Aufgabenwarteschlange“, um zu sehen, welche Ereignisse darin enthalten sind. Diese entsprechenden asynchronen Aufgaben beenden den Wartezustand und gelangen in den Ausführungsstapel, um mit der Ausführung zu beginnen.
Das heißt, nachdem der Hauptthread die vorherige Aufgabe abgeschlossen hat, prüft er das Flag in der „Aufgabenwarteschlange“, um die entsprechende asynchrone Aufgabe zur Ausführung zu packen.
(4) Der Hauptthread wiederholt weiterhin die oben genannten drei Schritte.
Solange der Hauptthread leer ist, wird die „Aufgabenwarteschlange“ gelesen. Dieser Vorgang wird immer wieder wiederholt. So funktioniert JavaScript.
Woher wissen Sie dann, dass der Haupt-Thread-Ausführungsstapel leer ist? In der js-Engine gibt es einen Überwachungsprozess, der kontinuierlich prüft, ob der Haupt-Thread-Ausführungsstapel leer ist. Sobald er leer ist, wird er in die Ereigniswarteschlange verschoben, um zu prüfen, ob eine Funktion auf den Aufruf wartet.
Das Folgende ist ein
Leitfadendiagramm zur Veranschaulichung des Hauptthreads und der Aufgabenwarteschlange.
Wenn der Inhalt der Karte in Worten ausgedrückt wird:
Synchronische und asynchrone Aufgaben gelangen jeweils an unterschiedliche Ausführungs-„Orte“, synchron in den Hauptthread und asynchron in die Ereignistabelle und registrieren Funktionen.
Wenn die angegebene Sache abgeschlossen ist, verschiebt die Ereignistabelle diese Funktion in die Ereigniswarteschlange.
Die Aufgabe im Hauptthread ist nach der Ausführung leer. Sie wird in die Ereigniswarteschlange verschoben, um die entsprechende Funktion zu lesen und zur Ausführung in den Hauptthread einzutreten.
Der obige Vorgang wird kontinuierlich wiederholt, was oft als Ereignisschleife bezeichnet wird.
„Aufgabenwarteschlange“ ist eine Ereigniswarteschlange (kann auch als Nachrichtenwarteschlange verstanden werden), IO Wann Wenn das Gerät eine Aufgabe abschließt, wird ein Ereignis zur „Aufgabenwarteschlange“ hinzugefügt, das angibt, dass die zugehörigen asynchronen Aufgaben in den „Ausführungsstapel“ gelangen können. Dann liest der Hauptthread die „Aufgabenwarteschlange“, um zu sehen, welche Ereignisse darin enthalten sind.
Ereignisse in der „Aufgabenwarteschlange“ umfassen neben IO-Geräteereignissen auch einige benutzergenerierte Ereignisse (z. B. Mausklicks, Seitenscrollen usw.). Solange die Rückruffunktion angegeben ist, werden diese Ereignisse bei ihrem Auftreten in die „Aufgabenwarteschlange“ eingegeben und warten auf das Lesen durch den Hauptthread.
Die sogenannte „Rückruffunktion“ (Callback) ist der Code, der vom Hauptthread aufgehängt wird. Asynchrone Aufgaben müssen eine Rückruffunktion angeben. Wenn der Hauptthread mit der Ausführung einer asynchronen Aufgabe beginnt, wird die entsprechende Rückruffunktion ausgeführt.
„Aufgabenwarteschlange“ ist eine First-In-First-Out-Datenstruktur. Die zuerst eingestuften Ereignisse werden zuerst vom Hauptthread gelesen. Der Lesevorgang des Hauptthreads erfolgt grundsätzlich automatisch. Sobald der Ausführungsstapel gelöscht wird, gelangt das erste Ereignis in der „Aufgabenwarteschlange“ automatisch in den Hauptthread. Wenn jedoch ein „Timer“ enthalten ist, muss der Hauptthread zunächst die Ausführungszeit überprüfen. Bestimmte Ereignisse können erst nach der angegebenen Zeit zum Hauptthread zurückkehren.
Der Hauptthread liest Ereignisse aus der „Aufgabenwarteschlange“. Dieser Prozess ist zyklisch, daher wird der gesamte Betriebsmechanismus auch „Ereignisschleife“ genannt.
Um den Event Loop besser zu verstehen, hier ein Bild aus der Rede von Philip Roberts.
Im Bild oben generiert der Hauptthread einen Heap (Heap) und einen Stack (Stack). Der Code im Stack ruft verschiedene externe APIs auf , und in „ Verschiedene Ereignisse (Klicken, Laden, Fertig) werden zur „Aufgabenwarteschlange“ hinzugefügt. Wenn der Code im Stapel ausgeführt wird, liest der Hauptthread die „Aufgabenwarteschlange“ und führt die diesen Ereignissen entsprechenden Rückruffunktionen nacheinander aus.
Der Code im Ausführungsstapel (synchrone Aufgabe) wird immer ausgeführt, bevor die „Aufgabenwarteschlange“ (asynchrone Aufgabe) gelesen wird.
let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('发送成功!'); } })console.log('代码执行结束');
Das Obige ist ein einfacher ajax
Anforderungscode:
Ajax betritt die Ereignistabelle und registriert die Rückruffunktion success
.
Ausführen console.log('代码执行结束')
.
Das Ajax-Ereignis ist abgeschlossen und die Rückruffunktion success
tritt in die Ereigniswarteschlange ein.
Der Hauptthread liest die Callback-Funktion success
aus der Event Queue und führt sie aus.
Zusätzlich zum Platzieren von Ereignissen für asynchrone Aufgaben kann die „Aufgabenwarteschlange“ auch zeitgesteuerte Ereignisse platzieren, d Code wird ausgeführt. Dies wird als Timer-Funktion bezeichnet, bei der es sich um Code handelt, der regelmäßig ausgeführt wird.
SetTimeout()
und setInterval()
können zum Registrieren von Funktionen verwendet werden, die nach einer bestimmten Zeit einmal oder wiederholt aufgerufen werden. Ihre internen Betriebsmechanismen sind genau die gleichen. Der Unterschied besteht darin, dass der von erstere angegebene Code ist wird einmal ausgeführt, während der von letzterem angegebene Code einmal in Abständen von angegebenen Millisekunden aufgerufen wird:
setInterval(updateClock, 60000); //60秒调用一次updateClock()
Da es sich um wichtige globale Funktionen in clientseitigem JavaScript handelt, werden sie als Methoden definiert des Window-Objekts.
Aber als allgemeine Funktion wird es eigentlich nichts mit dem Fenster tun.
Die setTImeout()
-Methode des Window-Objekts wird verwendet, um eine Funktion zu implementieren, die nach einer angegebenen Anzahl von Millisekunden ausgeführt wird. Es akzeptiert also zwei Parameter: Der erste ist die Rückruffunktion und der zweite ist die Anzahl der Millisekunden, um die die Ausführung verzögert wird. setTimeout()
und setInterval()
geben einen Wert zurück, der an clearTimeout()
übergeben werden kann, um die Ausführung dieser Funktion abzubrechen.
console.log(1); setTimeout(function(){console.log(2);}, 1000);console.log(3);
Die Ausführungsergebnisse des obigen Codes sind 1, 3, 2, da setTimeout()
die Ausführung der zweiten Zeile auf 1000 Millisekunden später verschiebt.
Wenn der zweite Parameter von setTimeout()
auf 0 gesetzt ist, bedeutet dies, dass nach der Ausführung des aktuellen Codes (der Ausführungsstapel wird geleert) die angegebene Rückruffunktion sofort ausgeführt wird (Intervall von 0 Millisekunden).
setTimeout(function(){console.log(1);}, 0);console.log(2)
Das Ausführungsergebnis des obigen Codes ist immer 2,1, da das System die Rückruffunktion in der „Aufgabenwarteschlange“ erst ausführt, nachdem die zweite Zeile ausgeführt wurde.
Kurz gesagt besteht die Bedeutung von setTimeout(fn,0)
darin, eine Aufgabe anzugeben, die in der frühesten verfügbaren Leerlaufzeit des Hauptthreads ausgeführt werden soll, also so früh wie möglich ausgeführt werden soll. Es fügt am Ende der „Aufgabenwarteschlange“ ein Ereignis hinzu, sodass es erst ausgeführt wird, wenn die Synchronisierungsaufgabe und die vorhandenen Ereignisse in der „Aufgabenwarteschlange“ verarbeitet wurden.
HTML5标准规定了
setTimeout()
的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。
需要注意的是,setTimeout()
只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()
指定的时间执行。
由于历史原因,setTimeout()
和setInterval()
的第一个参数可以作为字符串传入。如果这么做,那这个字符串会在指定的超时时间或间隔之后进行求值(相当于执行eval()
)。
Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。
Node.js的运行机制如下。
(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与”任务队列”有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对”任务队列”的理解。
process.nextTick方法可以在当前”执行栈”的尾部—-下一次Event Loop(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子
process.nextTick(function A() {console.log(1);process.nextTick(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0)// 1// 2// TIMEOUT FIRED
上面代码中,由于process.nextTick方法指定的回调函数,总是在当前”执行栈”的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前”执行栈”执行。
现在,再看setImmediate。
setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0);
上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1–2。
令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。
setImmediate(function (){setImmediate(function A() {console.log(1); setImmediate(function B(){console.log(2);});}); setTimeout(function timeout() {console.log('TIMEOUT FIRED'); }, 0); }); // 1 // TIMEOUT FIRED // 2
上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1–TIMEOUT FIRED–2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。
我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前”执行栈”一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取”事件队列”!
process.nextTick(function foo() {process.nextTick(foo); });
事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。
另外,由于process.nextTick指定的回调函数是在本次”事件循环”触发,而setImmediate指定的是在下次”事件循环”触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查”任务队列”)。
除了广义的同步任务和异步任务,任务还有更精细的定义:
macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick
事件循环,宏任务,微任务的关系如图所示:
按照宏任务和微任务这种分类方式,JS的执行机制是
执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完
请看下面的例子:
setTimeout(function(){ console.log('定时器开始啦') }); new Promise(function(resolve){ console.log('马上执行for循环啦'); for(var i = 0; i < 10000; i++){ i == 99 && resolve(); } }).then(function(){ console.log('执行then函数啦') }); console.log('代码执行结束');
首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
遇到 new Promise直接执行,打印”马上执行for循环啦”
遇到then方法,是微任务,将其放到微任务的【队列里】
打印 “代码执行结束”
本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印”执行then函数啦”
到此,本轮的event loop 全部完成。
下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印”定时器开始啦”
所以最后的执行顺序是【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】
我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:
console.log('1'); setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) })process.nextTick(function() { console.log('6'); }) new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { console.log('8') }) setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) })
第一轮事件循环流程分析如下:
整体script作为第一个宏任务进入主线程,遇到console.log
,输出1。
遇到setTimeout
,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
。
遇到process.nextTick()
,其回调函数被分发到微任务Event Queue中。我们记为process1
。
遇到Promise
,new Promise
直接执行,输出7。then
被分发到微任务Event Queue中。我们记为then1
。
又遇到了setTimeout
,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | process1 |
setTimeout2 | then1 |
* 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
我们发现了process1
和then1
两个微任务。
执行process1
,输出6。
执行then1
,输出8。
好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1
宏任务开始:
首先输出2。接下来遇到了process.nextTick()
,同样将其分发到微任务Event Queue中,记为process2
。new Promise
立即执行输出4,then
也分发到微任务Event Queue中,记为then2
。
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout2 | process2 |
then2 |
* 第二轮事件循环宏任务结束,我们发现有process2
和then2
两个微任务可以执行。
* 输出3。
* 输出5。
* 第二轮事件循环结束,第二轮输出2,4,3,5。
* 第三轮事件循环开始,此时只剩setTimeout2了,执行。
* 直接输出9。
* 将process.nextTick()
分发到微任务Event Queue中。记为process3
。
* 直接执行new Promise
,输出11。
* 将then
分发到微任务Event Queue中,记为then3
。
宏任务Event Queue | 微任务Event Queue |
---|---|
process3 | |
then3 |
* 第三轮事件循环宏任务执行结束,执行两个微任务process3
和then3
。
* 输出10。
* 输出12。
* 第三轮事件循环结束,第三轮输出9,11,10,12。
整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)
Das obige ist der detaillierte Inhalt vonDetaillierte Erläuterung der Beispiele für js-Ausführungsmechanismen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!