Heim  >  Artikel  >  Web-Frontend  >  Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

不言
不言Original
2018-09-10 16:55:421371Durchsuche

Der Inhalt dieses Artikels befasst sich mit der Analyse asynchroner Einzelthreads in JS (Bilder und Texte). Ich hoffe, dass er für Sie hilfreich ist.

Für normale Entwickler (insbesondere solche mit Parallel-Computing/Multi-Threading-Hintergrundwissen) ist die asynchrone Verarbeitung von js wirklich seltsam. Infolgedessen wird diese Seltsamkeit durch die „Single-Thread“-Funktion von js verursacht.

Ich habe versucht, die Lehrbuchmethode „zuerst definieren und dann erweitern“ zu verwenden, um diesen Inhalt zu erklären, aber ich fand es äußerst schmerzhaft. Denn um die Details hinter dieser Sache zu klären, sie zu verallgemeinern und das Problem aus einer höheren Perspektive zu betrachten, bedarf es wirklich einer Menge Grundwissen. Wenn ich dieses Wissen klar erkläre und es zu Ende bringe, wird es so sein, als ob ich die Leser zwingen würde, mehrere Kapitel von Hypnosebüchern wie Betriebssysteme und Computernetzwerke zu lesen, was wirklich langweilig und langweilig ist.

Und was noch wichtiger ist: Wenn Sie an diesem Punkt angelangt sind, ist die Energie des Lesers erschöpft und er hat keine Energie mehr, sich um das ursprüngliche Problem zu kümmern – warum die asynchrone Verarbeitung von js seltsam ist.

Also beschloss ich, das Gegenteil zu tun, fangen wir wie ein Anfänger bei Null an,

Benutzen Sie zuerst die „falsche Idee“ , um unsere Diskussion zu beginnen, und dann Verwenden Sie den Code, um Bereiche zu finden, die gegen das Konzept verstoßen.

Nehmen Sie weitere Korrekturen vor, prüfen Sie einige Beispiele noch einmal, überlegen Sie, ob es etwas gibt, das nicht zufriedenstellend und klar ist, und nehmen Sie dann Anpassungen vor. Auf diese Weise werden wir wie ein Detektiv sein, der mit einer Hypothese beginnt, die nicht ganz richtig ist, ständig nach Beweisen sucht, die Hypothese ständig überarbeitet und Schritt für Schritt weiterverfolgt, bis wir zur endgültigen vollständigen Wahrheit gelangen.

Ich denke, diese Art des Schreibens entspricht eher dem wahren Wissenssuch- und Forschungsprozess einer Person und kann Ihnen mehr Inspiration zu „Erkundungsthemen“ bringen. Ich denke, dass diese Denkweise und Forschungskonzepte wichtiger sind als gewöhnliches Wissen. Es ermöglicht Ihnen, ein Jäger des Wissens zu werden, der in der Lage ist, selbstständig nach Nahrung zu suchen, anstatt gezwungen zu sein, ein Baby zu sein, das auf seine Fütterung wartet.

Okay, beginnen wir unsere Erkundungsreise mit einem Stück JS-Code.

console.log('No. 1');

setTimeout(function(){
    console.log('setTimeout callback');
}, 5000);

console.log('No. 2');

Das Ausgabeergebnis ist:

No. 1
No. 2
setTimeout callback

In diesem Code ist fast nichts Kompliziertes, er besteht ausschließlich aus Druckanweisungen. Das einzig Besondere ist die Funktion setTimeout. Laut groben Online-Informationen akzeptiert sie zwei Parameter:

  • Der erste Parameter ist die Callback-Funktion, die nach ihrer Ausführung zurückkehren soll . Die Funktion, die übertrieben aufgerufen werden soll.

  • Der andere ist der Zeitparameter, der verwendet wird, um anzugeben, wie viele Sekunden später die Rückruffunktion ausgeführt wird. Hier verwenden wir 5000 Mikrosekunden, also 5 Sekunden.

Ein weiterer wichtiger Punkt ist, dass setTimeout eine asynchrone Funktion ist, was bedeutet, dass mein Hauptprogramm nicht warten muss, bis setTimeout die Ausführung abgeschlossen hat, und seinen laufenden Prozess woanders ablegen muss. Ausführen, und dann läuft das Hauptprogramm weiter. Das heißt, das Hauptprogramm ist ein Schritt und setTimeout ist ein weiterer Schritt, was eine „asynchrone“ Methode zum Ausführen von Code darstellt.

