No4.瀏覽器中的頁面循環系統

前段時間在《極客時間》上學了一個專欄,通篇略過,乾貨不少,但理解相當不夠透徹,於是計劃用幾周的時間,對本專欄內容用作者的總結以及自己的相對逐字理解,來個通篇的文字記錄學習,書讀百遍,其義自現。
本篇是這個專欄的第四章:《瀏覽器中的頁面循環系統》。本章分爲六節。

15|消息隊列和事件循環:頁面是怎麼“活”起來的?


本節主要專門介紹頁面的事件循環系統,希望通過幾段總結能對頁面的事件循環系統有一個整體上的理解。

使用單線程處理安排好的任務

單線程處理的流程就是把所有任務代碼按照順序寫進主線程裏,等線程運行時,這些任務按照順序在線程中執行,等所有任務執行完成,線程自動退出。

在線程運行過程中處理任務

當然並非所有任務都可以使用單線程處理,有時我們需要在線程運行的過程中處理任務。
那麼要想在線程運行過程中,能接受並執行新的任務,就需要採用事件循環機制。
相較與單線程處理任務,此線程做了兩點改進:

  • 引入了循環機制。(比如一個實現方式是添加for循環。線程一直循環執行)。
  • 引入了事件。
處理其他線程發送過來的任務

如何設計好一個線程模型,能讓其能夠接受其他線程發送的消息呢?
一個通用的模式是消息隊列:「消息隊列是一種數據結構、可以存放要執行的任務。它符合隊列“先進先出”的特點。」
有了隊列之後繼續改進步驟如下:

  • 添加一個消息隊列。
  • IO線程中產生的新任務添加進消息隊列尾部。
  • 渲染主進程會循環地從消息隊列頭部中讀取任務,執行任務。
處理其他進程發送過來的任務

渲染進程專門有一個 IO 線程用來接收其他進程傳進來的消息,接收到消息之後,會將這些消息組裝成任務發送給渲染主線程,後續的步驟就和前面的“處理其他線程發送的任務”一樣。

消息隊列中的任務類型

消息隊列中的任務都有哪些呢?
輸入事件(鼠標滾動、點擊、移動)、微任務、文件讀寫、WebSocket、JavaScript 定時器等等。除此之外,消息隊列中還包含了很多與頁面相關的事件,如 JavaScript 執行、解析 DOM、樣式計算、佈局計算、CSS 動畫等。

頁面使用單線程的缺點
  • 第一個問題是如何處理高優先級的任務。
    由於優先級的問題使得微任務應用而生,微任務是如何權衡效率和實時性的呢?
    通常我們把消息隊列中的任務稱爲宏任務,每個宏任務中都包含了一個微任務隊列,在執行宏任務的過程中,如果 DOM 有變化,那麼就會將該變化添加到微任務列表中,這樣就不會影響到宏任務的繼續執行,因此也就解決了執行效率的問題.等宏任務中的主要功能都直接完成之後,這時候,渲染引擎並不着急去執行下一個宏任務,而是執行當前宏任務中的微任務,因爲 DOM 變化的事件都保存在這些微任務隊列中,這樣也就解決了實時性問題
  • 第二個是如何解決單個任務執行時長過久的問題.
    針對這種情況,JavaScript 可以通過回調功能來規避這種問題,也就是讓要執行的 JavaScript 任務滯後執行。
總結

如果有一些確定好的任務,可以使用一個單線程來按照順序處理這些任務,這是第一版線程模型。
要在線程執行過程中接收並處理新的任務,就需要引入循環語句和事件系統,這是第二版線程模型。
如果要接收其他線程發送過來的任務,就需要引入消息隊列,這是第三版線程模型。
如果其他進程想要發送任務給頁面主線程,那麼先通過 IPC 把任務發送給渲染進程的 IO 線程,IO 線程再把任務發送給頁面主線程。
消息隊列機制並不是太靈活,爲了適應效率和實時性,引入了微任務。

16 | WebAPI : setTimeout是如何實現的


瀏覽器怎麼實現setTimeout

通過上一小節的學習,我們知道:對於一些事件執行的過程是:這些事件先被添加到消息隊列,然後事件循環系統就會按照消息隊列中的順序來執行事件。也就是說,執行一段異步任務,需要先將任務添加到消息隊列中。
不過通過定時器設置回調函數有點特別,它們需要在指定的時間間隔內被調用,但消息隊列中的任務是按照順序執行的,所以爲了保證回調函數能在指定時間內執行,你不能將定時器的回調函數直接添加到消息隊列中。
從Chromium隊列的部分源碼中我們知道,在Chrome中除了正常使用的消息隊列外,還有另外一個消息隊列,這個隊列中維護了需要延遲執行的任務列表,包括了定時器和Chromium內部一些需要延遲執行的任務。
由於消息隊列排隊和一些系統級別的限制,通過setTimeout設置的回調任務並非總是可以實時的執行,這樣就不能滿足一些實時性要求較高的需求。

使用setTimeout的一些注意事項
  • 如果當前任務執行時間過久,會影響延遲到期定時器任務的執行。
  • 如果 setTimeout 存在嵌套調用,那麼系統會設置最短時間間隔爲 4 毫秒。
  • 未激活的頁面,setTimeout 執行最小間隔是 1000 毫秒.
  • 延時執行時間有最大值:大約 24.8 天
  • 使用 setTimeout 設置的回調函數中的 this 不符合直覺.

17 | WebAPI:XMLHttpRequest是怎麼實現的?


在深入講解 XMLHttpRequest 之前,我們得先介紹下同步回調異步回調這兩個概念.

回調函數 VS 系統調用棧

回調函數:將一個函數作爲參數傳遞給另外一個函數,那作爲參數的這個函數就是回調函數。

  • 同步回調函數代碼:
let callback = function(){
    console.log('i am do homework')
}
function doWork(cb) {
    console.log('start do work')
    cb()
    console.log('end do work')
}
doWork(callback)
//start do work
//i am do homework
//end do work
  • 異步回調函數代碼:
let callback = function(){
    console.log('i am do homework')
}
function doWork(cb) {
    console.log('start do work')
    setTimeout(cb,1000)   
    console.log('end do work')
}
doWork(callback)
XMLHttpRequest運作機制

對回調函數有了一個認知後,那麼接着我們來分析下從發起請求到接收數據的完整流程:

首先從XMLHttpRequest的用法開始:

  • 第一步:創建XMLHttpRequest對象。
  • 第二步:爲xhr對象註冊回調函數。
  • 第三步:配置基礎的請求信息。
  • 第四步:發起請求。
XMLRequest使用過程中的“坑”
  • 跨域問題
  • HTTPS混合內容的問題:這是指HTTPS頁面中包含了不符合HTTPS安全要求的內容,比如包含了HTTP資源。
小結

setTimeout 是直接將延遲任務添加到延遲隊列中,而 XMLHttpRequest 發起請求,是由瀏覽器的其他進程或者線程去執行,然後再將執行結果利用 IPC 的方式通知渲染進程,之後渲染進程再將對應的消息添加到消息隊列中。

18 | 宏任務和微任務:不是所有的任務都是一個待遇


前面我們已經知道微任務可以在實時性和效率之間做一個有效的權衡。微任務已被廣泛應用,比如Promise以及以Promise爲基礎開發出來的很多其他的技術。
宏任務與微任務的區別:

宏任務

頁面中的大部分任務都是在主線程上執行的。如渲染事件、用戶交互事件、JavaScript腳本執行事件、網絡請求等等。這些在消息隊列中的任務稱爲宏任務。
雖然宏任務可以滿足我們大部門的日常需求,但是有時對時間精度要求較高的需求,宏任務就難以勝任了。

微任務

微任務就是一個需要異步執行的函數,執行時機是在主函數執行結束之後、當前宏任務結束之前。
產生微任務的兩種方式:

  • 第一種方式是使用 MutationObserver 監控某個 DOM 節點,然後再通過 JavaScript 來修改這個節點,或者爲這個節點添加、刪除部分子節點,當 DOM 節點發生變化時,就會產生 DOM 變化記錄的微任務。
  • 第二種方式是使用 Promise,當調用 Promise.resolve() 或者 Promise.reject() 的時候,也會產生微任務。
    通過微任務的工作流程,我們可以得出如下結論:
  • 微任務和宏任務是綁定的,每個宏任務在執行時,會創建自己的微任務隊列。
  • 微任務的執行時長會影響到當然宏任務的執行時長,因此寫代碼的時候一定要注意微任務的執行時長。
  • 在一個宏任務中,分別創建一個用於回調的宏任務和微任務,無論什麼情況下,微任務早於宏任務執行。
監聽DOM變化演變

微任務應用在了MutationObserver中,MutationObserver是用來監聽DOM變化的一套方法。 監聽DOM變化一直是前端工程師一項非常核心的需求。
下面是監聽DOM變化演變的簡單總結:

  • 早起觀測DOM變化就是輪詢檢測。比如使用 setTimeout 或者 setInterval 來定時檢測 DOM 是否有改變。無疑這種方式實時性不好,效率還低效。
  • 2000年的時候引入了Mutation Event,Mutation Event採用了觀察者的設計模式,當DOM有變動時立即出發相應的事件。此方式屬於同步回調。雖然這種方式解決了實時性問題,但是因爲會產生較大性能開銷、導致頁面性能出現問題,被反對使用並逐步從web標準事件中刪除。
  • MutationObserver替代MutationEvent,相較於Event方式,Observer採用了一次觸發異步回調。且採用微任務的處理,使得實時性與性能功能都得到有效提高。

19 | Promise:使用Promise,告別回調函數

微任務的另一個應用:Promise。
本節簡單介紹JavaScript引入Promise的動機,以及解決問題的幾個核心關鍵點。
講到動機,也就是說Promise解決了什麼問題。衆所周知,他解決的是異步編碼風格的問題。

頁面編程的一大特點就是:異步編程,下面分析異步編程的代碼風格進化。

  • 之前的代碼編碼風格,一段代碼可能會出現五次回調,這種回調導致代碼邏輯不連貫、不連線,不符合人的直覺。
  • 然後開發人員們通過封裝異步代碼,讓處理流程變得線性,但是這種處理方式如果嵌套了太多的回調函數就容易陷入回調地獄。
  • 陷入回調地獄的後代碼看上去很亂主要是兩點:嵌套調用和任務不確定性(成功或者失敗)。於是Promise出現,解決了這兩個問題。
Promise:消滅嵌套調用和多次錯誤處理

Promise通過兩步解決嵌套回調問題:

  • 首先,Promise實現了回調函數的延時綁定(.then)
  • 其次,將回調函數返回值穿透到最外層。

Promise處理異常:
通過最後一個catch,將所有對象合併到一個函數來處理之前的所有異常。

Promise與微任務

Promise 之所以要使用微任務是由 Promise 回調函數延遲綁定技術導致的。

20 | async/await:使用同步的方式去寫異步代碼

當Promise解決回調地獄代碼風格的同時,我們發現寫很多的then函數,還是有些不太容易閱讀。
基於這個原因,ES7引入了async/await,這是JavaScript異步編程的一個重大改進,提供了在不阻塞主線程的情況下使用同步代碼實現異步訪問資源的能力。並且使得代碼邏輯更加清晰。

本節首先介紹生成器(Generator)是如何工作的,接着介紹了Generator的底層實現機制--協程。
這是因爲async/await使用了Generator和Promise兩種技術。所以緊接着通過Generator和Promise來分析async/await到底是如何通過以同步方式來編寫異步代碼的。

生成器 VS 協程

生成器函數:生成器函數是一個帶星號函數,而且是可以暫停執行和恢復執行的。
具體使用方式就是:在生成器函數內部執行一段代碼,若遇到yiled關鍵字,那JS引擎將返回該關鍵字後面的內容且暫停該函數執行,外部函數通過next方法恢復函數的執行。
那麼JavaScript引擎V8是如何實現一個函數的暫停和恢復的?

搞懂它的暫停和恢復,需要首先了解協程的概念。協程是一種比線程更加輕量級的存在。可以把協程看作是跑在線程上的任務,一個線程可以存在多個協程。但在線程上同時只能執行一個協程。
在JS中,生成器就是協程的一種實現方式。

asnyc/await

爲了更近一步改進生成器代碼,ES7引入了async/awit,實現了更加直觀簡潔的代碼。
async/aswit技術背後的實現就是Promise和生成器應用。往底層說就是微服務和協程應用。

async:是一個通過異步執行並隱式返回Promise作爲結果的函數。
await:我們知道了 async 函數返回的是一個 Promise 對象,那下面我們再結合文中這段代碼來看看 await 到底是什麼。

async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)
//輸出結果:0 3 100 2

async/await 無疑是異步編程領域非常大的一個革新,也是未來的一個主流的編程風格。其實,除了 JavaScript,Python、Dart、C# 等語言也都引入了 async/await,使用它不僅能讓代碼更加整潔美觀,而且還能確保該函數始終都能返回 Promise。

世界有多大,取決於你認識和見過多少人和事。

如有疑問請添加我的微信號:18231133236。歡迎交流!
更多內容,請訪問的我的個人博客:https://www.liugezhou.online.
您也可以關注我的個人公衆號:【Dangerous Wakaka】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章