做者:Vlad Ostrenko
譯者:前端小智
來源:mediuum
點贊再看,養成習慣本文
GitHub
https://github.com/qq44924588... 上已經收錄,更多往期高贊文章的分類,也整理了不少個人文檔,和教程資料。歡迎Star和完善,你們面試能夠參照考點複習,但願咱們一塊兒有點東西。前端
Javascript 是一種奇怪語言,有些人喜歡它,有些人討厭它。它有許多獨特的機制,這些機制在其餘流行語言中不存在,也沒有對應的機制,還有突出明顯的就是代碼的執行順序c++
瞭解瀏覽器環境,它的組成以及它的工做原理會讓咱們在編寫 JS 時更加自信,併爲可能發生的潛在問題作好了充分的準備。git
在這篇文章中,咱們試着解釋一下Chrome瀏覽器下到底發生了什麼,來一塊兒看看:github
最流行的JavaScript引擎是V8,它是用c++編寫的,並被基於Chrome的瀏覽器使用,如Chrome、Opera甚至Edge。基本上,這個引擎是一個將 JS 轉換成機器碼並在計算機的中央處理器(CPU)上執行結果的程序。面試
當瀏覽器加載 JS 文件時,V8的解析器將其轉換爲一個抽象語法樹(AST)。該樹用於生成字節碼的解釋器。字節碼是一種能夠經過編譯成非優化的機器碼來執行的機器碼的抽象。V8在主線程中執行它,而優化編譯器TurboFan在另外一個線程中進行一些優化並生成優化的機器碼。算法
這個管道稱爲即時(JIT)編譯。編程
JavaScript 是一種單線程編程語言,只有一個調用堆棧。它意味着咱們的代碼是同步執行的。每當一個函數運行時,它將在任何其餘代碼運行以前徹底運行。api
當V8調用 JS 函數時,它必須將運行時數據存儲在某個地方。調用堆棧是內存中由堆棧幀組成的位置。每一個堆棧幀對應於一個還沒有被調用函數。堆棧結構由如下組成:瀏覽器
若是咱們執行一個函數,V8 會將幀推到棧頂。當咱們從一個函數返回時,V8 會跳出幀。微信
如上例所示,在每次函數調用時都會建立一個幀,並在每一個return
語句中將其刪除。
其餘全部內容都動態地分配到一個稱爲堆的大型非結構化內存塊中。
有時V8在編譯時不知道對象變量須要多少內存。 此類數據的全部內存分配都發生在堆中。 退出分配內存的函數後,堆上的對象繼續存在。
V8有一個內置的垃圾收集器(GC)。垃圾收集是內存管理的一種形式。它就像一個收集器,試圖釋放再也不使用的對象佔用的內存。換句話說,當一個變量失去全部引用時,GC將該內存標記爲不可訪問並釋放它。
咱們能夠經過在Chrome開發工具中建立快照來研究堆。
實例化的每一個 JS 對象都分組在其構造函數類下。括號中的分組表示不能直接調用的原生構造函數。能夠看到有不少(編譯代碼)和(系統)實例,但也有一些傳統的 JS 對象,如Math
、String
、Array
等。
V8能夠根據標準,同步地使用一個調用堆棧來執行 JS 。但,咱們須要渲染UI,須要處理用戶與UI的交互。此外,咱們還須要在發出網絡請求時處理用戶交互,對此卻無能爲力。當全部代碼都是同步的時候,咱們如何實現併發呢? 這還得感謝瀏覽器引擎。
瀏覽器引擎負責用 HTML 和 CSS 渲染頁面。在 Chrome 中它被稱爲Blink。它是WebCore的一個分支,Blink 是一個佈局、渲染和文檔對象模型(DOM)庫。Blink 是用 c++ 中實現的,它提供了DOM元素和事件、XMLHttpRequest
、fetch
、setTimeout
、setInterval
等 Web api,這些api能夠經過 JS 訪問。
咱們一塊兒思考下面帶有setTimeout(onTimeout, 0)
的示例:
能夠看到,瀏覽器首先將f1()
和f2()
函數推入堆棧,而後執行onTimeout
。那麼上面的示例如何工做?
setTimeout
函數執行後,瀏覽器引擎當即將setTimeout
的回調函數放入一個事件表中。它是一個數據結構,將註冊的回調映射到事件,在咱們的例子中是onTimeout
函數映射到timeout
事件。
一旦計時器到時,在本例中,咱們將延遲設爲0 ms
,則當即觸發事件,並將onTimeout
函數放入事件隊列(又名回調隊列,消息隊列或任務隊列)中。 事件隊列是一種數據結構,由未來要處理的回調函數(任務)組成。
最後且重要的是,事件循環(一個不斷運行的循環)檢查調用堆棧是否爲空。若是是,則執行從事件隊列中添加的第一個回調,從而移動到調用堆棧。
函數的處理將繼續,直到調用堆棧再次爲空。而後,事件循環將處理事件隊列中的下一個回調(若是有的話)。
const fn1 = () => console.log('fn1') const fn2 = () => console.log('fn2') const callback = () => console.log('timeout') fn1() setTimeout(callback, 1000) fn2() // 運行結果: // fn1 // fn2 // timeout
注意onResolve1
、onResolve2
和onTimeout
回調的執行順序。
簡單地說,全部 JS 代碼都被認爲是阻塞的。當 V8 忙於處理堆棧幀時,瀏覽器被卡住了,應用程序的 UI 被阻塞。用戶將沒法單擊、導航或滾動。直到 V8 完成它的工做,纔會處理來自網絡請求的響應。
想象一下,咱們若是在瀏覽器中運行的程序中解析圖像。
const fn1 = () => console.log('fn1') const onResolve = () => console.log('resolved') const parseImage = () => { /* 這裏會長時間運行解析算法 */ } fn1() Promise.resolve().then(onResolve) // 或任何其餘Web API異步函數 parseImage()
在上面的示例中,事件循環被阻止。 它沒法處理事件/做業隊列中的回調,由於調用堆棧包含這一幀。
Web API 爲咱們提供了經過異步回調來編寫非阻塞代碼的可能性。當調用像setTimeout
或fetch
這樣的函數時,咱們把全部的工做委託給c++原生代碼,它在一個單獨的線程中運行。一旦操做完成,回調就被放入事件隊列。同時,V8能夠繼續執行 JS 代碼。
使用這種併發模型,咱們能夠處理網絡請求、用戶與UI的交互等等,而不會阻塞 JS 執行線程。
對於但願可以解決複雜任務的每一個開發人員來講,理解 JS 環境由什麼組成是相當重要的。如今咱們知道了異步JavaScript是如何工做的,調用堆棧、事件循環、事件隊列和做業隊列在其併發模型中的角色。
你可能已經猜到的,在V8引擎和瀏覽器引擎後面還有不少工做要作。然而,咱們大多數人只是須要對全部這些概念有一個基本的理解。若是上面的文章對你有幫助,請點擊"在看"呦。
代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug。
https://medium.com/better-pro...
文章每週持續更新,能夠微信搜索「 大遷世界 」第一時間閱讀和催更(比博客早一到兩篇喲),本文 GitHub https://github.com/qq449245884/xiaozhi 已經收錄,整理了不少個人文檔,歡迎Star和完善,你們面試能夠參照考點複習,另外關注公衆號,後臺回覆福利,便可看到福利,你懂的。