Wenn Sie über Hintergrundkenntnisse im Parallelrechnen oder Multithread-Programmieren verfügen, wird Ihnen die obige Aussage sehr bekannt vorkommen. Wenn Sie sich in einer Umgebung mit mehreren Threads befinden, müssen Sie lediglich einen neuen Thread starten, um die Druckanweisung console.log('setTimeout callback') auszuführen. Dann geht der Hauptthread weiter nach unten, und der neue Thread ist dafür verantwortlich, die Anweisungen klar und deutlich zu drucken.

Zusammenfassend bedeutet dieser Code also, dass der Hauptthread, wenn er die Anweisung setTimeout ausführt, sie an „andere Orte“ weitergibt und diese „anderen Orte“ 5 Sekunden warten lässt, bevor sie ausgeführt wird . Der Hauptthread geht weiter nach unten, um den Ausdruck von „Nr. 2“ durchzuführen. Da andere Teile vor der Ausführung 5 Sekunden warten müssen und der Hauptthread beim Drucken von „Nr. 2“ sofort herunterfährt, besteht das endgültige Ausgabeergebnis darin, zuerst „Nr. 2“ und dann „setTimeout“ zu drucken Rückruf" ".

Na ja, soweit so gut. Alles sieht besser aus.

Was passiert, wenn wir einige Änderungen am oben genannten Verfahren vornehmen? Kann ich beispielsweise die Meldung „setTimeout callback“ zuerst ausdrucken lassen? Denn beim parallelen Rechnen besteht das Problem, auf das wir häufig stoßen, darin, dass wir die endgültige Ausführungsreihenfolge der Anweisung nicht bestimmen können, weil wir nicht wissen, wer unter mehreren Threads schnell und wer langsam ausführt. Hier lassen wir „setTimeout callback“ 5 Sekunden stehen. Die Zeit ist zu lang.

console.log('No. 1');

setTimeout(function(){
    console.log('setTimeout callback');
}, 1);

console.log('No. 2');

Wir haben den an setTimeout übergebenen Parameter auf 1 Millisekunde geändert. Nachdem Sie es mehrmals ausgeführt haben, werden Sie feststellen, dass sich das Ergebnis nicht geändert hat? ! Es scheint etwas ungewöhnlich zu sein. Auf 0 ändern?

console.log('No. 1');

setTimeout(function(){
    console.log('setTimeout callback');
}, 0);

console.log('No. 2');

Nachdem ich es viele Male ausgeführt habe, stellte ich fest, dass es immer noch nicht geändert werden kann. Das ist tatsächlich etwas seltsam. Denn bei der üblichen parallelen Berechnung und Multithread-Programmierung kann es bei mehreren Durchläufen tatsächlich zu verschiedenen unvorhersehbaren Ergebnissen kommen. Hier werden auf magische Weise die gleichen Ergebnisse der Ausführungssequenz erzielt. Das ist ungewöhnlich.

但我们还无法完全下一个肯定的结论,可不可能因为是setTimeout的启动时间太长,而导致“No. 2”这条语句先被执行呢?为了做进一步的验证,我们可以在“No. 2”这条打印语句之前,加上一个for循环,给setTimeout充分的时间去启动。

console.log('No. 1');

setTimeout(function(){
    console.log('setTimeout callback');
}, 0);

for (let i = 0; i <p>运行这段代码,我们发现,"No. 1"这条打印语句很快地显示到了浏览器命令行,等了一秒钟左右,接着输出了</p><pre class="brush:php;toolbar:false">No. 2
setTimeout callback

诶?!这不就更加奇怪了吗?!setTimeout不是等待0秒钟后立刻运行吗,就算启动再慢,也不至于等待一秒钟之后,还是无法正常显示吧?况且,在加入这个for循环之前,“setTimeout callback”这条输出不是立刻就显示了吗?

综合这些现象,我们有理由怀疑,似乎“setTimeout callback”一定是在“No. 2”后显示的,也即是:setTimeout的callback函数,一定是在console.log('No. 2')之后执行的。为了验证它,我们可以做一个危险一点的测试,将这个for循环,更改为无限while循环。

console.log('No. 1');

setTimeout(function(){
    console.log('setTimeout callback');
}, 0);

while {}  // dangerouse testing

console.log('No. 2');

如果setTimeout的callback函数是按照自己的步调做的运行,那么它就有可能在某个时刻打印出“setTimeout callback”。而如果真的是按照我们猜测的那样,“setTimeout callback”必须排在“No. 2”之后,那么浏览器命令行就永远不会出现“setTimeout callback”。

运行后发现,在浏览器近乎要临近崩溃、达到内存溢出的情形下,“setTimeout callback”依旧没有打印出来。这也就证明了我们的猜测!

这里,我们第一次出现了理念和现实的矛盾。按照通常并行计算的理念,被扔到“其它地方”的setTimeout callback函数,应该被同时运行。可事实却是,这个“其它地方”并没有和后一条打印“No. 2”的语句共同执行。这时候,我们就必须要回到基础,回到js这门语言底层的实现方式上去追查,以此来挖掘清楚这后面的猫腻。

js的特性之一是“单线程”,也即是从头到尾,js都在同一根线程下运行。或许这是一个值得调查深入的点。想来,如果是多线程,那么setTimeout也就该按照我们原有的理念做执行了,但事实却不是。而这两者的不同,便在于单线程和多线程上。

找到了这个不同点,我们就可以更深入去思考一些细节。细想起来,所谓“异步”,就是要开辟某个“别的地方”,让“别的地方”和你的主运行路线一起运行。可是,如果现在是单线程,也就意味着计算资源有且只有一份,请问,你如何做到“同时运行”呢?

这就好比是,如果你去某个办事大厅,去缴纳水费、电费、天然气。那么,我们可以粗略地将它们分为水费柜台、电费柜台、天然气柜台。那么,如果我们依次地“先在水费柜台办理业务,等到水费的明细打印完毕、缴纳完费用后;再跑去电费柜台打印明细、加纳费用;再跑去天然气柜台打印明细、加纳费用”,这就是一个同步过程,必须等待上一个步骤做完后,才能做下一步。

而异步呢,就是说我们不必在某个环节浪费时间瞎等待。比如,我们可以在“打印水费明细”的空闲时间,跑到电费和天然气柜台去办理业务,将“电费明细、天然气明细的打印”这两个任务提前启动起来。再回过头去缴纳水费、缴纳电费、缴纳天然气费用。其实,这就是华罗庚推广优选法的时候举的例子,烧水、倒茶叶、泡茶,如何安排他们的顺序为高效。

显然,异步地去做任务更高效。但这要有一个前提,就是你做任务的资源,也即是干活的人或者机器,得有多份才行。同样按照上面的例子来展开讨论,虽然有水费、电费、天然气这三个柜台,可如果这三个柜台背后的办事人员其实只有一个呢?比如你启动了办理水费的业务,然后想要在办理水费业务的等待期,去电费柜台办理电费业务。表面上,你去电费柜台下了申请单,请求办理电费业务,可却发现根本没有办事员去接收你的这个业务!为何?因为这有且只有一个的办事员,还正在办理你的水费业务啊!这时候,你的这个所谓的“异步”,有何意义?!

所以从这个角度来看,当计算资源只有一份的时候,你做“异步”其实是没什么意义的。因为干活的资源只有一份,就算在表面做了名义上的“异步”,可最终就像上面的多柜台单一办事员那样,到了执行任务层面,还是会一个接一个地完成任务,这就没有意义了。

Die Eigenschaften von js sind also „Single-Threaded“ + „asynchron“. Ist das nicht genau die „bedeutungslose“ Situation, die wir diskutieren? ! Warum muss man dann auf einen Schlag etwas Sinnloses tun?

Nun... es wird langsam interessant.

Wenn bei einem Ereignis etwas Magisches oder Seltsames auftritt, liegt das im Allgemeinen daran, dass wir ein bestimmtes Detail übersehen haben oder ein Missverständnis oder Missverständnis bezüglich eines bestimmten Details haben. Um das Problem zu lösen, müssen wir die vorhandenen Materialien ständig überprüfen und durch wiederholte Inspektionen können wir die wenigen Tricks entdecken, die wir übersehen haben.

Lassen Sie uns das Werbevideo über js-Asynchronität ansehen. Um die Notwendigkeit der Asynchronität von js zu veranschaulichen, wird normalerweise der Widerspruch zwischen dem Laden von Browserressourcen und dem Rendern von Seiten angeführt.

Rendering kann grob als der Prozess des Zeichnens eines „Bildes“ verstanden werden. Wenn der Browser beispielsweise die Schaltflächen und Bilder auf der Seite anzeigen möchte, muss er über eine Aktion verfügen, um das „Bild“ auf der Webseite zu zeichnen. Oder wenn das Betriebssystem die grafische Oberfläche „Desktop“ auf dem Monitor anzeigen möchte, muss es das entsprechende „Bild“ auf dem Monitor zeichnen. Zusammenfassend wird dieser Vorgang des „Auszeichnens“ als „Rendering“ bezeichnet.

Wenn Sie beispielsweise auf eine Schaltfläche auf der Seite klicken, ruft der Browser die Back-End-Datenbank auf, um den Datenbericht abzurufen und die Zahlen auf der Webseite anzuzeigen. Und wenn js die Asynchronität nicht unterstützt, bleibt die gesamte Webseite hängen, das heißt, sie bleibt hängen. Der Programmablauf kann nicht fortgesetzt werden, bis das Backend die Daten an das Frontend zurückgibt.

Hier besteht die „Asynchronität“ von js tatsächlich darin, dem Browser zu ermöglichen, die Aufgabe „Laden“ „anderen Orten“ zuzuweisen, sodass der „Ladevorgang“ und der „Rendering-Prozess“ synchron ablaufen können.

Moment, ist es wieder dieser „andere Ort“? ! !

Oh mein Gott, heißt es nicht, dass js Single-Threaded ist? Wie kann es „gleichzeitig geladen und gerendert“ werden? ! WTF, machst du Witze? !

Scheiße, welcher Satz hier ist wahr? ! Stimmt es, dass js Single-Threaded ist? Oder stimmt es, dass der Browser gleichzeitig „laden und rendern“ kann? !

Wie können wir diesen Zweifel lösen? ! Natürlich müssen wir tief in das Innere des Browsers vordringen, um zu sehen, wie er gestaltet ist.

In Suchmaschinen ist es für uns nicht schwierig, durch einige Suchanfragen zu Browsern und JS einige grundlegende Informationen zu erhalten. JS ist nicht alles im Browser. Der Browser muss sich um zu viele Dinge kümmern. Für JS ist nur eine Komponente des Browsers verantwortlich, die sogenannte JS-Engine. Die bekannteste und in Chrome verwendete ist die berühmte V8-Engine, die für das Parsen und Ausführen von js verantwortlich ist.

Andererseits wissen wir auch, dass ein wichtiger Grund für die Verwendung von js darin besteht, dass es DOM-Elemente frei manipulieren, asynchrone Ajax-Anforderungen ausführen und setTimeoutasynchrone Aufgabenverteilung durchführen kann. Dies sind alles hervorragende Funktionen von js.

Aber hier kommt das Überraschende:setTimeout

Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

Das obige Bild stammt von Alexander Zlatkov, seine Struktur ist:

  1. JS-Engine

  • Speicherheap

  • Aufrufstapel

  • Web-APIs

    • DOM (Dokument)

    • Ajax (XMLHttpRequest)

    • Timeout (setTimeout)

  • Rückrufwarteschlange

  • Ereignisschleife

  • Es ist offensichtlich eine Funktion von js, warum werden diese Funktionen nicht verwaltet? die js-Engine? Na ja, interessant~~~

    Hey! Ist es nicht „Single-Threaded“? Wird der Ladevorgang nicht woanders abgebrochen? ! js ist Single-Threaded, das heißt, js ist Single-Threaded in der js-Engine und kann nur einen Anteil an Rechenressourcen zuweisen. Ist die Ajax-Funktion zum Laden von Daten jedoch nicht in der js-Engine platziert? !

    诶~等等,让我们再仔细看看上面这张图呢?!Ajax不在js引擎里,可是setTimeout也不在js引擎里面啊!!如果Web APIs这部分是在不同于js引擎的另外一根线程里,它们不就可以实现真正意义上的并行吗?!那为何我们开头的打印信息“setTimeout callback”,无法按照并行的方式,优先于“No. 2”打印出来呢?

    嗯......真是interesting......事情果然没有那么简单。

    显然,我们需要考察更多的细节,特别是,每一条语句在上图中,是按照什么顺序被移动、被执行的。

    谈到语句的执行顺序,我们需要再一次将关注点放回到js引擎上。再次回看上面这幅结构图,JS引擎包含了两部分:一个是 memory heap,另一个是call stack。前者关于内存分配,我们可以暂时放下。后面即是函数栈,嗯,它就是要进一步理解执行顺序的东西。

    函数栈(call stack)为什么要叫做“栈(stack)”呢?为什么不是叫做函数队列或者别的神马?这其实可以从函数的执行顺序上做一个推断。

    函数最开始被引进,其实就是为了代码复用和模块化。我们期望一段本该出现的代码,被单独提出来,然后只需要用一个函数调用,就可以将这段代码的执行内容给插入进来。

    所以,如果当我们执行一段代码时,如果遇到了函数调用,我们会期望先去将函数里面的内容执行了,再跳出来回到主程序流,继续往下执行。

    所以,如果把一个函数看作函数节点的话,整个执行流程其实是关于函数节点的“深度优先”遍历,也即是从主函数开始运行的函数调用,整个呈深度优先遍历的方式做调用。而结合算法和数据结构的知识,我们知道,要实现“深度遍历”,要么使用递归、要么使用stack这种数据结构。而后者,无疑更为经济使用。

    所以咯,既然期望函数调用呈深度优先遍历,而深度优先遍历又需要stack这种数据结构做支持,所以维护这个函数调用的结构就当然呈现为stack的形式。所以叫做函数栈(stack)。

    当然,如果再发散思考一下,操作系统的底层涌来维护函数调用的部分也叫做函数栈。那为何不用递归的方式来实现维护呢?其实很简单,计算机这么个啥都不懂的东西,如何知道递归和返回?它只不过会一往无前的一直执行命令而已。所以,在没有任何辅助结构的情况下,能够一往无前地执行的方式,只能是stack,而不是更为复杂的递归概念的实现。

    另一方面,回到我们最开头的问题,矛盾其实是出现在setTimeout的callback函数上。而上面的结构图里,还有一部分叫做“callback queue”。显然,这一部分也是我们需要了解的东西。

    结合js的call stack和callback queue这两个关键词,我们不难搜索到一些资料,来展开讨论这两部分是如何同具体的语句执行相结合的。

    先在整体上论述一下这个过程:

    • 正常的语句执行,会一条接一条地压入call stack,执行,再根据执行的内容继续压入stack。

    • 而如果遇到有Web APIs相关的语句,则会将相应的执行内容扔到Web APIs那边。

    • Web APIs这边,可以独立于js引擎,并行地分配给它的语句,如Ajax数据加载、setTimeout的内容。

    • Web APIs这边的callback function,会在在执行完相关语句后,被扔进“callback queue”。

    • Event loop会不断地监测“call stack”和“callback queue”。当“call stack”为空的时候,event loop会将“callback queue”里的语句压入到stack中,继续做执行。

    • 如此循环往复。

    以上内容比较抽象,让我们用一个具体的例子来说明。这个例子同样来自于Alexander Zlatkov。使用它的原因很简单,因为Zlatkov在blog中使用的说明图,实在是相当清晰明了。而目前我没有多余的时间去使用PS绘制相应的结构图,就直接拿来当作例子说明了。

    让我们考察下面的代码片段:

    console.log('Hi');
    setTimeout(function cb1() { 
        console.log('cb1');
    }, 5000);
    console.log('Bye');

    哈哈,其实和我们使用的代码差不多,只是打印的内容不同。此时,在运行之前,整个底层的结构是这样的:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    然后,让我们执行第一条语句console.log('Hi'),也即是将它压入到call stack中:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Dann führt die js-Engine die oberste Anweisung im Stapel aus. Entsprechend gibt die Browserkonsole die Meldung „Hallo“ aus:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Da diese Anweisung ausgeführt wird, beginnt sie auch mit Verschwindet vom Stapel:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Drücken Sie dann die zweite Aussage setTimeout:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    AusführensetTimeout(function cb1() { console.log('cb1'); }, 5000);:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Beachten Sie, dass der Teil setTimout nicht in der js-Engine enthalten ist, sondern einfach direkt in die geworfen wird Timeout Teil von Web-APIs. Hier besteht die Rolle des blauen Teils im Stapel darin, den entsprechenden Inhalt „Timer, Wartezeit von 5 Sekunden, Rückruffunktion cb1“ an Web-APIs zu übergeben. Dann kann diese Aussage vom Stapel verschwinden:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Weiter zur nächsten Aussage schieben console.log('Bye'):

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Beachten Sie, dass im Web-APIs-Teil die entsprechenden Anweisungen parallel zur js-Engine ausgeführt werden, das heißt: 5 Sekunden warten. Okay, der Timer wartet weiter und es gibt bereits eine Anweisung auf dem Stapel, daher muss sie ausgeführt werden:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Entsprechende Browser-Kontrollstation , wird die Meldung „Bye“ angezeigt. Die ausgeführten Anweisungen im Stapel sollten verschwinden:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Zu diesem Zeitpunkt ist der Stapel leer. Die Ereignisschleife erkennt, dass der Stapel leer ist, und möchte natürlich die Anweisungen in der Rückrufwarteschlange in den Stapel verschieben. Zu diesem Zeitpunkt ist jedoch auch die Rückrufwarteschlange leer, sodass die Ereignisschleife die Schleifenerkennung fortsetzen muss.

    Der Timer auf der Seite der Web-APIs startet seine Ausführung hingegen nach 5 Sekunden parallel – ohne etwas zu tun. Fügen Sie dann die entsprechende Rückruffunktion cb1() in die Rückrufwarteschlange ein:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Die Ereignisschleife läuft immer zur Erkennung. Zu diesem Zeitpunkt Sehen Sie etwas in der Rückrufwarteschlange, nehmen Sie es schnell aus der Rückrufwarteschlange und schieben Sie es in den Stapel:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Now Stack Wenn etwas drin ist Um es zu verwenden, müssen Sie die Rückruffunktion cb1() ausführen. Die Anweisung cb1() wird in    console.log('cb1') aufgerufen und muss daher in den Stapel verschoben werden:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Stack wird weiterhin ausgeführt, und zwar jetzt it Die oberste Ebene ist    console.log('cb1') und muss daher zuerst ausgeführt werden. Das Browser-Steuerelement gibt also die entsprechenden Informationen „cb1“ aus:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Löst die ausgeführte    console.log('cb1')-Anweisung vom Stapel:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    Fahren Sie mit der Ausführung der cb1() verbleibenden Anweisungen fort. Zu diesem Zeitpunkt hat cb1() keine anderen Anweisungen, die ausgeführt werden müssen, das heißt, es wurde ausgeführt, also nehmen Sie es aus dem Stapel:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    整个过程结束!如果从头到尾看一遍的话,就是下面这个gif图了:

    Analyse eines asynchronen Einzelthreads in Javascript (Bild und Text)

    相当清晰直观,对吧!

    如果你想进一步地把玩js的语句和call stack、callback queue的关系,推荐Philip Roberts的一个GitHub的开源项目:Loupe,里面有他online版本供你做多种尝试。

    有了这些知识,现在我们回过头去看开头的那段让人产生疑惑的代码:

    console.log('No. 1');
    
    setTimeout(function(){
        console.log('setTimeout callback');
    }, 0);
    
    console.log('No. 2');

    按照上面的js处理语句的顺序,第一条语句console.log('No. 1')会被压入stack中,然后被执行的是setTimout

    根据我们上面的知识,它会被立刻扔进Web APIs中。可是,由于这个时候我们给它的等待时间是0,所以,它的callback函数console.log('setTimeout callback')会立刻被扔进“Callback Queue”里面。所以,那个传说中的“其它地方”指的就是callback queue。

    那么,我们能够期望这一条console.log('setTimeout callback')先于“No. 2”被打印出来吗?

    其实是不可能的!为什么?因为要让它被执行,首先它需要被压入到call stack中。可是,此时call stack还没有将程序的主分支上的语句执行完毕,即还有console.log('No. 2')这条语句。所以,event loop在stack还未为空的情况下,是不可能把callback queue的语句压入stack的。所以,最后一条“setTimeout callback”的信息,一定是会排在“No. 2”这条信息后面被打印出来的!

    这完全符合我们之前加入无限while循环的结果。因为主分支一直被while循环占有,所以stack就一直不为空,进而,callback queue里的打印“setTimeout callback”的语句就更不可能被压入stack中被执行。

    探索到这里,似乎该解决的问题也都解决了,好像就可以万事大吉,直接封笔走人了。可事实却是,这才是我们真正的泛化讨论的开始!

    做研究和探索,如果停留于此,就无异于小时候自己交作业给老师,目的仅仅是完成老师布置的任务。在这里,这个老师布置的任务就是文章开头所提出的让人疑惑的代码。可是,解决这段代码并不是我们的终极目的。我们需要泛化我们的所学和所知,从更深层次的角度去探索,为什么我们会疑惑,为什么一开始无法发现这些潜藏在表面之下不同。我们要继续去挖掘,我们到底在哪些最根本的问题上出现了误解和错误认识,从而导致我们一路如此辛苦,无法在开头看到事情的真相。

    回顾我们的历程,一开始让我们载跟斗的,其实就是对“异步”和“多线程”的固定假设。多线程了,就是异步,而异步了,一定是多线程吗?我们潜意识里是很想做肯定回答的。这是因为如果异步了,但却是单线程,整个异步就没有意义了(回忆那个多柜台、单一办事员的例子)。可js却巧妙地运用了:使用异步单线程去分配任务,而让真正做数据加载的Ajax、或者时间等待的setTimeout的工作,扔给浏览器的其它线程去做。所以,本质上js虽然是单线程的,可在做实际工作的时候,却利用了浏览器自身的多线程。这就好比是,虽然是多柜台、单一办事员,可办事员将缴纳电费、水费的任务,外包给其它公司去做,这样,虽然自己仍然是一个办事员,但却由于有了外包服务的支持,依旧可以一起并行来做。

    另一方面,js的异步、单线程的特性,逼迫我们去把并行计算中的“同步/异步、阻塞/非阻塞”等概念理得更清楚。

    “同步”的英文是synchronize,但在中文的语境下,却很容易和“同时”挂钩。于是,在潜意识里有可能会有这样一种联想,“同步”就是“同时”,所以,一个同步(synchronize)的任务就被理解为“可以一边做A,一边做B”。而这个潜意识的印象,其实完全是错误的(一般做A一边做B,其实是“异步”+“并行”的情况)。

    但在各类百科词典上,确实有用“同时”来作为对“同步”的解释。这是为什么呢?其实这是对”同步“用作”同时“的一个混淆理解。如果仔细考虑”同时“的意思,细分起来,其实是有两种理解:

    • Gleichzeitig, zum Beispiel um 9:00 Uhr, machen wir sowohl A als auch B.

    • Das andere ist das gleiche Zeitreferenzsystem, das heißt, die sogenannte Uhr an der Wand ist dieselbe.

    Ersteres ist leicht zu verstehen, hier werde ich mich auf die Erklärung des Letzteren konzentrieren. Ich habe zum Beispiel einen WeChat-Voice-Chat mit einem Klassenkameraden in den USA auf dem chinesischen Festland eröffnet. Es war 22:00 Uhr auf meiner Seite und 9:00 Uhr auf seiner Seite. Wenn wir chatten, sind wir zur gleichen Zeit, aber nicht im gleichen Zeitbezugssystem (Uhr an der Wand). Bei der bei Computern diskutierten Synchronisierung geht es tatsächlich um die Diskussion des „gleichen Referenzsystems“ des Computers. Synchronisierung bedeutet, unsere Referenzsysteme zusammenzuführen und unter dasselbe System zu stellen.

    Als weiteres Beispiel fällt es uns im Leben leicht zu sagen: Synchronisieren Sie Ihren Computer, synchronisieren Sie Ihr Mobiltelefon-Adressbuch, synchronisieren Sie Ihr Fotoalbum. Was meinen Sie? Dies dient dazu, die Inhalte Ihrer verschiedenen Clients (PC, Mobiltelefon und Server) konsistent zu halten, d. h. alle sind in einem konsistenten Referenzsystem untergebracht. Sagen Sie nicht, dass Sie Foto A auf Ihrem PC haben, aber Sie haben nicht Foto A, sondern Foto B auf Ihrem Telefon. Zu diesem Zeitpunkt sprechen Personen über die Informationen auf dem PC und Personen, die über die Informationen auf dem PC sprechen Telefon sprechen über das Gleiche. Der Grund dafür ist, dass nicht jeder im gleichen Bezugsrahmen steht.

    Synchronisieren bezieht sich also auf „zur gleichen Zeit“, wenn alle die Uhren an der Wand so einstellen, dass sie konsistent und im gleichen Tempo sind, was bedeutet, dass es sich um die gleiche Zeit und das gleiche Zeitreferenzsystem handelt. Anstatt dass Dinge gleichzeitig im selben Moment passieren. Was ist natürlich asynchron? Asynchronität bedeutet, dass das Zeitreferenzsystem bei jedem anders ist als bei Ihnen Tempo auf dem Frequenzband.

    Tatsächlich stellt jede unabhängige Person und jede unabhängige Computerressource ihr eigenes Referenzsystem dar. Solange Sie Aufgaben an andere Personen oder andere Computerressourcen verteilen, werden zwei Referenzsysteme angezeigt: eines ist das Referenzsystem des ursprünglichen Hauptzweigs und das andere ist das Referenzsystem der neuen Computerressourcen. Beim Parallelrechnen gibt es einen Synchronisationsmechanismus, der mithilfe einer Anweisungsbarriere allen Berechnungszweigen ermöglicht, ihre Berechnungen an diesem Standortknoten abzuschließen. Warum soll es sich um einen Synchronisationsmechanismus handeln? Nach unserem Verständnis des einheitlichen Referenzsystems soll sichergestellt werden, dass alle anderen Berechnungszweige die Berechnung abschließen, wodurch auch sichergestellt wird, dass andere Zweige verschwinden und nur der Hauptzweig als Referenzsystem übrig bleibt. So kann jeder über die gleichen Dinge sprechen und die gleichen Worte sagen, ohne dass es zu Missverständnissen kommt.

    Wenn Sie andererseits das Design von js besser verstehen möchten, müssen Sie meiner Meinung nach zu den Anfängen der Computergeschichte zurückkehren, beispielsweise zur Ära der Single-Core-Time-Sharing-Systeme . Zu dieser Zeit waren die Hardwareeinschränkungen des Betriebssystems nicht geringer als die Einschränkungen der JS-Engine im Browser. Wie würde das vorherige Betriebssystem unter denselben Einschränkungen die extrem begrenzten Rechenressourcen geschickt nutzen, um dem gesamten Betriebssystem die Illusion von Glätte, Reibungslosigkeit und leistungsstarken Funktionen zu verleihen? Ich denke, dass diese Designs von js eng mit dem frühen Design des Betriebssystems zusammenhängen müssen. Auf dieser Ebene geht es also wieder zurück zu den Grundlagen wie den Betriebssystemen. Ob Sie die moderne Technologie vollständig verstehen können, hängt tatsächlich zu einem großen Teil davon ab, ob Sie die Geschichte des Designs gründlich verstehen und verstehen, wie Meister aller Gesellschaftsschichten in diesen Epochen geschickt Straßen über Berge öffneten und Brücken über Flüsse bauten der Ressourcenverknappung. Unabhängig davon, wie reichlich moderne Computerhardwareressourcen vorhanden sind, werden sie aufgrund der primären und sekundären Beziehungen zwischen Zielen und Unternehmen definitiv begrenzt sein. Wie man innerhalb von Grenzen tanzt und kreiert, ist ein häufiges Problem, das sich im Laufe der Geschichte verfolgen lässt.

    Verwandte Empfehlungen:

    Detaillierte Erläuterung der asynchronen Programmiertechnologie von JavaScript

    Das obige ist der detaillierte Inhalt vonAnalyse eines asynchronen Einzelthreads in Javascript (Bild und Text). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

    Stellungnahme:
    Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn