In diesem Artikel wird der JavaScript-Timing-Mechanismus vorgestellt. Um den Timing-Mechanismus von JavaScript zu verstehen, müssen Sie den Ausführungsmechanismus von JavaScript kennen.
Zuallererst ist JavaScript ereignisgesteuert, indem es in einem einzelnen Thread (JavaScript-Engine-Thread) ausgeführt wird.
1. Es gibt mehrere Threads im Browser
Die grundlegendsten Threads, die in einem Browser enthalten sind:
1. JavaScript-Engine-Thread.
2. Timer-Thread, setInterval und setTimeout lösen diesen Thread aus.
3. Browser-Ereignis-Trigger-Thread, dieser Thread löst Onclick-, Onmousemove- und andere Browser-Events aus.
4. Der Interface-Rendering-Thread ist für das Rendern der HTML-Elemente der Browser-Oberfläche verantwortlich. Hinweis: Während die JavaScript-Engine das Skript ausführt, befindet sich der Schnittstellen-Rendering-Thread in einem angehaltenen Zustand. Das heißt, wenn JavaScript zum Betreiben von Knoten in der Schnittstelle verwendet wird, wird dies nicht sofort widergespiegelt, bis der JavaScript-Engine-Thread inaktiv ist. (Dies ist der letzte)
5. HTTP-Anfragethread (Ajax-Anfragen gehören auch dazu).
Die oben genannten Threads arbeiten zusammen, um die Arbeit unter der Kontrolle des Browserkernels abzuschließen (ich kenne die Details nicht).
2. Aufgabenwarteschlange
Wir wissen, dass JavaScript Single-Threaded ist und der gesamte JavaScript-Code im JavaScript-Engine-Thread ausgeführt wird. Der Artikel von Lehrer Ruan Yifeng nennt diesen Thread den Hauptthread, bei dem es sich um einen Ausführungsstapel handelt. (Der folgende Inhalt basiert hauptsächlich auf dem Verständnis und der Zusammenfassung des Artikels von Lehrer Ruan Yifeng.)
Wir können diese JavaScript-Codes einzeln als Aufgaben betrachten. Diese Aufgaben sind in synchrone Aufgaben und asynchrone Aufgaben unterteilt. Synchrone Aufgaben (z. B. Variablenzuweisungsanweisungen, Warnanweisungen, Funktionsdeklarationsanweisungen usw.) werden nacheinander direkt im Hauptthread ausgeführt, und asynchrone Aufgaben (z. B. verschiedene Ereignisse, die durch Browser-Ereignistrigger-Threads ausgelöst werden, verwenden von Ajax zurückgegebene Serverantworten usw.) werden in chronologischer Reihenfolge in der Aufgabenwarteschlange (auch Ereigniswarteschlange, Nachrichtenwarteschlange genannt) eingereiht und warten auf ihre Ausführung. Solange die Aufgaben im Hauptthread ausgeführt werden, wird die Aufgabenwarteschlange überprüft, um festzustellen, ob in der Warteschlange Aufgaben warten. Wenn dies der Fall ist, werden die in der Warteschlange befindlichen Aufgaben zur Ausführung in den Hauptthread eingegeben.
Zum Beispiel das folgende Beispiel:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>定时机制</title> <style type="text/css"> body{ margin: 0; padding: 0; position: relative; height: 600px; } #test{ height: 30px; width: 30px; position: absolute; left: 0; top: 100px; background-color: pink; } </style> </head> <body> <div id="test"> </div> <script> var pro = document.getElementById('test'); pro.onclick = function() { alert('我没有立即被执行。'); }; function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } test(); </script> </body> </html>
In diesem Beispiel dauert es etwa 8 bis 9 Sekunden für test() Wenn wir also diese Seite öffnen und vor 8 Sekunden auf das rosa Kästchen klicken, wird das Eingabeaufforderungsfeld nicht sofort angezeigt, sondern erst 8 Sekunden später, wenn wir vorher mehrmals auf das rosa Kästchen klicken Nach ca. 8 Sekunden wird es mehrmals angezeigt.
Wenn wir diese Seite öffnen, deklariert der Hauptthread zuerst den Funktionstest, dann die Variable pro, weist dann den p-Knoten pro zu, fügt dann dem p-Knoten ein Klickereignis hinzu und gibt eine Rückruffunktion an (Suspendierung) und rufen Sie dann die Testfunktion auf und führen Sie den darin enthaltenen Code aus. Während der Ausführung des Codes in der Testfunktion haben wir auf den p-Knoten geklickt. Der Browser-Ereignistrigger-Thread hat dieses Ereignis erkannt und das Ereignis in die Aufgabenwarteschlange gestellt, sodass die Aufgabe im Hauptthread (hier der Testfunktion) liegt Abschließend wird dieses Ereignis bei der Überprüfung der Aufgabenwarteschlange gefunden und die entsprechende Callback-Funktion ausgeführt. Wenn wir mehrmals klicken, werden diese mehrfach ausgelösten Ereignisse entsprechend der Auslösezeit in die Aufgabenwarteschlange eingereiht (Sie können ein Klickereignis einem anderen Element hinzufügen und zur Überprüfung abwechselnd auf verschiedene Elemente klicken).
Das Folgende ist eine Zusammenfassung des Ausführungsmechanismus der Aufgabe:
Der Ausführungsmechanismus der asynchronen Ausführung ist wie folgt. (Dasselbe gilt für die synchrone Ausführung, da sie als asynchrone Ausführung ohne asynchrone Aufgaben betrachtet werden kann.)
1. Alle synchronen Aufgaben werden im Hauptthread ausgeführt und bilden einen Ausführungskontextstapel.
2. Zusätzlich zum Hauptthread gibt es auch eine „Aufgabenwarteschlange“. Solange die asynchrone Aufgabe laufende Ergebnisse hat, wird ein Ereignis in die „Aufgabenwarteschlange“ gestellt.
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, betreten den Ausführungsstapel und beginnen mit der Ausführung.
4. Der Hauptthread wiederholt den dritten Schritt oben.
3. Ereignisse und Rückruffunktionen
Wenn wir ein Ereignis für ein DOM-Element angeben, geben wir eine Rückruffunktion an, damit der entsprechende Code ausgeführt werden kann, wenn das Ereignis tatsächlich eintritt .
Die Rückruffunktion des Ereignisses im Hauptthread wird angehalten. Wenn sich ein entsprechendes Ereignis in der Aufgabenwarteschlange befindet, wird die entsprechende Rückruffunktion ausgeführt, wenn der Hauptthread es erkennt. Wir können auch sagen, dass der Hauptthread asynchrone Aufgaben ausführt und die entsprechende Rückruffunktion ausführt.
4. Ereignisschleife
Der Prozess der Überprüfung von Ereignissen in der Aufgabenwarteschlange durch den Hauptthread ist zyklisch, sodass wir ein Diagramm der Ereignisschleife zeichnen können:
In der obigen Abbildung generiert der Hauptthread einen Heap und einen Ausführungsstapel. Nachdem die Aufgaben im Stapel ausgeführt wurden, überprüft der Hauptthread die Aufgabenwarteschlange auf Ereignisse Wurden von anderen Threads übergeben, wird die dem Ereignis entsprechende Rückruffunktion aus den ausstehenden Rückruffunktionen gefunden und dann im Ausführungsstapel ausgeführt.
5. Timer
结合以上知识,下面探讨JavaScript中的定时器:setTimeout()和setInterval()。
setTimeout(func, t)是超时调用,间隔一段时间后调用函数。这个过程在事件循环中的过程如下(我的理解):
主线程执行完setTimeout(func, t);语句后,把回调函数func挂起,同时定时器线程开始计时,当计时等于t时,相当于发生了一个事件,这个事件传入任务队列(结束计时,只计时一次),当主线程中的任务执行完后,主线程检查任务队列发现了这个事件,就执行挂起的回调函数func。我们指定的时间间隔t只是参考值,真正的时间间隔取决于执行完setTimeout(func, t);语句后的代码所花费的时间,而且是只大不小。(即使我们把t设为0,也要经历这个过程)。
setInterval(func, t)是间歇调用,每隔一段时间后调用函数。这个过程在事件循环中的过程与上面的类似,但又有所不同。
setTimeout()是经过时间t后定时器线程在任务队列中添加一个事件(注意是一个),而setInterval()是每经过时间t(一直在计时,除非清除间歇调用)后定时器线程在任务队列中添加一个事件,而不管之前添加的事件有没有被主线程检测到并执行。(实际上浏览器是比较智能的,浏览器在处理setInterval的时候,如果发现已任务队列中已经有排队的同一ID的setInterval的间歇调用事件,就直接把新来的事件 Kill 掉。也就是说任务队列中一次只能存在一个来自同一ID的间歇调用的事件。)
举个例子,假如执行完setInterval(func, t);后的代码要花费2t的时间,当2t时间过后,主线程从任务队列中检测到定时器线程传入的第一个间歇调用事件,func开始执行。当第一次的func执行完毕后,第二次的间歇调用事件早已传入任务队列,主线程马上检测到第二次的间歇调用事件,func函数又立即执行。这种情况下func函数的两次执行是连续发生的,中间没有时间间隔。
下面是个例子:
function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } function test2() { var d = new Date().valueOf(); //var e = d-a; console.log('我被调用的时刻是:'+d+'ms'); //alert(1); } setInterval(test2,3000); test();
结果:
为什么8.6秒过后没有输出两个一样的时刻,原因在上面的内容中可以找到。
执行例子中的for循环花费了8601ms,在执行for循环的过程中队列中只有一个间歇调用事件在排队(原因如上所述),当8601ms过后,第一个间歇调用事件进入主线程,对于这个例子来说此时任务队列空了,可以再次传入间歇调用事件了,所以1477462632228ms这个时刻第二次间歇调用事件(实际上应该是第三次)传入任务队列,由于主线程的执行栈已经空了,所以主线程立即把对应的回调函数拿来执行,第二次调用与第一次调用之间仅仅间隔了320ms(其实8601+320=8920,差不多就等于9秒了)。我们看到第三次调用已经恢复正常了,因为此时主线程中已经没有其他代码了,只有一个任务,就是隔一段时间执行一次间歇调用的回调函数。
用setTimeout()实现间歇调用的例子:
function test() { a = new Date(); var b=0; for(var i=0;i<3000000000;i++) { b++; } c = new Date(); console.log(c-a); } function test2(){ var d = new Date().valueOf(); console.log('我被调用的时刻是:'+d+'ms'); setTimeout(test2,3000); } setTimeout(test2,3000); test();
结果:
每两次调用的时间间隔基本上是相同。想想为什么?
再看一个例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Flex布局练习</title> <style type="text/css"> body{ margin: 0; padding: 0; position: relative; height: 600px; } #test{ height: 30px; width: 30px; position: absolute; left: 0; top: 100px; background-color: pink; } </style> </head> <body> <div id="test"> </div> <script> var p = document.createElement('p'); p.style.width = '50px'; p.style.height = '50px'; p.style.border = '1px solid black'; document.body.appendChild(p); alert('ok'); </script> </body> </html>
这个例子的结果是提示框先弹出,然后黑色边框的p元素才出现在页面中。原因很简单,就一句话:
在JavaScript引擎运行脚本期间,界面渲染线程都是处于挂起状态的。也就是说当使用JavaScript对界面中的节点进行操作时,并不会立即体现出来,要等到JavaScript引擎线程空闲时,才会体现出来。