Heim >Web-Frontend >js-Tutorial >Lassen Sie uns über das Ereignisschleifenmodell von setTimeout sprechen
Als Entwickler, der von anderen Programmiersprachen (C#/Java) auf Javascript umgestiegen ist, ist mir beim Erlernen von Javascript das Funktionsprinzip der setTimeout()-Methode auf einen Teil gestoßen, der nicht leicht zu verstehen ist. In diesem Artikel wird versucht, die Implementierung mit anderen Programmiersprachen aus dem setTimeout-Ereignisschleifenmodell
zu kombinieren
Die setTimeout()-Methode wird nicht durch die Ecmascript-Spezifikation definiert, sondern ist eine vom BOM bereitgestellte Funktion. Siehe w3schools Definition der setTimeout()-Methode. Die setTimeout()-Methode wird verwendet, um eine Funktion aufzurufen oder einen Ausdruck nach einer bestimmten Anzahl von Millisekunden zu berechnen.
Syntax setTimeout(fn, millisec), wobei fn den auszuführenden Code darstellt, der eine Zeichenfolge mit JavaScript-Code oder eine Funktion sein kann. Der zweite Parameter Millisekunden ist die in Millisekunden ausgedrückte Zeit und gibt an, wie lange fn verzögert werden muss.
Nach dem Aufruf der Methode setTimeout() gibt die Methode eine Nummer zurück. Diese Nummer ist eine eindeutige Kennung des geplanten Ausführungscodes, mit der der Timeout-Aufruf abgebrochen werden kann.
Anfangs war die Verwendung von setTimeout() für mich relativ einfach und ich hatte kein tiefes Verständnis für den Funktionsmechanismus, bis ich den folgenden Code sah
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
In meinem anfänglichen Verständnis von setTimeout() war die Verzögerung auf 500 ms eingestellt, daher sollte die Ausgabe „Zeit verstrichen: 500 ms“ lauten. Denn in einem intuitiven Verständnis sollte die Javascript-Ausführungs-Engine bei der Ausführung des obigen Codes ein sequentieller Ausführungsprozess von oben nach unten sein und die Funktion setTimeout vor der while-Anweisung ausführen. Tatsächlich verzögert sich die Ausgabe jedoch um mindestens 1000 ms, nachdem der obige Code mehrmals ausgeführt wurde.
Das oben erwähnte setTimeout() von Javascript erinnert mich an meine früheren Erfahrungen beim Erlernen von Java. Java verfügt über mehrere API-Implementierungen für setTimeout. Hier nehmen wir das Paket java.util.Timer als Beispiel. Verwenden Sie den Timer, um die obige Logik in Java zu implementieren. Nach mehrmaliger Ausführung lautet die Ausgabe: Verstrichene Zeit: 501 ms.
import java.util.Date; import java.util.Timer; import java.util.TimerTask; public class TimerTest { public static void main(String[] args) { // TODO Auto-generated method stub long start = System.currentTimeMillis(); Timer timer = new Timer(); timer.schedule(new MyTask(start), 500); while (System.currentTimeMillis() - start < 1000) {}; } } class MyTask extends TimerTask { private long t; public MyTask(long start) { // TODO Auto-generated constructor stub t=start; } @Override public void run() { // TODO Auto-generated method stub long end = System.currentTimeMillis(); System.out.println("Time elapsed:"+(end - this.t)+ "ms"); } }
Bevor wir näher darauf eingehen, warum dieser Unterschied in setTimeout() auftritt, sprechen wir zunächst über das Implementierungsprinzip von java.util.Timer.
Die Schlüsselelemente des obigen Codes sind die Klassen Timer, TimerTask und die Zeitplanmethode der Timer-Klasse. Sie können ihre Implementierung verstehen, indem Sie den entsprechenden Quellcode lesen.
Timer: Die Planungsklasse einer Task-Aufgabe. Es handelt sich um eine API-Klasse, mit der Benutzer den Ausführungsplan der Task über die Schedule-Methode festlegen können. Diese Klasse vervollständigt die Aufgabenplanung über die TaskQueue-Aufgabenwarteschlange und die TimerThread-Klasse.
TimerTask: Implementiert die Runnable-Schnittstelle, um anzuzeigen, dass jede Aufgabe ein unabhängiger Thread ist, und ermöglicht es Benutzern, ihre eigenen Aufgaben über die run()-Methode anzupassen.
TimerThread: Von Thread geerbt, ist es die Klasse, die die Aufgabe tatsächlich ausführt.
TaskQueue: Eine Datenstruktur, die intern durch einen minimalen Heap implementiert wird. Jede Aufgabe wird nach dem nextExecutionTime-Attributwert von TimerTask sortiert der Warteschlange, so dass die früheste Implementierung erfolgt.
Nachdem wir uns die Implementierung von setTimeout() durch Java.util.Timer angesehen haben, kehren wir zur setTimeout()-Methode von Javascript zurück und sehen, warum die vorherige Ausgabe nicht den Erwartungen entspricht.
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
Beim Lesen des Codes ist es nicht schwer zu erkennen, dass die setTimeout()-Methode vor der while()-Schleife ausgeführt wird. Sie gibt an, dass sie „hofft“, die anonyme Funktion einmal nach 500 ms auszuführen Die anonyme Funktion befindet sich in der setTimeout()-Methode. Sie wird sofort nach der Ausführung wirksam. Die while-Schleife in der letzten Zeile des Codes läuft 1000 ms lang weiter. Die Verzögerungszeit der über die setTimeout()-Methode registrierten Ausgabe ist immer größer als 1000 ms, was darauf hinweist, dass der tatsächliche Aufruf dieser anonymen Funktion erfolgt Der eigentliche Aufruf erfolgt durch die while()-Schleife. Die while()-Schleife wird nach dem Blockieren tatsächlich ausgeführt.
In Java.util.Timer wird die Lösung geplanter Aufgaben durch Multithreading implementiert. Das Aufgabenobjekt wird in der Aufgabenwarteschlange gespeichert und ein dedizierter Planungsthread schließt die Ausführung der Aufgabe in einem neuen Unterthread ab. Wenn Sie eine asynchrone Aufgabe über die Methode „schedule()“ registrieren, beginnt der Planungsthread sofort mit der Arbeit im untergeordneten Thread und der Hauptthread blockiert nicht die Ausführung der Aufgabe.
Dies ist ein wesentlicher Unterschied zwischen Javascript und Sprachen wie Java/C#, also dem Single-Thread-Mechanismus von Javascript. In der vorhandenen Browserumgebung ist die Javascript-Ausführungs-Engine Single-Threaded. Die Ausführung geplanter Aufgaben wird von der Ausführungs-Engine erst nach Ausführung der Anweisungen des Haupt-Threads blockiert ist Der Zeitraum kann größer sein als die bei der Registrierung der Aufgabe festgelegte Verzögerungszeit. Zu diesem Zeitpunkt sind die Mechanismen von Javascript und Java/C# sehr unterschiedlich.
Wie funktioniert setTimeout() in einer Single-Threaded-Javascript-Engine? Hier erwähnen wir das Ereignisschleifenmodell im Browserkernel. Einfach ausgedrückt gibt es außerhalb der Javascript-Ausführungs-Engine eine Aufgabenwarteschlange. Wenn die setTimeout()-Methode im Code aufgerufen wird, wird die registrierte Verzögerungsmethode an andere Module im Browser-Kernel übergeben (wobei das Webkit als verwendet wird). Bei der Verarbeitung handelt es sich beispielsweise um das Webcore-Modul. Wenn die Verzögerungsmethode die Auslösebedingung erreicht, dh wenn die eingestellte Verzögerungszeit erreicht ist, wird diese Verzögerungsmethode der Aufgabenwarteschlange hinzugefügt. Dieser Prozess wird von anderen Modulen des Browserkernels abgewickelt und ist unabhängig vom Hauptthread der Ausführungs-Engine. Nachdem die Ausführung der Haupt-Thread-Methode abgeschlossen ist und die Ausführungs-Engine den Ruhezustand erreicht, ruft sie nacheinander Aufgaben von der Aufgabe ab Warteschlange zur Ausführung. Dieser Prozess ist eine kontinuierliche Schleife. Der Prozess wird als Ereignisschleifenmodell bezeichnet.
Unter Bezugnahme auf die Informationen in einer Rede kann das obige Ereignisschleifenmodell anhand der folgenden Abbildung beschrieben werden.
Wenn der Hauptthread der Javascript-Ausführungs-Engine ausgeführt wird, werden ein Heap und ein Stapel generiert. Der Code im Programm gelangt nacheinander in den Stapel und wartet auf die Ausführung. Wenn die Methode setTimeout() aufgerufen wird, dh wenn die WebAPIs-Methode auf der rechten Seite der Abbildung aufgerufen wird, wird das entsprechende Modul des Browserkernels gestartet Wenn die Verzögerungsmethode die Auslösebedingung erreicht, wird die Methode für den Rückruf zur Aufgabenwarteschlange hinzugefügt. Solange der Code im Ausführungs-Engine-Stack ausgeführt wird, liest der Hauptthread die Aufgabenwarteschlange und führt diesen Rückruf aus Funktionen, die die Triggerbedingungen nacheinander erfüllen.
Verwenden Sie Beispiele in der Rede, um dies näher zu erläutern
Nehmen wir den Code im Bild als Beispiel: Wenn die Ausführungs-Engine mit der Ausführung des obigen Codes beginnt, entspricht dies dem Hinzufügen einer main()-Methode zum Ausführungsstapel. Wenn Sie console.log('Hi') weiter starten, wird die Methode log('Hi') auf den Stapel verschoben. Die Methode console.log ist eine allgemeine Methode, die vom Webkit-Kernel unterstützt wird, nicht die Methode, die in WebAPIs im enthalten ist Die vorherige Abbildung zeigt, dass die Methode „Hier loggen“ („Hi“) sofort vom Stapel entfernt und von der Engine ausgeführt wird.
Nachdem die Anweisung console.log('Hi') ausgeführt wurde, wird die Methode log() aus dem Stapel entfernt und Hi ausgegeben. Die Engine läuft weiter herunter und fügt setTimeout(callback,5000) zum Ausführungsstapel hinzu. Die setTimeout()-Methode gehört zur Methode in WebAPIs im Ereignisschleifenmodell. Wenn die Engine die setTimeout()-Methode zur Ausführung vom Stapel entfernt, übergibt sie die verzögerte Ausführungsfunktion an das entsprechende Modul, also das Timer-Modul auf der rechten Seite der Abbildung zur Bearbeitung.
Wenn die Ausführungs-Engine setTimeout zur Ausführung vom Stapel entfernt, übergibt sie die Verzögerungsverarbeitungsmethode an das Webkit-Timer-Modul und fährt dann sofort mit der Verarbeitung des folgenden Codes fort, sodass log('SJS') zum Ausführungsstapel hinzugefügt wird und dann wird log('SJS' ) aus dem Stapel ausgeführt und gibt SJS aus. Nachdem die Ausführungs-Engine console.log('SJS') ausführt, wird das Programm verarbeitet und die main()-Methode wird ebenfalls vom Stapel entfernt.
Zu diesem Zeitpunkt, 5 Sekunden nach Ausführung der setTimeout-Methode, erkennt das Timer-Modul, dass die Verzögerungsverarbeitungsmethode die Auslösebedingung erreicht, und fügt die Verzögerungsverarbeitungsmethode daher der Aufgabenwarteschlange hinzu. Zu diesem Zeitpunkt ist der Ausführungsstapel der Ausführungs-Engine leer, daher beginnt die Engine mit der Abfrage, ob Aufgaben in der Aufgabenwarteschlange vorhanden sind, die ausgeführt werden müssen. Sie erkennt, dass die Verzögerungsmethode die Ausführungsbedingung erreicht hat, und fügt sie hinzu die Verzögerungsmethode an den Ausführungsstapel. Die Engine hat festgestellt, dass die Verzögerungsmethode die log()-Methode aufgerufen hat, und hat daher die log()-Methode auf den Stapel verschoben. Dann wird der Ausführungsstapel nacheinander herausgenommen, dort ausgegeben und der Ausführungsstapel gelöscht.
Nach dem Löschen des Ausführungsstapels fragt die Ausführungs-Engine weiterhin die Aufgabenwarteschlange ab, um zu prüfen, ob noch Aufgaben vorhanden sind, die ausgeführt werden können.
到这里已经可以彻底理解下面代码的执行流程,执行引擎先将setTimeout()方法入栈被执行,执行时将延时方法交给内核相应模块处理。引擎继续处理后面代码,while语句将引擎阻塞了1秒,而在这过程中,内核timer模块在0.5秒时已将延时方法添加到任务队列,在引擎执行栈清空后,引擎将延时方法入栈并处理,最终输出的时间超过预期设置的时间。
var start = new Date; setTimeout(function(){ var end = new Date; console.log('Time elapsed:', end - start, 'ms'); }, 500); while (new Date - start < 1000) {};
前面事件循环模型图中提到的WebAPIs部分,提到了DOM事件,AJAX调用和setTimeout方法,图中简单的把它们总结为WebAPIs,而且他们同样都把回调函数添加到任务队列等待引擎执行。这是一个简化的描述,实际上浏览器内核对DOM事件、AJAX调用和setTimeout方法都有相应的模块来处理,webkit内核在Javasctipt执行引擎之外,有一个重要的模块是webcore模块,html的解析,css样式的计算等都由webcore实现。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现,这里还是继续以setTimeout为例,看下timer模块的实现。
Timer类是webkit 内核的一个必需的基础组件,通过阅读源码可以全面理解其原理,本文对其简化,分析其执行流程。
通过setTimeout()方法注册的延时方法,被传递给webcore组件timer模块处理。timer中关键类为TheadTimers类,其包含两个重要成员,TimerHeap任务队列和SharedTimer方法调度类。延时方法被封装为timer对象,存储在TimerHeap中。和Java.util.Timer任务队列一样,TimerHeap同样采用最小堆的数据结构,以nextFireTime作为关键字排序。SharedTimer作为TimerHeap调度类,在timer对象到达触发条件时,通过浏览器平台相关的接口,将延时方法添加到事件循环模型中提到的任务队列中。
TimerHeap采用最小堆的数据结构,预期延时时间最小的任务最先被执行,同时,预期延时时间相同的两个任务,其执行顺序是按照注册的先后顺序执行。
var start = new Date; setTimeout(function(){ console.log('fn1'); }, 20); setTimeout(function(){ console.log('fn2'); }, 30); setTimeout(function(){ console.log('another fn2'); }, 30); setTimeout(function(){ console.log('fn3'); }, 10); console.log('start while'); while (new Date - start < 1000) {}; console.log('end while');
上述代码输出依次为
start while end while fn3 fn1 fn2 another fn2
1.《Javascript异步编程》
2.JavaScript 运行机制详解:再谈Event Loophttp://www.ruanyifeng.com/blog/2014/10/event-loop.html
3.Philip Roberts: Help, I'm stuck in an event-loop.https://vimeo.com/96425312
4.How JavaScript Timers Work.http://ejohn.org/blog/how-javascript-timers-work/
5.How WebKit’s event model works.http://brrian.tumblr.com/post/13951629341/how-webkits-event-model-works
6.Timer实现.http://blog.csdn.net/shunzi__1984/article/details/6193023
Das obige ist der detaillierte Inhalt vonLassen Sie uns über das Ereignisschleifenmodell von setTimeout sprechen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!