本節主要專門介紹頁面的事件循環系統,但願經過幾段總結能對頁面的事件循環系統有一個總體上的理解。前端
單線程處理的流程就是把全部任務代碼按照順序寫進主線程裏,等線程運行時,這些任務按照順序在線程中執行,等全部任務執行完成,線程自動退出。web
固然並不是全部任務均可以使用單線程處理,有時咱們須要在線程運行的過程當中處理任務。
那麼要想在線程運行過程當中,能接受並執行新的任務,就須要採用事件循環機制。 相較與單線程處理任務,此線程作了兩點改進:編程
- 引入了循環機制。(好比一個實現方式是添加for循環。線程一直循環執行)。
- 引入了事件。
如何設計好一個線程模型,能讓其可以接受其餘線程發送的消息呢?
一個通用的模式是消息隊列:「消息隊列是一種數據結構、能夠存放要執行的任務。它符合隊列「先進先出」的特色。」
有了隊列以後繼續改進步驟以下:設計模式
- 添加一個消息隊列。
- IO線程中產生的新任務添加進消息隊列尾部。
- 渲染主進程會循環地從消息隊列頭部中讀取任務,執行任務。
渲染進程專門有一個 IO 線程用來接收其餘進程傳進來的消息,接收到消息以後,會將這些消息組裝成任務發送給渲染主線程,後續的步驟就和前面的「處理其餘線程發送的任務」同樣。跨域
消息隊列中的任務都有哪些呢?
輸入事件(鼠標滾動、點擊、移動)、微任務、文件讀寫、WebSocket、JavaScript 定時器等等。除此以外,消息隊列中還包含了不少與頁面相關的事件,如 JavaScript 執行、解析 DOM、樣式計算、佈局計算、CSS 動畫等。瀏覽器
- 第一個問題是如何處理高優先級的任務。
因爲優先級的問題使得微任務應用而生,微任務是如何權衡效率和實時性的呢? 一般咱們把消息隊列中的任務稱爲宏任務,每一個宏任務中都包含了一個微任務隊列,在執行宏任務的過程當中,若是 DOM 有變化,那麼就會將該變化添加到微任務列表中,這樣就不會影響到宏任務的繼續執行,所以也就解決了執行效率的問題.等宏任務中的主要功能都直接完成以後,這時候,渲染引擎並不着急去執行下一個宏任務,而是執行當前宏任務中的微任務,由於 DOM 變化的事件都保存在這些微任務隊列中,這樣也就解決了實時性問題- 第二個是如何解決單個任務執行時長太久的問題. 針對這種狀況,JavaScript 能夠經過回調功能來規避這種問題,也就是讓要執行的 JavaScript 任務滯後執行。
若是有一些肯定好的任務,可使用一個單線程來按照順序處理這些任務,這是初版線程模型。
要在線程執行過程當中接收並處理新的任務,就須要引入循環語句和事件系統,這是第二版線程模型。
若是要接收其餘線程發送過來的任務,就須要引入消息隊列,這是第三版線程模型。
若是其餘進程想要發送任務給頁面主線程,那麼先經過 IPC 把任務發送給渲染進程的 IO 線程,IO 線程再把任務發送給頁面主線程。
消息隊列機制並非太靈活,爲了適應效率和實時性,引入了微任務。安全
經過上一小節的學習,咱們知道:對於一些事件執行的過程是:這些事件先被添加到消息隊列,而後事件循環系統就會按照消息隊列中的順序來執行事件。也就是說,執行一段異步任務,須要先將任務添加到消息隊列中。
不過經過定時器設置回調函數有點特別,它們須要在指定的時間間隔內被調用,但消息隊列中的任務是按照順序執行的,因此爲了保證回調函數能在指定時間內執行,你不能將定時器的回調函數直接添加到消息隊列中。
從Chromium隊列的部分源碼中咱們知道,在Chrome中除了正常使用的消息隊列外,還有另一個消息隊列,這個隊列中維護了須要延遲執行的任務列表
,包括了定時器和Chromium內部一些須要延遲執行的任務。 因爲消息隊列排隊和一些系統級別的限制,經過setTimeout設置的回調任務並不是老是能夠實時的執行,這樣就不能知足一些實時性要求較高的需求。bash
- 若是當前任務執行時間太久,會影響延遲到期定時器任務的執行。
- 若是 setTimeout 存在嵌套調用,那麼系統會設置最短期間隔爲 4 毫秒。
- 未激活的頁面,setTimeout 執行最小間隔是 1000 毫秒.
- 延時執行時間有最大值:大約 24.8 天
- 使用 setTimeout 設置的回調函數中的 this 不符合直覺.
在深刻講解 XMLHttpRequest 以前,咱們得先介紹下
同步回調
和異步回調
這兩個概念.網絡
回調函數
:將一個函數做爲參數傳遞給另一個函數,那做爲參數的這個函數就是回調函數。前端工程師
- 同步回調函數代碼:
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對象。
- 第二步:爲xhr對象註冊回調函數。
- 第三步:配置基礎的請求信息。
- 第四步:發起請求。
- 跨域問題
- HTTPS混合內容的問題:這是指HTTPS頁面中包含了不符合HTTPS安全要求的內容,好比包含了HTTP資源。
setTimeout 是直接將延遲任務添加到延遲隊列中,而 XMLHttpRequest 發起請求,是由瀏覽器的其餘進程或者線程去執行,而後再將執行結果利用 IPC 的方式通知渲染進程,以後渲染進程再將對應的消息添加到消息隊列中。
前面咱們已經知道微任務能夠在實時性和效率之間作一個有效的權衡。微任務已被普遍應用,好比Promise以及以Promise爲基礎開發出來的不少其餘的技術。
宏任務與微任務的區別:
頁面中的大部分任務都是在主線程上執行的。如渲染事件、用戶交互事件、JavaScript腳本執行事件、網絡請求等等。這些在消息隊列中的任務稱爲宏任務。
雖然宏任務能夠知足咱們大部門的平常需求,可是有時對時間精度要求較高的需求,宏任務就難以勝任了。
微任務就是一個須要異步執行的函數,執行時機是在主函數執行結束以後、當前宏任務結束以前。
產生微任務的兩種方式:
- 第一種方式是使用 MutationObserver 監控某個 DOM 節點,而後再經過 JavaScript 來修改這個節點,或者爲這個節點添加、刪除部分子節點,當 DOM 節點發生變化時,就會產生 DOM 變化記錄的微任務。
- 第二種方式是使用 Promise,當調用 Promise.resolve() 或者 Promise.reject() 的時候,也會產生微任務。 經過微任務的工做流程,咱們能夠得出以下結論:
- 微任務和宏任務是綁定的,每一個宏任務在執行時,會建立本身的微任務隊列。
- 微任務的執行時長會影響到固然宏任務的執行時長,所以寫代碼的時候必定要注意微任務的執行時長。
- 在一個宏任務中,分別建立一個用於回調的宏任務和微任務,不管什麼狀況下,微任務早於宏任務執行。
微任務應用在了
MutationObserver
中,MutationObserver
是用來監聽DOM變化的一套方法。 監聽DOM變化一直是前端工程師一項很是核心的需求。
下面是監聽DOM變化演變的簡單總結:
- 早起觀測DOM變化就是輪詢檢測。好比使用 setTimeout 或者 setInterval 來定時檢測 DOM 是否有改變。無疑這種方式實時性很差,效率還低效。
- 2000年的時候引入了Mutation Event,Mutation Event採用了觀察者的設計模式,當DOM有變更時當即出發相應的事件。此方式屬於同步回調。雖然這種方式解決了實時性問題,可是由於會產生較大性能開銷、致使頁面性能出現問題,被反對使用並逐步從web標準事件中刪除。
- MutationObserver替代MutationEvent,相較於Event方式,Observer採用了一次觸發異步回調。且採用微任務的處理,使得實時性與性能功能都獲得有效提升。
微任務的另外一個應用:Promise。 本節簡單介紹JavaScript引入Promise的動機,以及解決問題的幾個核心關鍵點。 講到動機,也就是說Promise解決了什麼問題。衆所周知,他解決的是異步編碼風格的問題。
頁面編程的一大特色就是:異步編程,下面分析異步編程的代碼風格進化。
- 以前的代碼編碼風格,一段代碼可能會出現五次回調,這種回調致使代碼邏輯不連貫、不連線,不符合人的直覺。
- 而後開發人員們經過封裝異步代碼,讓處理流程變得線性,可是這種處理方式若是嵌套了太多的回調函數就容易陷入回調地獄。
- 陷入回調地獄的後代碼看上去很亂主要是兩點:嵌套調用和任務不肯定性(成功或者失敗)。因而Promise出現,解決了這兩個問題。
Promise經過兩步解決嵌套回調問題:
- 首先,Promise實現了回調函數的延時綁定(.then)
- 其次,將回調函數返回值穿透到最外層。
Promise處理異常: 經過最後一個catch,將全部對象合併到一個函數來處理以前的全部異常。
Promise 之因此要使用微任務是由 Promise 回調函數延遲綁定技術致使的。
當Promise解決回調地獄代碼風格的同時,咱們發現寫不少的then函數,仍是有些不太容易閱讀。 基於這個緣由,ES7引入了async/await,這是JavaScript異步編程的一個重大改進,提供了在不阻塞主線程的狀況下使用同步代碼實現異步訪問資源的能力。而且使得代碼邏輯更加清晰。
本節首先介紹生成器(Generator)是如何工做的,接着介紹了Generator的底層實現機制--協程。
這是由於async/await使用了Generator和Promise兩種技術。因此緊接着經過Generator和Promise來分析async/await究竟是如何經過以同步方式來編寫異步代碼的。
生成器函數:生成器函數是一個帶星號函數,並且是能夠暫停執行和恢復執行的。 具體使用方式就是:在生成器函數內部執行一段代碼,若遇到yiled關鍵字,那JS引擎將返回該關鍵字後面的內容且暫停該函數執行,外部函數經過next方法恢復函數的執行。
那麼JavaScript引擎V8是如何實現一個函數的暫停和恢復的?
搞懂它的暫停和恢復,須要首先了解協程的概念。協程是一種比線程更加輕量級的存在。能夠把協程看做是跑在線程上的任務,一個線程能夠存在多個協程。但在線程上同時只能執行一個協程。
在JS中,生成器就是協程的一種實現方式。
爲了更近一步改進生成器代碼,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。