91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

JavaScript運行機制及原理是什么

發布時間:2022-04-25 17:35:02 來源:億速云 閱讀:267 作者:zzz 欄目:web開發

這篇文章主要介紹“JavaScript運行機制及原理是什么”,在日常操作中,相信很多人在JavaScript運行機制及原理是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”JavaScript運行機制及原理是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!

JavaScript運行機制及原理是什么

什么是JavaScript解析引擎

簡單地說,JavaScript解析引擎就是能夠“讀懂”JavaScript代碼,并準確地給出代碼運行結果的一段程序。

比方說,當你寫了 var a = 1 + 1; 這樣一段代碼,JavaScript引擎做的事情就是看懂(解析)你這段代碼,并且將a的值變為2。

學過編譯原理的人知道,對于靜態語言來說(如Java、C++、C),處理上述這些事情的叫編譯器(Compiler),相應地對于JavaScript這樣的動態語言則叫解釋器(Interpreter)。這兩者的區別用一句話來概括就是:編譯器是將源代碼編譯為另外一種代碼(比如機器碼,或者字節碼),而解釋器是直接解析并將代碼運行結果輸出。 比方說,firebug的console就是一個JavaScript的解釋器。

但是,現在很難去界定說,JavaScript引擎它到底算是個解釋器還是個編譯器,因為,比如像V8(Chrome的JS引擎),它其實為了提高JS的運行性能,在運行之前會先將JS編譯為本地的機器碼(native machine code),然后再去執行機器碼(這樣速度就快很多)。

JavaScript解析引擎與ECMAScript是什么關系

JavaScript引擎是一段程序,我們寫的JavaScript代碼也是程序,如何讓程序去讀懂程序呢?這就需要定義規則。比如,之前提到的var a = 1 + 1;,它表示:

左邊var代表了這是申明(declaration),它聲明了a這個變量
右邊的+表示要將1和1做加法
中間的等號表示了這是個賦值語句
最后的分號表示這句語句結束了
上述這些就是規則,有了它就等于有了衡量的標準,JavaScript引擎就可以根據這個標準去解析JavaScript代碼了。那么這里的ECMAScript就是定義了這些規則。其中ECMAScript 262這份文檔,就是對JavaScript這門語言定義了一整套完整的標準。其中包括:

var,if,else,break,continue等是JavaScript的關鍵詞
abstract,int,long等是JavaScript保留詞
怎么樣算是數字、怎么樣算是字符串等等
定義了操作符(+,-,>,<等)
定義了JavaScript的語法
定義了對表達式,語句等標準的處理算法,比如遇到==該如何處理
??
標準的JavaScript引擎就會根據這套文檔去實現,注意這里強調了標準,因為也有不按照標準來實現的,比如IE的JS引擎。這也是為什么JavaScript會有兼容性的問題。至于為什么IE的JS引擎不按照標準來實現,就要說到瀏覽器大戰了,這里就不贅述了,自行Google之。

所以,簡單的說,ECMAScript定義了語言的標準,JavaScript引擎根據它來實現,這就是兩者的關系。

JavaScript解析引擎與瀏覽器是什么關系

簡單地說,JavaScript引擎是瀏覽器的組成部分之一。因為瀏覽器還要做很多別的事情,比如解析頁面、渲染頁面、Cookie管理、歷史記錄等等。那么,既然是組成部分,因此一般情況下JavaScript引擎都是瀏覽器開發商自行開發的。比如:IE9的Chakra、Firefox的TraceMonkey、Chrome的V8等等。

從而也看出,不同瀏覽器都采用了不同的JavaScript引擎。因此,我們只能說要深入了解哪個JavaScript引擎。

為什么JavaScript是單線程

JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那么,為什么JavaScript不能有多個線程呢?這樣能提高效率啊。

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為準?

所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特征,將來也不會改變。

為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質。

區分進程和線程

進程是cpu資源分配的最小單位,進程可以包含多個線程。 瀏覽器就是多進程的,每打開的一個瀏覽器窗口就是一個進程。

線程是cpu調度的最小單位,同一進程下的各個線程之間共享程序的內存空間。

可以把進程看做一個倉庫,線程是可以運輸的貨車,每個倉庫有屬于自己的多輛貨車為倉庫服務(運貨),每個倉庫可以同時由多輛車同時拉貨,但是每輛車同一時間只能干一件事,就是運輸本次的貨物。

核心點:

進程是 cpu 資源分配的最小單位(是能擁有資源和獨立運行的最小單位)

線程是 cpu 調度的最小單位(線程是建立在進程的基礎上的一次程序運行單位,一個進程中可以有多個線程)

不同進程之間也可以通信,不過代價較大。
瀏覽器是多進程的
理解了進程與線程了區別后,接下來對瀏覽器進行一定程度上的認識:(先看下簡化理解)

瀏覽器是多進程的

瀏覽器之所以能夠運行,是因為系統給它的進程分配了資源(cpu、內存)

簡單點理解,每打開一個Tab頁,就相當于創建了一個獨立的瀏覽器進程。

以 Chrome 為例,它的多個標簽頁,然后可以在Chrome的任務管理器中看到有多個進程(分別是每一個 Tab 頁面有一個獨立的進程,以及一個主進程),在 Windows 的任務管理器中也可以看出。

注意:在這里瀏覽器應該也有自己的優化機制,有時候打開多個tab頁后,可以在Chrome任務管理器中看到,有些進程被合并了 (所以每一個Tab標簽對應一個進程并不一定是絕對的)

瀏覽器都包含哪些進程

知道了瀏覽器是多進程后,再來看看它到底包含哪些進程:(為了簡化理解,僅列舉主要進程)

(1)Browser 進程:瀏覽器的主進程(負責協調、主控),只有一個。作用:

  • 負責瀏覽器界面顯示,與用戶交互。如前進,后退等

  • 負責各個頁面的管理,創建和銷毀其他進程

  • 將 Renderer 進程得到的內存中的 Bitmap,繪制到用戶界面上

  • 網絡資源的管理,下載等

(2)第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
(3)GPU 進程:最多一個,用于 3D 繪制等
(4)瀏覽器渲染進程(瀏覽器內核,Renderer進程,內部是多線程的):默認每個 Tab 頁面一個進程,互不影響。主要作用為:頁面渲染,腳本執行,事件處理等

強化記憶:在瀏覽器中打開一個網頁相當于新起了一個進程(進程內有自己的多線程)

當然,瀏覽器有時會將多個進程合并(譬如打開多個空白標簽頁后,會發現多個空白標簽頁被合并成了一個進程)

瀏覽器多進程的優勢

相比于單進程瀏覽器,多進程有如下優點:

  • 避免單個 page crash 影響整個瀏覽器

  • 避免第三方插件 crash 影響整個瀏覽器

  • 多進程充分利用多核優勢

  • 方便使用沙盒模型隔離插件等進程,提高瀏覽器穩定性

簡單理解:

如果瀏覽器是單進程,那么某個 Tab 頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單進程,插件崩潰了也會影響整個瀏覽器。

當然,內存等資源消耗也會更大,有點空間換時間的意思。再大的內存也不夠 Chrome 吃的,內存泄漏問題現在已經改善了一些了,也僅僅是改善,還有就是會導致耗電增加。

瀏覽器內核(渲染進程)

重點來了,我們可以看到,上面提到了這么多的進程,那么,對于普通的前端操作來說,最終要的是什么呢?答案是渲染進程。

可以這樣理解,頁面的渲染,JS 的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程

請牢記,瀏覽器的渲染進程是多線程的(JS 引擎是單線程的)

那么接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

1、GUI 渲染線程
  • 負責渲染瀏覽器界面,解析 HTML,CSS,構建 DOM 樹和 RenderObject 樹(簡單理解為 CSS 形成的樣式樹,Flutter 核心之一),布局和繪制等。

  • 當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行

  • 注意,GUI 渲染線程與 JS 引擎線程是互斥的,當 JS 引擎執行時 GUI 線程會被掛起(凍結),GUI 更新會被保存在一個隊列中 等到 JS 引擎空閑時 立即被執行。

2、JS 引擎線程
  • 也稱為 JS 內核,負責處理 Javascript 腳本程序。(例如 V8 引擎)

  • JS 引擎線程負責解析 Javascript 腳本,運行代碼。

  • JS 引擎一直等待著任務隊列中任務的到來,然后加以處理,一個 Tab 頁(renderer 進程)中無論什么時候都只有一個 JS 線程在運行 JS 程序

  • 同樣注意,GUI 渲染線程與 JS 引擎線程是互斥的,所以如果 JS 執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞。

3、事件觸發線程
  • 歸屬于瀏覽器而不是 JS 引擎,用來控制事件循環(可以理解,JS 引擎自己都忙不過來,需要瀏覽器另開線程協助)

  • 當 JS 引擎要執行代碼塊如 setTimeOut 時(也可來自瀏覽器內核的其他線程,如鼠標點擊、Ajax 異步請求等),會將對應任務添加到事件線程中。并且會負責排序

  • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JS 引擎的處理

  • 注意,由于 JS 的單線程關系,所以這些待處理隊列中的事件都得排隊等待 JS 引擎處理(當 JS 引擎空閑時才會去執行)

  • 這里可以簡單理解為,它負責管理一堆事件和一個“事件隊列”,只有在事件隊列的任務 JS 引擎才會在空閑的時候去執行,而它要做的,就是負責當某個事件被觸發時,把它加入到事件隊列。例如鼠標單擊。

4、定時觸發器線程
  • 傳說中的 setInterval 與 setTimeout 所在的線程

  • 瀏覽器定時計數器并不是由 JavaScript 引擎計數的,(因為 JavaScript 引擎是單線程的,如果處于阻塞線程狀態就會影響記計時的準確)

  • 因此通過單獨線程來計時并觸發定時,計時完畢后,添加到事件隊列中(對應 事件觸發線程 的“事件符合觸發條件被觸發時”),等待 JS 引擎空閑后執行。

  • 注意,W3C 在 HTML 標準中規定,規定要求 setTimeout 中低于 4ms 的時間間隔算為 4ms。

5、異步http請求線程
  • 在 XMLHttpRequest 在連接后是通過瀏覽器新開一個線程請求

  • 將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件,將這個回調再放入事件隊列中。再由 JavaScript 引擎執行。

Browser進程和瀏覽器內核(Renderer進程)的通信過程

看到這里,首先,應該對瀏覽器內的進程和線程都有一定理解了,那么接下來,再談談瀏覽器的 Browser 進程(控制進程)是如何和內核通信的, 這點也理解后,就可以將這部分的知識串聯起來,從頭到尾有一個完整的概念。

如果自己打開任務管理器,然后打開一個瀏覽器,就可以看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開 Tab 頁的渲染進程), 然后在這前提下,看下整個的過程:(簡化了很多)

  • Browser 進程收到用戶請求,首先需要獲取頁面內容(譬如通過網絡下載資源),隨后將該任務通過 RendererHost 接口傳遞給 Render (內核)進程

  • Renderer 進程的 Renderer 接口收到消息,簡單解釋后,交給渲染線程,然后開始渲染

  1. 渲染線程接收請求,加載網頁并渲染網頁,這其中可能需要 Browser 進程獲取資源和需要 GPU 進程來幫助渲染

  2. 當然可能會有 JS 線程操作 DOM(這樣可能會造成回流并重繪)

  3. 最后 Render 進程將結果傳遞給 Browser 進程

  • Browser 進程接收到結果并將結果繪制出來

這里繪一張簡單的圖:(很簡化)
JavaScript運行機制及原理是什么

瀏覽器內核中線程之間的關系

到了這里,已經對瀏覽器的運行有了一個整體的概念,接下來,先簡單梳理一些概念

GUI 渲染線程與 JS 引擎線程互斥

由于 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時渲染界面(即 JS 線程和 UI 線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致了。

因此為了防止渲染出現不可預期的結果,瀏覽器設置 GUI 渲染線程與 JS 引擎為互斥的關系,當 JS 引擎執行時 GUI 線程會被掛起, GUI 更新則會被保存在一個隊列中等到 JS 引擎線程空閑時立即被執行。

JS阻塞頁面加載

從上述的互斥關系,可以推導出,JS 如果執行時間過長就會阻塞頁面。

譬如,假設 JS 引擎正在進行巨量的計算,此時就算 GUI 有更新,也會被保存到隊列中,等待 JS 引擎空閑后執行。 然后,由于巨量計算,所以 JS 引擎很可能很久很久后才能空閑,自然會感覺到巨卡無比。

所以,要盡量避免 JS 執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。

解決這種問題,除了將運算放在后端,如果避不開,并且巨量計算還和 UI 有關系,那么我的思路就是使用 setTimeout 將任務分割,中間給出一點空閑時間讓 JS 引擎去處理下 UI,不至于頁面直接卡死。

如果直接決定最低要求 HTML5+ 的版本,那么可以看看下面的 WebWorker。

WebWorker, JS的多線程

前文中有提到 JS 引擎是單線程的,而且 JS 執行時間過長會阻塞頁面,那么 JS 就真的對 cpu 密集型計算無能為力么?

所以,后來 HTML5 中支持了Web Worker。

MDN 的官方解釋是:

Web Worker 為 Web 內容在后臺線程中運行腳本提供了一種簡單的方法。

線程可以執行任務而不干擾用戶界面,一個 worker 是使用一個構造函數創建的一個對象(Worker())運行一個命名的 JavaScript 文件(這個文件包含將在工作線程中運行的代碼)。

workers 運行在另一個全局上下文中,不同于當前的 window。

因此,使用 window 快捷方式獲取當前全局的范圍(而不是 self)在一個 Worker 內將返回錯誤

這樣理解下:

創建 Worker 時,JS 引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程控制,而且不能操作 DOM)
JS 引擎線程與 worker 線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)
所以,如果有非常耗時的工作,請單獨開一個 Worker 線程,這樣里面不管如何翻天覆地都不會影響 JS 引擎主線程, 只待計算出結果后,將結果通信給主線程即可,perfect!

而且注意下,JS 引擎是單線程的,這一點的本質仍然未改變,Worker 可以理解是瀏覽器給 JS 引擎開的外掛,專門用來解決那些大量計算問題。

其它,關于 Worker 的詳解就不是本文的范疇了,因此不再贅述。

WebWorker與SharedWorker

既然都到了這里,就再提一下 SharedWorker(避免后續將這兩個概念搞混)

WebWorker 只屬于某個頁面,不會和其他頁面的 Render 進程(瀏覽器內核進程)共享
所以 Chrome 在 Render 進程中(每一個 Tab 頁就是一個 Render 進程)創建一個新的線程來運行 Worker 中的 JavaScript 程序。
SharedWorker 是瀏覽器所有頁面共享的,不能采用與 Worker 同樣的方式實現,因為它不隸屬于某個 Render 進程,可以為多個 Render 進程共享使用
所以 Chrome 瀏覽器為 SharedWorker 單獨創建一個進程來運行 JavaScript 程序,在瀏覽器中每個相同的 JavaScript 只存在一個 SharedWorker 進程,不管它被創建多少次。
看到這里,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker 由獨立的進程管理,WebWorker 只是屬于 Render 進程下的一個線程。

瀏覽器渲染流程

補充下瀏覽器的渲染流程(簡單版本)

為了簡化理解,前期工作直接省略成:

瀏覽器輸入 url,瀏覽器主進程接管,開一個下載線程,然后進行 http 請求(略去 DNS 查詢,IP 尋址等等操作),然后等待響應,獲取內容,隨后將內容通過 RendererHost 接口轉交給 Renderer 進程
瀏覽器渲染流程開始
瀏覽器器內核拿到內容后,渲染大概可以劃分成以下幾個步驟:

解析 html 建立 dom 樹
解析 css 構建 render 樹(將 CSS 代碼解析成樹形的數據結構,然后結合 DOM 合并成 render 樹)
布局 render 樹(Layout/reflow),負責各元素尺寸、位置的計算
繪制 render 樹(paint),繪制頁面像素信息
瀏覽器會將各層的信息發送給 GPU,GPU 會將各層合成(composite),顯示在屏幕上。
所有詳細步驟都已經略去,渲染完畢后就是 load 事件了,之后就是自己的 JS 邏輯處理了。

既然略去了一些詳細的步驟,那么就提一些可能需要注意的細節把。
JavaScript運行機制及原理是什么

load事件與DOMContentLoaded事件的先后

上面提到,渲染完畢后會觸發 load 事件,那么你能分清楚 load 事件與 DOMContentLoaded 事件的先后么?

很簡單,知道它們的定義就可以了:

當 DOMContentLoaded 事件觸發時,僅當 DOM 加載完成,不包括樣式表,圖片,async 腳本等。
當 onload 事件觸發時,頁面上所有的 DOM,樣式表,腳本,圖片都已經加載完成了,也就是渲染完畢了
所以,順序是:DOMContentLoaded -> load

css加載是否會阻塞dom樹渲染

這里說的是頭部引入 css 的情況

首先,我們都知道:css 是由單獨的下載線程異步下載的。

然后再說下幾個現象:

  • css 加載不會阻塞 DOM 樹解析(異步加載時 DOM 照常構建)

  • 但會阻塞 render 樹渲染(渲染時需等 css 加載完畢,因為 render 樹需要 css 信息)

這可能也是瀏覽器的一種優化機制。因為你加載 css 的時候,可能會修改下面 DOM 節點的樣式,如果 css 加載不阻塞 render 樹渲染的話,那么當 css 加載完之后, render 樹可能又得重新重繪或者回流了,這就造成了一些沒有必要的損耗。

所以干脆就先把 DOM 樹的結構先解析完,把可以做的工作做完,然后等你 css 加載完之后, 在根據最終的樣式來渲染 render 樹,這種做法性能方面確實會比較好一點。

普通圖層和復合圖層

渲染步驟中就提到了 composite 概念。

可以簡單的這樣理解,瀏覽器渲染的圖層一般包含兩大類:普通圖層 以及 復合圖層

首先,普通文檔流內可以理解為一個復合圖層(這里稱為 默認復合層,里面不管添加多少元素,其實都是在同一個復合圖層中)

其次,absolute 布局(fixed 也一樣),雖然可以脫離普通文檔流,但它仍然屬于 默認復合層。

然后,可以通過 硬件加速 的方式,聲明一個 新的復合圖層,它會單獨分配資源 (當然也會脫離普通文檔流,這樣一來,不管這個復合圖層中怎么變化,也不會影響 默認復合層 里的回流重繪)

可以簡單理解下:GPU 中,各個復合圖層是單獨繪制的,所以互不影響,這也是為什么某些場景硬件加速效果一級棒

可以Chrome DevTools --> More Tools --> Rendering --> Layer borders中看到,黃色的就是復合圖層信息

如何變成復合圖層(硬件加速)

將該元素變成一個復合圖層,就是傳說中的硬件加速技術

最常用的方式:translate3d、translateZ
opacity 屬性/過渡動畫(需要動畫執行的過程中才會創建合成層,動畫沒有開始或結束后元素還會回到之前的狀態)
will-chang 屬性(這個比較偏僻),一般配合 opacity 與 translate 使用, 作用是提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優化工作(這個最好用完后就釋放)
video、 iframe、 canvas、 webgl 等元素
其它,譬如以前的 flash 插件
absolute和硬件加速的區別

可以看到,absolute 雖然可以脫離普通文檔流,但是無法脫離默認復合層。 所以,就算 absolute 中信息改變時不會改變普通文檔流中 render 樹, 但是,瀏覽器最終繪制時,是整個復合層繪制的,所以 absolute 中信息的改變,仍然會影響整個復合層的繪制。 (瀏覽器會重繪它,如果復合層中內容多,absolute 帶來的繪制信息變化過大,資源消耗是非常嚴重的)

而硬件加速直接就是在另一個復合層了(另起爐灶),所以它的信息改變不會影響默認復合層 (當然了,內部肯定會影響屬于自己的復合層),僅僅是引發最后的合成(輸出視圖)

復合圖層的作用

一般一個元素開啟硬件加速后會變成復合圖層,可以獨立于普通文檔流中,改動后可以避免整個頁面重繪,提升性能,但是盡量不要大量使用復合圖層,否則由于資源消耗過度,頁面反而會變的更卡

硬件加速時請使用index

使用硬件加速時,盡可能的使用 index,防止瀏覽器默認給后續的元素創建復合層渲染

具體的原理時這樣的: webkit CSS3 中,如果這個元素添加了硬件加速,并且 index 層級比較低, 那么在這個元素的后面其它元素(層級比這個元素高的,或者相同的,并且 releative 或 absolute 屬性相同的), 會默認變為復合層渲染,如果處理不當會極大的影響性能

簡單點理解,其實可以認為是一個隱式合成的概念:如果 a 是一個復合圖層,而且 b 在 a 上面,那么 b 也會被隱式轉為一個復合圖層,這點需要特別注意。

從EventLoop談JS的運行機制

到此時,已經是屬于瀏覽器頁面初次渲染完畢后的事情,JS 引擎的一些運行機制分析。

注意,這里不談 可執行上下文,VO,scop chain 等概念(這些完全可以整理成另一篇文章了),這里主要是結合 Event Loop 來談 JS 代碼是如何執行的。

讀這部分的前提是已經知道了 JS 引擎是單線程,而且這里會用到上文中的幾個概念:

  • JS 引擎線程

  • 事件觸發線程

  • 定時觸發器線程
    然后再理解一個概念:

JS 分為同步任務和異步任務

  • 同步任務都在主線程上執行,形成一個 執行棧

  • 主線程之外,事件觸發線程管理著一個 任務隊列,只要異步任務有了運行結果,就在 任務隊列 之中放置一個事件。

  • 一旦 執行棧 中的所有同步任務執行完畢(此時 JS 引擎空閑),系統就會讀取 任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

看圖:
JavaScript運行機制及原理是什么

看到這里,應該就可以理解了:為什么有時候 setTimeout 推入的事件不能準時執行?因為可能在它推入到事件列表時,主線程還不空閑,正在執行其它代碼, 所以自然有誤差。

事件循環機制進一步補充

JavaScript運行機制及原理是什么

上圖大致描述就是:

  • 主線程運行時會產生執行棧, 棧中的代碼調用某些 api 時,它們會在事件隊列中添加各種事件(當滿足觸發條件后,如 ajax 請求完畢)

  • 而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調,如此循環

注意,總是要等待棧中的代碼執行完畢后才會去讀取事件隊列中的事件

單獨說說定時器

上述事件循環機制的核心是:JS 引擎線程和事件觸發線程

但事件上,里面還有一些隱藏細節,譬如調用 setTimeout 后,是如何等待特定時間后才添加到事件隊列中的?

是 JS 引擎檢測的么?當然不是了。它是由定時器線程控制(因為 JS 引擎自己都忙不過來,根本無暇分身)

為什么要單獨的定時器線程?因為 JavaScript 引擎是單線程的,如果處于阻塞線程狀態就會影響記計時的準確,因此很有必要單獨開一個線程用來計時。

什么時候會用到定時器線程?當使用 setTimeout 或 setInterval 時,它需要定時器線程計時,計時完成后就會將特定的事件推入事件隊列中。

setTimeout而不是setInterval

用 setTimeout 模擬定期計時和直接用 setInterval 是有區別的。

因為每次 setTimeout 計時到后就會去執行,然后執行一段時間后才會繼續 setTimeout,中間就多了誤差 (誤差多少與代碼執行時間有關)

而 setInterval 則是每次都精確的隔一段時間推入一個事件,但是,事件的實際執行時間不一定就準確,還有可能是這個事件還沒執行完畢,下一個事件就來了。

而且 setInterval 有一些比較致命的問題就是:

累計效應,如果 setInterval 代碼在再次添加到隊列之前還沒有完成執行, 就會導致定時器代碼連續運行好幾次,而之間沒有間隔。 就算正常間隔執行,多個 setInterval 的代碼執行時間可能會比預期小(因為代碼執行需要一定時間)
譬如像 iOS 的 webview,或者 Safari 等瀏覽器中都有一個特點,在滾動的時候是不執行JS的,如果使用了 setInterval,會發現在滾動結束后會執行多次由于滾動不執行 JS 積攢回調,如果回調執行時間過長,就會非常容器造成卡頓問題和一些不可知的錯誤(這一塊后續有補充,setInterval 自帶的優化,不會重復添加回調)
而且把瀏覽器最小化顯示等操作時,setInterval 并不是不執行程序, 它會把 setInterval 的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間全部執行
所以,鑒于這么多問題,目前一般認為的最佳方案是:用 setTimeout 模擬 setInterval,或者特殊場合直接用 requestAnimationFrame

補充:JS 高程中有提到,JS 引擎會對 setInterval 進行優化,如果當前事件隊列中有 setInterval 的回調,不會重復添加。

事件循環進階:macrotask與microtask

上文中將 JS 事件循環機制梳理了一遍,在 ES5 的情況是夠用了,但是在 ES6 盛行的現在,仍然會遇到一些問題,譬如下面這題:

console.log('script start');

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

Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});

console.log('script end');

嗯哼,它的正確執行順序是這樣子的:

script start
script end
promise1
promise2
setTimeout

為什么呢?因為 Promise 里有了一個一個新的概念:microtask

或者,進一步,JS 中分為兩種任務類型:macrotask 和 microtask,在 ECMAScript 中,microtask 稱為 jobs,macrotask 可稱為task。

它們的定義?區別?簡單點可以按如下理解:

1、macrotask(又稱之為宏任務)

可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放到執行棧中執行)

  • 每一個 task 會從頭到尾將這個任務執行完畢,不會執行其它

  • 瀏覽器為了能夠使得 JS 內部 task 與 DOM 任務能夠有序的執行,會在一個 task 執行結束后,在下一個 task 執行開始前,對頁面進行重新渲染 (task -> 渲染 -> task -> …)


2、microtask(又稱為微任務)

可以理解是在當前 task 執行結束后立即執行的任務

  • 也就是說,在當前 task 任務后,下一個 task 之前,在渲染之前

  • 所以它的響應速度相比 setTimeout(setTimeout 是 task)會更快,因為無需等渲染

  • 也就是說,在某一個 macrotask 執行完后,就會將在它執行期間產生的所有 microtask 都執行完畢(在渲染前)

3、分別什么樣的場景會形成 macrotask 和 microtask 呢
  • macrotask:主代碼塊,setTimeout,setInterval 等(事件隊列中的每一個事件都是一個 macrotask)

  • microtask:Promise,process.nextTick 等

補充:在 node 環境下,process.nextTick 的優先級高于 Promise,也就是可以簡單理解為:在宏任務結束后會先執行微任務隊列中的 nextTickQueue 部分,然后才會執行微任務中的 Promise 部分。

再根據線程來理解下:

(1)macrotask 中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護

(2)microtask 中的所有微任務都是添加到微任務隊列(Job Queues)中,等待當前 macrotask 執行完畢后執行,而這個隊列由 JS 引擎線程維護 (這點由自己理解+推測得出,因為它是在主線程下無縫執行的)
所以,總結下運行機制:

  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)

  • 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中

  • 宏任務執行完畢后,立即執行當前微任務隊列中的所有微任務(依次執行)

  • 當前宏任務執行完畢,開始檢查渲染,然后GUI線程接管渲染

  • 渲染完畢后,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

如圖:
JavaScript運行機制及原理是什么

另外,請注意下 Promise 的 polyfill 與官方版本的區別:

官方版本中,是標準的 microtask 形式
polyfill,一般都是通過 setTimeout 模擬的,所以是 macrotask 形式
注意,有一些瀏覽器執行結果不一樣(因為它們可能把 microtask 當成 macrotask 來執行了), 但是為了簡單,這里不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能并不標準)

補充:使用MutationObserver實現microtask

MutationObserver可以用來實現microtask (它屬于microtask,優先級小于Promise, 一般是Promise不支持時才會這樣做)

它是HTML5中的新特性,作用是:監聽一個DOM變動, 當DOM對象樹發生任何變動時,Mutation Observer會得到通知

像以前的Vue源碼中就是利用它來模擬nextTick的, 具體原理是,創建一個TextNode并監聽內容變化, 然后要nextTick的時候去改一下這個節點的文本內容, 如下:

var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))

observer.observe(textNode, {
    characterData: true
})
timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
}

不過,現在的Vue(2.5+)的nextTick實現移除了MutationObserver的方式(據說是兼容性原因), 取而代之的是使用MessageChannel (當然,默認情況仍然是Promise,不支持才兼容的)。

MessageChannel屬于宏任務,優先級是:MessageChannel->setTimeout, 所以Vue(2.5+)內部的nextTick與2.4及之前的實現是不一樣的,需要注意下。

到此,關于“JavaScript運行機制及原理是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

定州市| 新绛县| 塔城市| 沙坪坝区| 琼结县| 昌江| 淄博市| 西华县| 衡水市| 夏河县| 方城县| 清水县| 南京市| 北碚区| 顺平县| 乌拉特后旗| 登封市| 荔浦县| 新丰县| 城口县| 宣恩县| 昌宁县| 巴青县| 延川县| 清镇市| 凯里市| 晴隆县| 铜鼓县| 新野县| 和林格尔县| 铁岭县| 山阳县| 全椒县| 财经| 韶山市| 万安县| 澜沧| 阳谷县| 聂拉木县| 阿巴嘎旗| 宣威市|