Heim >Web-Frontend >js-Tutorial >Front-End Advanced (12): Detaillierte Erläuterung des Ereignisschleifenmechanismus
JavaScript Lernen ist verstreut und kompliziert, so oft lernen wir etwas, aber wir können es nicht spüren . Wenn ich Fortschritte gemacht habe, vergesse ich auch nach einer Weile, was ich gelernt habe. Um mein Problem zu lösen, habe ich versucht, während des Lernprozesses einen zentralen Hinweis zu finden. Solange ich diesem Hinweis folge, kann ich nach und nach Fortschritte machen.
Die grundlegende Weiterentwicklung des Front-Ends entwickelt sich langsam um diesen Hinweis herum, und der EventLoop-Mechanismus (Event Loop) ist dies am meisten kritische Wissenspunkte von Hinweisen. Deshalb habe ich den Ereignisschleifenmechanismus weiter eingehend untersucht und diesen Artikel zusammengefasst, um ihn mit Ihnen zu teilen.
Der Ereignisschleifenmechanismus als Ganzes teilt uns die Ausführungssequenz des von uns geschriebenen JavaScript-Codes mit. Während meiner Recherche fand ich jedoch viele inländische Blog-Artikel, die nur eine oberflächliche Erklärung lieferten. Viele Artikel zeichneten einen Kreis in das Bild, um einen Zyklus anzuzeigen. Nach dem Lesen hatte ich nicht das Gefühl, dass ich viel verstanden habe . Es ist jedoch so wichtig, dass bei Vorstellungsgesprächen für Positionen auf mittlerer bis höherer Ebene der Ereignisschleifenmechanismus immer ein Thema ist, das nicht vermieden werden kann. Insbesondere nachdem PromiseObject offiziell zu ES6 hinzugefügt wurde, ist das Verständnis des Ereignisschleifenmechanismus im neuen Standard noch wichtiger geworden. Das ist sehr peinlich.
Es gab kürzlich zwei beliebte Artikel, die ebenfalls die Bedeutung dieses Themas zum Ausdruck brachten.
Dieses Front-End-Interview verursacht Ärger
80 % der Kandidaten bestehen die JS-Interviewfrage nichtAber leider haben die Meister allen gesagt, dass dieser Wissenspunkt sehr wichtig ist, aber nicht Ich kann nicht jedem sagen, warum das passiert ist. Wenn wir also während eines Interviews auf eine solche Frage stoßen, sind wir immer noch verwirrt, selbst wenn wir das Ergebnis kennen und der Interviewer weitere Fragen stellt.
Bevor Sie den Ereignisschleifenmechanismus erlernen, gehe ich davon aus, dass Sie die folgenden Konzepte bereits verstehen. Wenn Sie noch Fragen haben, können Sie zurückgehen und meine vorherigen Artikel lesen.
Ausführungskontext
WarteschlangeDatenstruktur (Warteschlange)
Promise (Ich werde die detaillierte Verwendung von Promise im nächsten Artikel mit benutzerdefinierter Kapselung zusammenfassen)
Da der Ereignisschleifenmechanismus im neuen Standard von chrome Browser fast derselbe ist wie NodeJS, ist er hier integriert. Lassen Sie uns gemeinsam NodeJS verstehen . Wir werden mehrere APIs vorstellen, die nodejs hat, aber nicht im Browser. Sie müssen es nur verstehen und nicht unbedingt wissen, wie man es verwendet. Beispiel: Prozess.WeiterAnkreuzen, EinstellenSofort
OK, dann gebe ich zuerst die Schlussfolgerung und demonstriere dann das Ereignis im Detail anhand von Beispielen und Illustrationen.
Wir wissen, dass ein Hauptmerkmal von JavaScript ein einzelner Thread ist und dieser Thread über eine einzigartige Ereignisschleife verfügt.
Natürlich beinhaltet der Web-Worker im neuen Standard Multithreading. Ich weiß nicht viel darüber, deshalb werde ich hier nicht darauf eingehen.
Während der Ausführung von JavaScript-Code verlässt es sich nicht nur auf den Funktionsaufrufstapel, um die Ausführungsreihenfolge von Funktionen zu bestimmen, sondern auch auf die Aufgabenwarteschlange, um andere Codes auszuführen . .
In einem Thread ist die Ereignisschleife eindeutig, aber Sie können mehrere Aufgabenwarteschlangen haben.
Die Aufgabenwarteschlange ist in Makroaufgabe (Makroaufgabe) und Mikroaufgabe (Mikroaufgabe) unterteilt. In den neuesten Standards werden sie als Aufgaben bzw. Jobs bezeichnet.
Makroaufgabe umfasst wahrscheinlich: Skript (Gesamtcode), setTimeout, setInterval, setImmediate, I/O, UI rendering.
Mikroaufgabe umfasst wahrscheinlich: Process.nextTick, Promise, Object.observe (veraltet), MutationObserver (html5neue Funktion)
setTimeout/Promise usw. werden als Aufgabenquellen bezeichnet. Was in die Aufgabenwarteschlange gelangt, ist die spezifische Ausführungsaufgabe, die sie angegeben haben.
// setTimeout中的回调函数才是进入任务队列的任务 setTimeout(function() { console.log('xxxx'); })
Aufgaben aus verschiedenen Aufgabenquellen werden in unterschiedliche Aufgabenwarteschlangen eingegeben. Unter diesen haben setTimeout und setInterval denselben Ursprung.
Die Reihenfolge der Ereignisschleife bestimmt die Ausführungsreihenfolge des JavaScript-Codes. Es startet die erste Schleife des Skripts (des Gesamtcodes). Der globale Kontext gelangt dann in den Funktionsaufrufstapel. Bis der Aufrufstapel geleert ist (nur der globale ist übrig), werden alle Mikroaufgaben ausgeführt. Nachdem alle ausführbaren Mikroaufgaben ausgeführt wurden. Die Schleife beginnt erneut bei der Makroaufgabe, findet eine der Aufgabenwarteschlangen und führt dann alle Mikroaufgaben aus, und die Schleife wird fortgesetzt.
Die Ausführung jeder Aufgabe, egal ob es sich um eine Makroaufgabe oder eine Mikroaufgabe handelt, wird mithilfe eines Funktionsaufrufstapels abgeschlossen.
Der reine Textausdruck ist in der Tat etwas trocken, daher verwenden wir hier zwei Beispiele, um die spezifische Reihenfolge der Ereignisschleife schrittweise zu verstehen.
// demo01 出自于上面我引用文章的一个例子,我们来根据上面的结论,一步一步分析具体的执行过程。 // 为了方便理解,我以打印出来的字符作为当前的任务名称 setTimeout(function() { console.log('timeout1'); }) new Promise(function(resolve) { console.log('promise1'); for(var i = 0; i < 1000; i++) { i == 99 && resolve(); } console.log('promise2'); }).then(function() { console.log('then1'); }) console.log('global1');
Zuerst beginnt die Ereignisschleife in der Makroaufgabenwarteschlange. Zu diesem Zeitpunkt befindet sich nur eine Skriptaufgabe (vollständiger Code) in der Makroaufgabenwarteschlange. Die Ausführungsreihenfolge jeder Aufgabe wird durch den Funktionsaufrufstapel bestimmt. Wenn eine Aufgabenquelle gefunden wird, wird die Aufgabe zuerst an die entsprechende Warteschlange verteilt. Daher ist der erste Schritt des obigen Beispiels in der folgenden Abbildung dargestellt.
Schritt 2: Wenn die Skriptaufgabe ausgeführt wird, Es trifft zuerst auf setTimeout, und setTimeout ist eine Makro-Aufgabenquelle. Anschließend besteht seine Rolle darin, die Aufgabe an die entsprechende Warteschlange zu verteilen.
setTimeout(function() { console.log('timeout1'); })
Schritt 3: Während der Skriptausführung wird eine Promise-Instanz festgestellt. Der erste Parameter im Promise-Konstruktor wird im neuen Zustand ausgeführt, sodass er nicht in eine andere Warteschlange eingegeben wird, sondern direkt für die aktuelle Aufgabe ausgeführt wird und anschließend .then wird es sein Verteilen Sie es an die Promise-Warteschlange von Mikroaufgabe.
Wenn der Konstruktor ausgeführt wird, werden die darin enthaltenen Parameter daher zur Ausführung in den Funktionsaufrufstapel eingegeben. for-Schleife gelangt in keine Warteschlange, daher wird der Code nacheinander ausgeführt, sodass Versprechen1 und Versprechen2 hier nacheinander ausgegeben werden.
und die Skriptaufgabe wird weiterhin ausgeführt. Am Ende wird nur ein Satz ausgegeben: global1 und dann Die globale Aufgabe ist abgeschlossen.
Schritt 4: Nachdem das erste Makrotask-Skript ausgeführt wurde, beginnen alle ausführbaren Mikrotasks. Zu diesem Zeitpunkt befindet sich nur eine Aufgabe in der Promise-Warteschlange, then1, sodass sie direkt ausgeführt werden kann. Das Ausführungsergebnis wird natürlich auch im Funktionsaufrufstapel ausgegeben.
Schritt 5: Wenn alle Mikrotasks ausgeführt sind, ist die erste Runde der Schleife beendet. Zu diesem Zeitpunkt muss die zweite Runde des Zyklus beginnen. Der zweite Zyklus beginnt immer noch mit der Makroaufgabe.
Zu diesem Zeitpunkt stellten wir fest, dass unter den Makroaufgaben nur eine Timeout1-Aufgabe darauf wartete in der setTimeout-Warteschlange ausgeführt werden. Also führen Sie es einfach direkt aus.
Zu diesem Zeitpunkt befinden sich keine Aufgaben in der Makroaufgabenwarteschlange und der Mikroaufgabenwarteschlange. Daher wird der Code nicht mehr ausgegeben.
Dann ist die Ausgabe des obigen Beispiels offensichtlich. Sie können es selbst ausprobieren und erleben.
这个例子比较简答,涉及到的队列任务并不多,因此读懂了它还不能全面的了解到事件循环机制的全貌。所以我下面弄了一个复制一点的例子,再给大家解析一番,相信读懂之后,事件循环这个问题,再面试中再次被问到就难不倒大家了。
// demo02 console.log('golb1'); setTimeout(function() { console.log('timeout1'); process.nextTick(function() { console.log('timeout1_nextTick'); }) new Promise(function(resolve) { console.log('timeout1_promise'); resolve(); }).then(function() { console.log('timeout1_then') }) }) setImmediate(function() { console.log('immediate1'); process.nextTick(function() { console.log('immediate1_nextTick'); }) new Promise(function(resolve) { console.log('immediate1_promise'); resolve(); }).then(function() { console.log('immediate1_then') }) }) process.nextTick(function() { console.log('glob1_nextTick'); }) new Promise(function(resolve) { console.log('glob1_promise'); resolve(); }).then(function() { console.log('glob1_then') }) setTimeout(function() { console.log('timeout2'); process.nextTick(function() { console.log('timeout2_nextTick'); }) new Promise(function(resolve) { console.log('timeout2_promise'); resolve(); }).then(function() { console.log('timeout2_then') }) }) process.nextTick(function() { console.log('glob2_nextTick'); }) new Promise(function(resolve) { console.log('glob2_promise'); resolve(); }).then(function() { console.log('glob2_then') }) setImmediate(function() { console.log('immediate2'); process.nextTick(function() { console.log('immediate2_nextTick'); }) new Promise(function(resolve) { console.log('immediate2_promise'); resolve(); }).then(function() { console.log('immediate2_then') }) })
这个例子看上去有点复杂,乱七八糟的代码一大堆,不过不用担心,我们一步一步来分析一下。
第一步:宏任务script首先执行。全局入栈。glob1输出。
第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。
setTimeout(function() { console.log('timeout1'); process.nextTick(function() { console.log('timeout1_nextTick'); }) new Promise(function(resolve) { console.log('timeout1_promise'); resolve(); }).then(function() { console.log('timeout1_then') }) })
第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。
setImmediate(function() { console.log('immediate1'); process.nextTick(function() { console.log('immediate1_nextTick'); }) new Promise(function(resolve) { console.log('immediate1_promise'); resolve(); }).then(function() { console.log('immediate1_then') }) })
第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。
process.nextTick(function() { console.log('glob1_nextTick'); })
第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。
new Promise(function(resolve) { console.log('glob1_promise'); resolve(); }).then(function() { console.log('glob1_then') })
第六步:执行遇到第二个setTimeout。
setTimeout(function() { console.log('timeout2'); process.nextTick(function() { console.log('timeout2_nextTick'); }) new Promise(function(resolve) { console.log('timeout2_promise'); resolve(); }).then(function() { console.log('timeout2_then') }) })
第七步:先后遇到nextTick与Promise
process.nextTick(function() { console.log('glob2_nextTick'); }) new Promise(function(resolve) { console.log('glob2_promise'); resolve(); }).then(function() { console.log('glob2_then') })
第八步:再次遇到setImmediate。
setImmediate(function() { console.log('immediate2'); process.nextTick(function() { console.log('immediate2_nextTick'); }) new Promise(function(resolve) { console.log('immediate2_promise'); resolve(); }).then(function() { console.log('immediate2_then') }) })
这个时候,script中的代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去。接下来,将会执行所有的微任务队列中的任务。
其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。
当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。
这个时候,script已经执行完毕,所以就从setTimeout队列开始执行。
setTimeout任务的执行,也依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去。
只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。
setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。
当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。
大家需要注意这里的循环结束的时间节点。
当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。
OK,到这里,事件循环我想我已经表述得很清楚了,能不能理解就看读者老爷们有没有耐心了。我估计很多人会理解不了循环结束的节点。
当然,这些顺序都是v8的一些实现。我们也可以根据上面的规则,来尝试实现一下事件循环的机制。
// 用数组模拟一个队列 var tasks = []; // 模拟一个事件分发器 var addFn1 = function(task) { tasks.push(task); } // 执行所有的任务 var flush = function() { tasks.map(function(task) { task(); }) } // 最后利用setTimeout/或者其他你认为合适的方式丢入事件循环中 setTimeout(function() { flush(); }) // 当然,也可以不用丢进事件循环,而是我们自己手动在适当的时机去执行对应的某一个方法 var dispatch = function(name) { tasks.map(function(item) { if(item.name == name) { item.handler(); } }) } // 当然,我们把任务丢进去的时候,多保存一个name即可。 // 这时候,task的格式就如下 demoTask = { name: 'demo', handler: function() {} } // 于是,一个订阅-通知的设计模式就这样轻松的被实现了
这样,我们就模拟了一个任务队列。我们还可以定义另外一个队列,利用上面的各种方式来规定他们的优先级。
因此,在老的浏览器没有支持Promise的时候,就可以利用setTimeout等方法,来模拟实现Promise,具体如何做到的,下一篇文章我们慢慢分析。
Das obige ist der detaillierte Inhalt vonFront-End Advanced (12): Detaillierte Erläuterung des Ereignisschleifenmechanismus. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!