什麼是事件循環和消息隊列?c++
頁面中的大部分任務——包括渲染事件、用戶交互事件、JavaScript 腳本執行事件、網絡請求完成和文件讀寫完成事件等——都是在渲染進程的主線程上執行的,爲了協調這些任務有條不紊地在主線程上執行,渲染進程引入了消息隊列和事件循環機制。渲染進程內部會維護多個消息隊列,好比延遲執行隊列和普通地消息隊列,而後主線程採用一個 for 循環,不斷地從這些任務隊列中取出任務並執行任務。json
C++代碼模擬事件循環和消息隊列跨域
// 隊列
class TaskQueue {
public:
Task takeTask(); // 取出隊列頭部的一個任務
void pushTask(Task task); // 添加一個任務到隊列尾部
}
TaskQueue task_queue;
void ProcessTask(); // 執行任務
bool keep_running = true;
void MainThread() {
for(;;) {
Task task = task_queue.takeTask(); // 從消息隊列中讀取一個任務
ProcessTask(task);
if(!keep_running) // 若是設置了退出標誌,呢麼直接退出線程循環
break;
}
}
Task clickTask;
task_queue.pushTask(clickTask); // 添加一個任務到消息隊列中
複製代碼
消息隊列中的任務類型瀏覽器
頁面使用單線程的缺點安全
消息隊列有「先進先出」的特色,放入消息隊列中的任務,須要等前面的任務被執行完,纔會被執行。因此要解決如下兩個問題:網絡
(1) 如何處理高優先級的任務異步
(2) 如何解決單個任務執行時長太久的問題函數
setTimeout
方法是什麼?工具
setTimeout
方法是一個定時器,用來指定某個函數在多少毫秒後執行。返回一個整數,表示定時器的編號,能夠經過該編號來取消這個定時器。佈局
瀏覽器怎麼實現 setTimeout ?
定時器設置的回調函數須要在指定的時間間隔內被調用,但消息隊列中的任務是按照順序執行的,因此爲了保障回調函數能在指定時間內執行,Chrome 中除了正常使用的消息隊列以外,還有另一個消息隊列,這個隊列中維護了須要延遲執行的任務列表。setTimeout 任務就被添加到延遲執行隊列中。
C++ 模擬實現延遲隊列
DelayedIncomingQueue delayed_incoming_queue; // 源碼中延遲隊列的定義
// 模擬實現一個回調任務
struct DelayTask {
int64 id;
CallBackFunction cbf;
int start_time;
int delay_time;
};
DelayTask timerTask;
timerTask.cbf = showName;
timerTask.start_time = getCurrentTime(); // 獲取當前時間
timerTask.delay_time = 200; // 設置延遲時間
delayed_incoming_queue.push(timerTask); // 將回調任務添加到延遲執行隊列中
複製代碼
完善事件循環的代碼
void ProcessDelayTask() {
// 從delayed_incoming_queue中取出已經到期的定時器任務
// 依次執行這些任務
}
TaskQueue task_queue;
void ProcessTask(); // 執行任務
bool keep_running = true;
void MainThread() {
for(;;) {
// 執行消息隊列中的任務
Task task = task_queue.takeTask();
ProcessTask(task);
// 執行延遲隊列中的任務
ProcessDelayTask();
if(!keep_running) // 若是設置了退出標誌,那麼直接退出線程循環
break;
}
}
複製代碼
每處理完消息隊列中的一個任務以後,就開始執行延遲隊列中到期的任務。等到期的任務執行完成以後,再繼續下一個循環過程。這裏的延遲隊列其實是一個 hashmap 結構。
取消定時器
調用clearTimeout
函數,傳入須要取消的定時器的 ID。瀏覽器內部實現取消定時器的操做是直接從延遲隊列delayed_incoming_queue
中經過 ID 查找到對應的任務,而後將其從隊列中刪除。
使用 setTimeout 的一些注意事項
(1) 若是當前任務執行太久,會影響延遲到期定時器任務的執行。
(2) 若是 setTimeout 存在嵌套調用,那麼系統會設置最短期間隔爲 4 毫秒。
(3) 未激活的頁面,setTimeout 執行最小間隔是 1000 毫秒,目的是爲了優化後臺頁面的加載損耗以及下降耗電量。
(4) 延遲執行時間有最大值。Chrome、Safari、Firefox 都是以32 個 bit來存儲延時值的,延遲值大於 32 bit 能夠存放的最大數字時會溢出,致使定時器會被當即執行。
(5) 使用 setTimeout 設置的回調函數中的 this 不符合直覺。
var name = 1;
var myObj = {
name: 2,
showName: function() {
console.log(this.name);
}
}
setTimeout(myObj.showName, 1000); // 延遲1秒執行,結果是:1
setTimeout(myObj.showName(), 1000); // 當即執行,結果是:2
setTimeout(function() {
myObj.showName(); // 延遲1秒執行,結果是:2
}, 1000);
setTimeout(() => {
myObj.showName(); // 延遲1秒執行,結果是:2
}, 1000);
setTimeout(myObj.showName.bind(myObj), 1000); // 延遲1秒執行,結果是2
複製代碼
用requestAnimationFrame
實現的動畫效果比setTimeout
好的緣由是什麼?
(1) setTimeout 經過設置一個間隔時間來不斷改變圖像的位置,從而達到動畫效果。可是用 setTimeout 實現的動畫可能會出現卡頓、抖動的現象。有兩個緣由:
這兩種狀況致使 setTimeout 的執行步調和屏幕的刷新步調不一致,從而引發丟幀現象,致使動畫卡頓。
(2) requestAnimationFrame 是由系統來決定回調函數的執行時機的,它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象。
除此以外,requestAnimationFrame 還有CPU節能和函數節流的優點。由於頁面被隱藏或最小化時,requestAnimationFrame 會中止渲染,但 setTimeout 還會在後臺繼續執行動畫任務。在高頻率事件如 resize 和 scroll 中,requestAnimationFrame 能夠保證在每一個刷新間隔內,函數只被執行一次。
系統調用棧
消息隊列和主線程循環機制保證了頁面有條不紊地執行。當循環系統在執行一個任務的時候,都要爲這個任務維護一個系統調用棧。這個系統調用棧相似於 JavaScript 的調用棧,只不過是用 C++ 語言來維護的。能夠經過 Chrome 開發者工具的 Performance 抓取核心調用信息。
什麼是回調函數?
將一個函數做爲參數傳遞給另一個函數,做爲參數的這個函數就是回調函數。
XMLHttpRequest 運做機制
(1) 建立 XMLHttpRequest 對象;
(2) 爲 xhr 對象註冊回調函數:ontimeout
、onerror
、onreadystatechange
;
(3) 打開請求:open()
;
(4) 配置基礎的請求信息;
(5) 發起請求
XMLHttpRequest 示例代碼
function getDataByXhr(url) {
// 1.新建 XMLHttpRequest 請求對象
let xhr = new XMLHttpRequest()
// 2.註冊事件回調函數
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case 0: // 請求未初始化。還沒有調用open()方法
console.log('請求未初始化');
break;
case 1: // 請求已啓動。已經調用open()方法,但還沒有調用send()方法
console.log('OPENED');
break;
case 2: // 請求已發送。已經調用send()方法,但還沒有接收到響應
console.log('HEADERS_RECEIVED');
break;
case 3: // 正在接收。已經接收到部分響應數據
console.log('LOADING');
break;
case 4: // 請求完成。已經接收到所有響應數據
if (xhr.status == 200 || xhr.status == 304) {
console.log(xhr.responseText);
}
console.log('DONE')
break;
}
}
xhr.ontimeout = function(e) { console.log('timeout', e) }
xhr.onerror = function(e) { console.log('error', e) }
// 3.打開請求
xhr.open('GET', url, true); // open()方法的第三個參數設置爲true,表示異步請求
// 4.配置參數
xhr.timeout = 3000 // 設置請求的超時時間
xhr.responseType = 'json' // 設置響應返回的數據格式
// xhr.setRequestHeader()
// 5.發送請求
xhr.send();
}
複製代碼
XMLHttpRequest 使用過程當中可能遇到的問題
(1) 跨域問題
(2) HTTPS 混合內容的問題
setTimeout 和 XMLHttpRequest 工做機制的區別
關於消息隊列
WHATWG 規範定義了在主線程的循環系統中,能夠有多個消息隊列,好比鼠標事件隊列,IO 完成消息隊列,渲染任務隊列,而且能夠給這些消息隊列排優先級。但瀏覽器目前只實現了消息隊列和延遲執行隊列。
什麼是宏任務?
消息隊列中的任務稱爲宏任務。
宏任務的執行過程
爲何須要微任務?
由於 JavaScript 代碼不能掌控宏任務添加到隊列中的位置,難以控制開始執行任務的時間。對時間精度要求較高的需求,宏任務難以勝任,因此須要微任務。
什麼是微任務?
微任務是一個須要異步執行的函數,執行時機是在主函數執行結束以後、當前宏任務結束以前。
微任務是如何產生的?
產生微任務有兩種方式。
Promise.resolve()
或者Promise.reject()
的時候,也會產生微任務。執行微任務隊列的時機
微任務和宏任務
Mutation Event 和 MutationObserver 監聽 DOM 變化
(1) Mutation Event 採用觀察者模式監聽 DOM 變化。當 DOM 有變更時就馬上觸發相應的事件,這種方式屬於同步回調。可是這種實時性形成了嚴重的性能問題。
(2) MutationObserver 將事件的響應函數改爲異步調用,不是在每次 DOM 變化都觸發異步調用,而是等屢次 DOM 變化後,一次觸發異步調用。每次 DOM 節點發生變化的時候,渲染引擎將變化記錄封裝成微任務,並將微任務添加進當前的微任務隊列中。當執行到檢查點的時候,V8 引擎就會按順序執行這些微任務。
MutationObserver 經過異步調用和減小觸發次數解決同步操做的性能問題,經過微任務解決實時性問題。
參考: