Event Loop究竟是什麼鬼?

該文章是對Philip Roberts在JSConfEU演講的意譯和整理。若有誤導,請放棄閱讀。原文javascript

演講視頻

演講視頻html

演講前言

javascript程序員喜歡說各類高逼格的術語:「event-loop」,"non-blocking","callback","asynchronous","single-threaded"和「concurrency」等(譯者注:確實如此,一些所謂的面試官在本身對這些概念只知其一;不知其二的狀況下也喜歡問這些概念)。java

咱們老是裝出一成竹在胸的樣子說着這樣的話:「不要阻塞event loop」,"你要確保你的代碼以60FPS(frames-per-second)的速度去運行",「你這麼搞法確定是不行啦,那個函數是一個異步執行的callback啊」。node

若是你是像我同樣的慫貨,你確定會點點頭,表示十分認同的樣子。儘管你也不知道他說得對不對,由於你不知道這些術語究竟是啥意思。與此同時的事實是,找到一些講述javascript如何被解釋執行的好資料也是相對艱難的。(現在我花了18個月去作了這方面的研究),因此,咱們一塊兒學習一下吧。c++

在一些便利的可視化工具的幫助下,咱們能夠很直觀地理解到javascript的運行時到底發生了什麼。git

演講正文

你們好,感謝你們蒞臨side track(譯者注:side track是啥?)。it is awesomen to see it packed out in here。你們請允許我伸伸懶腰,這樣一來,我整我的看起來不至於那麼僵硬。我想跟你們談談event loop-event loop究竟是什麼鬼, 尤爲是javascript的event loop究竟是什麼鬼?程序員

首先,請允許我自我介紹一下。就像他(指主持人)說的那樣,我在AdnYet工做。AdnYet是美國的一個很不錯的軟件小廠。若是你們在實時軟件方面有需求的話,能夠聯繫咱們。咱們最擅長這方面的研發了。github

言歸正傳。18個月前,我是一個專業的在職javascript開發者。我經常自問:「javascript是如何運行的呢?」。每到這個時候,個人心裏都沒有一個很肯定的答案。我聽過v8團隊,也聽過v8做爲javascript在chrome的運行時而存在。除此以外,我就一無所知了。我徹底不知道v8具體意味着什麼(應該是指v8在瀏覽器內核的中分工角色是什麼),具體又是作什麼的。我聽過好比「single threaded」這些術語,我也知道我本身目前在用的就是叫「callback」。可是callback的運行原理是怎樣的呢?爲了對這些習覺得常的概念進行深究,我開始了個人探索旅程。探索過程當中,我一邊肯定探索主題,一邊查閱資料和在瀏覽器上作試驗。其過程用擬人手法能夠簡單描述爲以下的對話形式:web

  • 我:「javascript,你是誰?」。
  • javascript:「我?我是一門單線程,可併發的編程語言啊」。
  • 我:「哦,算你狠(做無語狀)」。
  • javascript:「好吧,我說得更具體點吧。我有一個call stack,一個event loop,一個callback queue和其它的一些API之類的東西」。
  • 我喃喃自語:「叼,我又不是學CS(computer sciense)的,鬼知道你說的這些術語是什麼。」

後來,我聽聞了v8,而且也知道,除了v8和chrome,外界還有其它各類javascript的運行時和瀏覽器。我就跑去問v8了。面試

  • 我:「v8,v8,你是否是有一個call stack,一個event loop,一個callback queue和其它的一些API之類的東西」。
  • v8:"我有一個call stack和一個heap,你說的其它那些東西我就不知道了。"
  • 我:「乜你甘得意噶」。

基本上就是這樣,18個月過去了。通過這18個月的上下求索,我如今終於搞清楚了。而這18個月研究所得出的乾貨,就是我今天要分享給大家的東西了。我但願這些乾貨可以幫助到那些javascript新手去理解「相比於其它編程語言,爲何javascript顯得那麼怪異呢?」,「爲何callback讓咱們又愛又恨,可是又不可或缺呢?」。若是你是經驗豐富的javascript開發者,但願此次分享能爲你提供一個視角去理解你當前所用的運行時是如何工做的。然後,你可以更好地編排你的代碼。

當咱們把Chrome中的javascript運行時平臺v8單獨拎出來看的話,咱們發現它是很好的javascript運行時學習對象。v8主要包含兩部分:heap和callstack。heap,是內存分配所發生的地方。call stack就是你的stack frame所在的地方。可是,若是你把v8的源碼下載下來,而後在裏面全局搜索一下「setTimeout」,「DOM」或者「HTTP request」等字眼,你會發現,它們根本就不在v8的源碼裏面。這是一路研究下來最讓我意外的發現。當咱們提到異步機制時第一反應所想到的東西居然不在那個你自覺得是的v8裏面。知道真相的你,不知道眼淚有沒有掉下來呢?

18個月的探索下來,我發現我所要研究的主題所涉及到的真的真的是一張很大的知識網。正由於知識網之巨大,之有價值,我才特別地但願你跟我一道學習學習。這樣一來,你就會明白各類圍繞v8零散的知識點是什麼?咱們已經有了一個v8。如今,給你介紹一個由瀏覽器提供實現的,叫「web API」的東西,好比DOM,AJAX和 time out(setTimeout和setInterval)之類的東西就是歸屬於「web API」這裏面的。同時,咱們還有神神祕祕的event loop和callback queue。我相信你以前也確定據說過這些術語,但也許你不太懂得這些術語是如何關聯起來,有機組成一個知識網的。我打算從最基礎,最多見的一些術語開始講。大家的一部分人可能已經了理解透,另一部分可能並無。我以爲大部分人應該是屬於後者的。若是你是前者,那麼忍受一下個人嘮叨吧。

again,javascript是一門single threaded的編程語言。這個single threaded的編程語言的runtime有一個single call stack。一個時間段中,call stack只能作一件事。一個時間段中,程序只能執行一片代碼片斷,這就是所謂的「single threaded」的意思。「single threaded」,「single call stack」和「do one thing at a time」這三者的關係以下:

single threaded === single call stack === do one thing at a time
複製代碼

如今,讓咱們開始嘗試經過可視化將咱們腦海裏面零散理解組織起來。若是咱們有大家左邊的代碼(演講者指向屏幕的ppt):

function multiply(a,b){
    return a * b;
}

function square(n) {
    return multiply(n,n);
}

function printSquare(n) {
    var squared = square(n);
    console.log(squared);
}

printSquare(4);
複製代碼

這裏面一個叫multiply的函數,負責將兩個數字相乘。一個叫square的函數,負責調用multiply來實現一個數的平方。而後有一個叫printSquare的函數負責將調用square的結果用console.log方法打印出來。最後,咱們在文件的底部開始調用print函數。

這些代碼應該沒寫錯吧?理解起來沒問題吧?嗯,那咱們開始把它跑起來。咱們上一個ppt。能夠這麼說,call stack基本上是一種用來告訴咱們當前是執行到程序的哪裏的數據結構。若是咱們執行到一個函數調用,那麼咱們就把一些東西放到棧頂,若是咱們從一個函數裏面return出去,那麼咱們就把棧頂的那些東西pop出來。這就是call stack能作的事情。當你運行這個文件的時候,我能夠把這個文件自己看成是整個程序的main函數。因此,一開始,咱們把main函數放進去。從文件的頂部開始看,接下來是一些函數聲明,they are just like defining the state of the world。最後,咱們來到了printSquare函數的調用。一提到函數調用,咱們得立刻把它push 到call stack的頂部。而在執行printSquare函數的過程當中,首先遇到了square函數調用,因此,咱們立刻又把square函數push到call stack的頂部。而在執行square過程當中,它又調用multiply函數。與此類推,咱們又把multiply函數push進去。如今call stack頂部的是multiply函數,那麼咱們就先執行它。它對A和B執行乘法操做後,就把結果return出去。一旦咱們遇到return語句,咱們就把call stack頂部的函數pop出來。因此,咱們把multiply函數pop走,如今程序返回到square函數,由於遇到了return語句,與此類推,咱們把square從call stack頂部pop走,回到了printSquare函數。最後,咱們調用console.log把結果打印出來。很明顯,這裏已經沒有return了,咱們到了函數的底部了。經過這種可視化的方式來說解,不知道大家明白了沒?(yes,Phil(譯者注:演講者本身用女生的聲明模仿觀衆說了這話))。即便你以前腦海裏面沒有call stack這個清晰的概念,可是若是你作過瀏覽器端的開發的話,那麼你已經遇到過它了。在舉個例子,若是咱們有一下代碼:

function Foo(){
    throw new Error('Oops');
}
function bar(){
    Foo()
}
function baz(){
    bar()
}

baz()
複製代碼

若是咱們把以上代碼在Chrome瀏覽上運行的話,咱們在console就會看到打印出來的stack trace。是的,stack trace就是執行出錯的時候call stack的當前狀態。

從上往下看,咱們依此看到:

unaugth error: Oops
Foo
bar
baz
anonymous
複製代碼

最後的那個anonymous function就是咱們的main函數。

一樣的,你也許聽過這樣的術語「blowing the stack」,那下面就是一個例子:

function Foo(){
    Foo()
}
複製代碼

在這個例子當中,main函數調用了Foo函數,而後在Foo函數又調用了Foo函數,與此類推。當咱們執行這段代碼的時候,chrome會說,遞歸調用Foo函數16000次可能不是你的本意,我如今報個錯,殺死掉進程,好讓你定位到bug的所在。

因此,雖然我可能會讓你看到了call stack的新的一面,可是其實在實際的開發中你多少對它已經有點印象了。

咱們講完了call stack。接下來的大主題就是blocking(阻塞)。咱們將會討論什麼是blocking和blocking的行爲表現是如何的。

實際上,對於blocking和非blocking這兩個概念並無十分嚴格的定義。在我看來,blocking就是指執行得十分緩慢的代碼而已。舉個例子,console.log不慢,可是若是執行一個1到100億的循環後去作console.log,那麼咱們能夠說這個console.log是很慢的。網絡請求是慢的,圖片請求也是慢的。當一個被push進call stack的函數執行起來很慢的話,咱們就能夠說這個函數是blocking的。 這就是咱們平時提到的blocking的實際含義。在這裏,咱們有一個用僞代碼寫成的小例子。

var foo = $.getSync('//foo.com');
 var bar = $.getSync('//bar.com');
 var qux = $.getSync('//qux.com');
 
 console.log(foo);
 console.log(bar);
 console.log(qux);
複製代碼

裏面有個jQuery AJAX請求風格的getSync方法。若是這些getSync方法執行起來都是同步的,那麼會發生什麼呢?咱們暫時先忘記異步callback其實本質也是同步的這個事實。按照咱們的寫法,執行起來的結果將會是這樣的:咱們調用了getSync方法,而後接下里就是等待。由於咱們正在發起了網絡請求,而網絡請求是於計算機硬件相關聯的,它們每每是很慢的。好在,咱們的第一個網絡請求終於完成了。咱們接着發起第二個網絡請求,而後咱們接下來仍是等待。第二個網絡請求完了,咱們立刻發起第三個......與此類推。只要其中的一個網絡請求永遠都不會完成的話,那麼我以爲我如今能夠回家洗洗睡了。最終,這三個網絡請求的blocking行爲完成了,call stack得以清空掉,是吧?因此,若是在一個single threaded的編程語言裏面,你是不能像ruby那樣使用線程的話,那麼,就會出現像這個例子所演示的情形那樣-咱們發起了一個網絡請求,在它完成以前,咱們只有乾等待。除此以外,咱們機關用盡啊。爲何咱們將這種結果看成一個問題而存在呢?一切的一切都是由於咱們的代碼跑在瀏覽器端。

下面我再舉一個例子進行說明。

這是chrome瀏覽器,而這是咱們上一個例子用過的代碼。咱們假設瀏覽器沒有爲咱們提供同步的AJAX請求(好吧,事實上是有提供的),那麼咱們就使用一個耗時5秒的大循環來模擬一下這種同步請求。接下里,咱們打開console,咱們將會看到發生了什麼。咱們看到,當咱們在請求foo.com的時候,咱們對界面作的什麼操做都沒有獲得響應。即便是我剛纔點擊過的按鈕仍是處於凹陷的狀態,還沒完成它的從新渲染。是的,瀏覽器被阻塞住了,它被卡得像便祕的屎同樣,一動不動。在這些網絡請求完成以前,瀏覽器作不了任何事情。瀏覽器雖然知道了我在界面進行了一些操做,可是它本身對於個人操做沒法做出響應,也便是說它沒法進行有效的界面渲染。這是由於,咱們的call stack到目前爲止仍是沒有處於清空狀態,因此瀏覽器沒法對界面進行重渲染,也執行不了其它代碼。界面被卡住了,咱們十分不開心,是吧?

做爲開發者,若是咱們想用戶看到一個好看的,流暢的界面的話,那麼咱們就不能阻塞call stack。咱們應該怎麼去解決這個問題呢?好吧,最簡單的解決方案就是異步callback方案。在瀏覽器中,幾乎沒有哪一個函數是blocking的,在nodejs裏面也是同樣的。全部會阻塞call stack的函數都會被改爲異步的。「將函數改爲異步」基本是是這樣的模式:咱們須要執行某些代碼,與此同時,咱們會提供一個callback給當前的運行時。當須要執行的代碼完成了,運行時就會執行咱們提供的callback。若是你見過javascript,那麼你也就見過了異步callback的樣子了。

下面是一個簡單的例子:

console.log('hi');

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

console.log('JSConfEU');
複製代碼

咱們用這個簡單的例子提醒一下你們咱們講到哪裏。咱們console.log "hi",而後執行setTimeout。可是這個setTimeout的執行會把console.log入隊到將來某個時刻去執行。咱們跳過這個log,轉而去log「JSConfEU」。5秒鐘以後,咱們會看到一個「there」的log,是吧?這個過程沒問題吧?不錯。基本上,咱們都知道這就是setTimeout要作的事情。顯然,異步callback是跟咱們以前見到的call stack有關的。那麼是如何相關法呢?咱們先把代碼跑起來。首先console.log('hi')入棧,而後是setTimeout。咱們知道這個setTimeout是不會立刻執行的,而是會在5秒鐘以後才執行。咱們不能把它推入到棧中。由於咱們目前尚未找到恰當的方式去描述它,因此就暫時假設它平白無故消失了。固然,咱們在後面會回來說它的。setTimeout平白無故消失後,咱們就將console.log('JSConfEU')入棧,而後log「JSConfEU」。最後,call stack被清空了。5秒鐘以後,console.log('there')神奇地出如今call stack上面了。這是怎麼回事呢?這時候該是event loop和concurrency出場的時候了。是的,我已經反反覆覆跟你說,javascript在一個時間段裏面只能作一件事件。打個比方,你不能在執行其它代碼的時候讓它(譯者注:它指的是javascript運行時)發起一個AJAX請求。你不能在執行其它代碼的時候執行setTimeout。而咱們之因此能以並行的方式去作事情,那是由於瀏覽器的能力遠在javascript運行時之上(譯者注:瀏覽器是javascript運行時的超集,javascript運行時以外還有別的東西)。記住下面這種圖:

確實,javascript運行時在一個時間段裏面只能作一件事,可是瀏覽器給了咱們其它的東西。它給了咱們一個叫webAPI的東西,這些都是一些有效的線程。你能夠調用它們,它們可以知道併發的發生,並承接住這些併發。若是你是後端開發人員。這張圖所闡述的機制跟nodejs差很少。在nodejs裏面,webAPI得換成了c++ API了。固然,還有一些被c++隱藏起來的線程。既然,咱們已經有了這張圖,咱們不妨以「瀏覽器」這個全局的視角來看看這段代碼是如何被執行的。

跟以前演示的那樣,執行console.log「hi」,把「hi」打印到控制檯,接下來,來看看當咱們調用setTimeout的時候到底發生了什麼。咱們把一個callback函數和須要延遲的毫秒數傳入到setTimeout調用中。如今,對於咱們來講,setTimeoout就是一個由瀏覽器提供給咱們的API。它的實現源碼並不在v8的源碼裏面。這是正在運行中的javascript運行時以外的東西。瀏覽器會另外給你設定一個倒計時。瀏覽器中的這部分代碼負責爲你倒數時間。這也就是說,setTimeout調用已經完成了,咱們二話不說就把它從call stack中pop出去。接着是log「JSConfEU」。咱們已經在webAPI中啓動了一個5秒中倒計時。

如今,webAPI不能直接修改你的代碼。它不能在本身準備好的時候直接將一些代碼塊推入到call stack中。若是它真的這麼幹的話,這些代碼塊就會隨機地插入到你當前代碼的中間,這豈不是亂套了。到了這裏,是時候讓task queue或者說callback queue(譯者注:以後統一採用「task queue」的叫法)參與進來了。任何一個web API一旦它完成了本身的任務後,就會把相關聯的callback推入到task queue中。最終的最終,咱們終於湊齊了講解了event loop,本次演講的題目:「what the heck is the event loop?」的全部要素了。

那到底什麼是event loop呢?event loop就好像是whole equation(整個方程?)中的一個最簡單的小片斷。它有一個簡單的任務。那就是同時監視call stack和task queue。若是call stack當前處於清空狀態的話,那麼event loop就會把task queue的排在第一個東西(callback)pop出來,推入到call stack中去運行。 迴歸到這個演示中來。咱們看到當前call stack是爲空的,並且有個一callback在task queue上。看到本身來活了,event loop立刻就運做起來的。它把這個callback推入到call stack中。時刻記住,call stack是javascript的地盤,它背靠v8這顆大樹。它的地盤它作主。因而乎,這個剛出現的callback立刻就被執行了-console.log('there')被執行了,在控制檯,「there」被打印出來。到這,call stack被清空了,咱們的代碼不折不扣被執行完了。你們理解了沒呢?everyone,where me?(譯者注:意思是你們都沒睡着吧,看到我嗎?)完美!

好的,咱們已經看到了event loop的基本工做流程了。在你跟異步編程打交道的過程當中,你遇到的衆多情景中的其中一個是這樣的:人家不跟你說明到底啥緣由,就是讓你用0毫秒去調用setTimeout。這時候你可能內心在想:「稍等,你讓我在0毫秒後執行這個函數。我爲何要把個人代碼包裹在一個0毫米的setTimeout裏面呢?這樣作意義何在呢?」。當你第一次遇到這種狀況的時候,應該跟我同樣,確定很困惑。咱們都知道這種寫法確定是作了什麼,可是不知道其中的具體緣由。這裏也不賣關子了,具體緣由就是人們想要把代碼延遲到call stack清空後才執行。那麼下面咱們來演示一下用0毫秒去調用setTimeout的狀況。若是你寫過javascript代碼,你就會知道跟上面的例子(非0毫秒去調用setTimeout)的結果是同樣的。 咱們將會看到依次打印「hi」和「JSConfEU」,「there」將會在最後打印。下面咱們看看具體的演示。在call stack以0毫秒去調用setTimeout,setTimeout立刻就會從call stack中pop走。接着它的callback會立刻被webAPIpush到task queue中。請記住我說過關於event loop的話。event loop必須等到當前call stack清空以後才能把task queue中的callback推入到call stack去的。因此,當前沒有清空的call stack會繼續執行。因而乎,咱們先看到打印"hi",而後看到打印「JSConfEU」,此時call stack已經清空了,是時候event loop參與進來的,最後調用了你的callback。無論基於何總緣由,咱們均可以把代碼的執行延遲到call stack最後一幀或者等到call stack清空後再執行。上面提到的以0毫秒去調用setTimeout只是這種作法的一個示例而已。全部的webAPI都是以相同的原理在工做。如今假設咱們要準備着callback向一個URL發起一個AJAX請求。這代碼的執行原理跟上面提到的setTimeout都是同樣的。oops,對不起。console打印「hi」,而後瀏覽器使用webAPI發起了AJAX請求。記住,真正實現發起AJAX請求的源代碼不在javascript的運行時中,而是在瀏覽器對webAPI的實現源碼裏面。因此,咱們保存着allback,把小菊花轉起來,默默等待。而後,繼續執行咱們在call stack中代碼。直到AJAX請求完成或者永遠都完成不了。如今我們假設這個AJAX請求完成了,那麼它對應的callback會立刻被推入到task queue中,由於此時call stack清空了,因此,它被event loop選中了,推入到call stack中跑起來了。javascript的異步調用背後所發生的事情大概就是這麼多了。

下面,讓咱們來跑一個瘋狂的,複雜的示例吧。我但願這段代碼可以跑起來。可能大家不知道吧,此次演講的全部PPT我都是在keynote上完成了。單單是當前這張幻燈片就有大概500個動畫步驟(代碼爆炸,化爲灰燼,觀衆大笑)。

譯者注:外國友人不多會把PPT稱爲「PPT」,正統的叫法應該「Slides(片子)」或是「Deck」,前者意指單張或幾張PPT頁,後者意指一整套PPT報告(就像a deck of cards)。

哇,真糸得意。我在這裏給出個連接。嗯....大夥們,我放得夠大嗎?你們能看見嗎?好的。基本此次演講思路和PPT都是沿用了今年初Scotlan JS上的東西。在那次演講以後,我損壞了大半部分的PPT。而後,本身又沒有耐心去重作那些PPT。由於用keynote作PPT真的是一件(坐到)屁股疼的事情。因此,我選擇了一條捷徑。我本身寫了一個工具用來可視化javascript的運行時機制。這個工具叫作loop。下面,咱們用這個新工具把這個例子跑起來吧。

console.log('Started');

$.on('button', 'click', function onClick(){
    console.log('Clicked');
});

setTimeout(function onTimeout(){
    console.log('Timeout finished');
}, 5000);

console.log('Done');
複製代碼

這個例子基本上跟前面的例子差很少,只不過我沒有使用shim過的XHR。使用shim過的XHR來演示徹底可行,只不過我這裏沒有這麼幹而已。正如你所看到的那樣,咱們的代碼是就是爲了log點東西出來。首先,$.on()是對addEventListener的一個封裝,而後是以5000毫秒調用setTimeout。最後,log一個「Done」。下面咱們真正把代碼跑起來,看看到底發生了什麼。

代碼開始後,打印個「Started」後,剛入棧的$.on()和setTimeout都會立刻被pop出去,而後加入到webAPI的地盤中去。call stack中的代碼繼續執行。5000毫秒到了,咱們就把callback推入到task queue中去。此時call stack處於清空狀態,因此callback會被推入到call stack去執行。最後打印出「Timeout finished」,代碼執行完畢。若是我點擊一下頁面的按鈕,那麼瀏覽器就會觸發webAPI的執行,把點擊事件的callback推入到task queue中去。call stack爲空,而後把callback推入到call stack中執行。若是咱們連續點擊100屢次,咱們能看到會發生什麼。在我點擊按鈕以後,click的callback不會被立刻執行,而是推入到task queue。當event loop開始安排task queue的時候,click的callback才能被處理到。是吧?我還有好幾個例子。咱們將經過這些例子來談談你跟異步API打交道過程可能遇到的可是沒有去深究的東西。我將會使用這個loupe的工具來進行演示。

首先第一個例子是以1秒的延遲調用setTimeout四次,每次都是在1秒後打印「hi」:

setTimeout(function onTimeout(){
    console.log('hi');
}, 1000);

setTimeout(function onTimeout(){
    console.log('hi');
}, 1000);

setTimeout(function onTimeout(){
    console.log('hi');
}, 1000);

setTimeout(function onTimeout(){
    console.log('hi');
}, 1000);
複製代碼

等到全部的callback被入隊到task queue之時,沒有任何一個callback被執行。此時,第四個callback尚未執行,時間已經超出了它所要求延遲的1秒了。是吧。這個例子說明的是javascript中的time out的實質含義。咱們傳入延遲時間(以毫秒爲單位)表明的正是可執行的最小時間,而不是確切的時間。瀏覽器並不能保證在你傳入的延遲時間內執行你的callback。以0毫秒調用setTimeout也是同樣的。你的callback不會立刻,當即被執行,而是被承諾爲儘快地執行。是吧。

下面這個例子,我想來談論一下callback。callback這個概念的定義取決於跟誰說和它們如何被解析的。callback這個概念能夠有兩種定義。第一種是,凡是被別的函數所調用的函數均可以稱之爲callback;第二種是,更加明確地指那種提供給異步操做的,最終會被推入到task queue的函數。下面這一點代碼會演示一下着二者的不一樣:

// 同步版本
[1,2,3,4].forEach(function(i){
    console.log(i);
});

// 異步版本
function asyncForEach(array, cb){
    array.forEach(function(i){
        setTimeout(cb.bind(null,i), 0);
    })
};

asyncForEach([1,2,3,4],function(i){
    console.log(i);
});

複製代碼

對於同步版本的代碼,咱們在數組上調用了forEach方法,forEach接受一個函數做爲參數,儘管這個函數不是被異步執行的,可是它是跑在當前的call stack中,你也能夠稱這個函數爲callback。我將會運行這段示例代碼,讓咱們看看這二者之間的差別究竟是什麼。首先,同步版本的代碼先運行了。整個代碼塊將會推入到call stack中,它就在這裏佔用並阻塞着call stack,是吧?直到當前call stack清空,才輪到異步版本的代碼執行。是的,執行速度降下來了。實際上,咱們將好幾個callback推入到了task queue中了。等到當前call stack清空了,咱們才能依次地從task queue中把callback彈出來,推入到call stack 中運行,最終打印出所遍歷的元素。在這個例子中,console.log()的執行足夠快,因此使用異步方式來遍歷數組的好處並無體現出來。假設,你如今在遍歷數組的過程當中對數組的元素作一些比較耗時的操做的時候,那麼,異步方式的寫法的好處就會體現出來。我好像有在哪裏寫過這種例子,噢,不,我應該記錯了,我沒有這樣的例子。好吧,咱們改造一下上面這個例子來進行說明:

function delay() {
// just do the slow thing    
}

// 同步版本
[1,2,3,4].forEach(function(i){
    console.log("Processing sync");
    delay();
});

// 異步版本
function asyncForEach(array, cb){
    array.forEach(function(i){
        setTimeout(cb.bind(null,i), 0);
    })
};

asyncForEach([1,2,3,4],function(i){
    console.log("Processing async");
    delay();
});
複製代碼

好的,如今我打算打開一個模擬瀏覽器repaint或者說render的開關。這個開關是我這個早上比較匆忙整合都這個loop工具的。目前,我沒有提到的一點是全部的這一切(call stack, webAPI,task queue和event loop)是如何界面渲染打交道的。好吧,我貌似提到過,可是沒有去深刻解釋它。能夠這麼說,瀏覽器的運行是受你當前正在執行的javascript代碼所約束的。理想狀態下,瀏覽器想要以1秒60幀的速度(也就是花16毫秒去完成一幀的渲染)去渲染屏幕的。60FPS是瀏覽器所能達到的刷新界面的最快的速度了。這裏再次強調,60FPS這種理想狀態是受正在執行的javascript代碼所約束的。若是當前call stack不爲空的話,實際上瀏覽器是沒法進行界面重繪的。 你能夠把界面渲染理解爲一個有callback與之對應的異步操做。這種render callback也是須要等待call stack清空以後才能執行的。render callback與普通的入隊到task queue的callback相比,不一樣之處在於,render callback比普通的callback的優先級要高。每隔16毫秒,瀏覽器就會往render queue上入隊一個render callback。這些render callback必須等到當前的call stack清空才能被執行。這裏所提到的「render queue」是由於要解釋界面渲染機制而模擬出來的。這就好像,每隔1秒,render queue就會問瀏覽器:「我能夠作界面渲染了嗎?」。瀏覽器回答:「是的,你能夠。」,而後又再下一秒進行如此類推的對話。

是的,由於當前咱們的代碼並無作一些阻塞call stack的事情。若是我把上面的這個同步版本的代碼跑起來的話,你能夠看到,當咱們經過同步執行的loop對數組進行很耗時的遍歷的時候,咱們的界面渲染工做是被阻塞掉的。一旦界面渲染工做被阻塞掉的話,那麼你就不能在界面上對文本進行選中,你也不能看到任何點擊以後的界面反應。是吧,這種狀況咱們在早先的那個例子又看到過。雖然,在同步版本中也會阻塞當前call stack。可是由於time out的時間爲0,因此阻塞的時間是至關的短的。而在此以後,在執行兩個個數組元素的操做的縫隙,咱們給了render queue一個時機去執行render callback。這是由於故意把對數組元素進行操做的callback編排爲異步callback。它們最終被入隊到task queue中去。鑑於render queue的優先級比task queue高,全部render callback能跟操做數組元素的callback交替進行,從而避免了界面較長時間裏面沒法獲得刷新。不知道你們理解不?雖然,上面引入render queue是爲了可視化地解釋界面渲染的工做原理。可是基本上它能正確地指出一個事實。那就是,當人們說「不要阻塞event loop」的時候,他們實際在說「不要一些耗時的代碼放在call stack上,以避免它們長期佔用call stack」。由於,一旦你這麼作了,call stack長時間內得不到清空,那麼瀏覽器也就無法完成一些它們須要急着作的事情了-好比說,建立一個流暢的界面瀏覽體驗。這也是爲何當你用javascript在作一些圖片處理工做或者動畫而又沒有好好地編排你的代碼的時候,界面上的不少東西都會變得很慢的緣由。

下面就是一個由於javascript代碼沒有獲得很好編排而致使界面變慢的例子。這是一個處理scroll事件的例子。

DOM中的scroll事件的觸發是很頻繁的。怎麼個頻繁法呢?我相信它們的觸發速度跟最佳界面刷新率60FPS是同樣的,也是16毫秒觸發一次。假如,我有如下代碼。我在document對象上對scroll事件作了監聽,同時監聽的callback主要負責作一些動畫或者其它一些耗時的活。當咱們滾動頁面的時候,那麼就會有大量的callback涌入到task queue中,是吧 ?

最後瀏覽器都必須處理一個個地處理這些callback。每一個callback執行起來都是挺慢的。這個時候,你不是阻塞call stack,你是直接用callback「淹沒」了task queue。這種狀況跟將觸發大量callback背後的狀況進行可視化很像。固然,經過debounce來減小callback的執行從而增長call stack的空閒時間,這種方式也是可行的。可是,我採用的另一種方式。我將這些event callback全數推入task queue,可是咱們每隔幾秒鐘或者等用戶中止滾動後的一小段時間後就作一些耗時的操做,這種方式也是可行的。我會有另一個專門來說述這個可視化loop工具的實現原理的演講。這實現原理大致是這樣的:當代碼運行在運行時的時候,我經過使用一個叫Esprima的javascript parser來執行這段代碼來下降代碼的執行速度。具體經過往裏面插入一段耗時半秒中的大while循環來達到的。把Esprima放在了web worker上,同時在上面完成了可視化這段代碼執行流程所須要作的全部事情。個人那個完整的演講將會深刻探究其中細節。對於此次演講的到來,我感到十分的興奮。到那個時候,我將會跟你們好好嘮嘮這事,屆時就功德圓滿了。好吧,就這樣。謝謝你們。

總結

  • 關於「single threaded」的理解
single threaded === single call stack === do one thing at a time
複製代碼
  • 關於「blocking」的理解: 對於blocking和非blocking這兩個概念並無十分嚴格的定義。在我看來,blocking就是指執行得十分緩慢的代碼而已。

  • 這種圖包含了幾個知識點:

    • 瀏覽器(內核)的能力是javascript運行時的超集。
    • webAPI通常用於處理異步操做
    • event loop把javascript運行時,webAPI和task queue統籌起來了,使之能協調工做。簡單而言,就是監聽call stack。一旦call stack爲空,就將task queue的第一callback推入其中執行。
  • javascript中的time out(setTimeout/setInterval)的實質含義是 「可執行的最小時間」,而不是「確切的時間」。

  • callback這個概念能夠有兩種定義。第一種是,凡是被別的函數所調用的函數均可以稱之爲callback;第二種是,更加明確地指那種提供給異步操做的,最終會被推入到task queue的函數。

  • 瀏覽器做爲一個界面終端而存在,這就決定了創造一個流暢的界面瀏覽體驗是其天生的職責之所在。創造一個流暢的界面瀏覽體驗具體是是指儘量地以60FPS的速率去刷新界面。而爲儘量地以60FPS的速率去刷新界面,那麼咱們就要儘量地不要長時間佔用call stack。爲了避免要長時間佔用call stack,那麼咱們必須學會編排咱們執行耗時較長的代碼。具體來講,就是把執行耗時較長的代碼改成異步代碼,push到task queue中。這樣子,雖然render callback跟普通的callback是交替執行,可是相比於同步方式,界面可以獲得儘快的刷新,故瀏覽體驗大勝一籌。

相關文章
相關標籤/搜索