消息隊列和事件循環(huán)
每一個渲染進程都有一個主線程,既要處理DOM擒滑,又要計算樣式鸠补,還要處理布局,同時還需要處理js任務以及各種輸入事件藐翎。要讓這么多不同類型的任務在主線程中有條不紊執(zhí)行材蹬,需要一個系統(tǒng)來統(tǒng)籌調度,這個系統(tǒng)就是消息隊列和事件循環(huán)系統(tǒng)
隊列:先進先出
- 一個消息隊列用來調度任務吝镣,接收其他線程的任務
- IO線程用來接收其他進程產生的任務
- 渲染主線程會循環(huán)的從消息隊列頭部中讀取任務堤器,執(zhí)行任務
消息隊列中的任務類型
- 輸入事件(鼠標滾動、點擊末贾、移動)
- 微任務
- 文件讀寫
- websocket
- js定時器
- js執(zhí)行闸溃,解析DOM,樣式計算拱撵,布局計算辉川,css動畫
宏任務微任務
消息隊列中的任務執(zhí)行過程中,會產生新的任務拴测,如果將這些新產生的任務添加到消息隊列尾部乓旗,那么這些任務會喪失實時性,如果任務直接出發(fā)集索,阻斷當前任務的執(zhí)行屿愚,又會影響當前任務的執(zhí)行效率
所以最好的時機是:當前任務執(zhí)行完,下個任務開始前务荆;
eg: dom節(jié)點的監(jiān)控妆距;
消息隊列中的任務稱為宏任務,每個宏任務都包含了一個微任務隊列函匕,在執(zhí)行宏任務的過程中娱据,產生的微任務會添加到列表中,當宏任務主要功能執(zhí)行完后盅惜,會執(zhí)行當前宏任務的微任務列表
宏任務
- 渲染事件(如解析DOM忌穿、計算布局、繪制)
- 用戶交互事件(如鼠標點擊伴网、滾動頁面、放大縮小等)
- JavaScript腳本執(zhí)行事件
- 網絡請求完成澡腾、文件讀寫完成事件
宏任務可以滿足我們大部分的日常需求,不過如果有對時間精度要求較高的要求糕珊,宏任務就難以勝任了。
頁面的渲染事件红选、各種IO的完成事件、執(zhí)行js腳本的事件喇肋、用戶交互的事件等都隨時有可能被添加到消息隊列中,而且添加事件是由系統(tǒng)操作的蝶防,js代碼不能準確掌控任務要添加到隊列中的位置甚侣,因此很難控制開始執(zhí)行任務的時間间学。
微任務
微任務就是一個需要異步執(zhí)行的函數,執(zhí)行時機是在主函數執(zhí)行結束之后低葫、當前宏任務結束之前
-
微任務的產生
使用MutationObserver監(jiān)控某個DOM節(jié)點,通過JS修改這個節(jié)點嘿悬,當DOM發(fā)生變化時,就會產生DOM變化記錄的微任務
Promise善涨,當調用Promise.resolve()或者reject時
-
微任務的執(zhí)行
宏任務快執(zhí)行完成時主到,也就在js引擎準備退出全局執(zhí)行上下文并清空調用棧的時候,js會檢查全局執(zhí)行上下文的微任務隊列
在執(zhí)行微任務的過程中躯概,產生了新的微任務,同樣會將該微任務添加到微任務隊列中畔师,v8引擎一直循環(huán)執(zhí)行微任務隊列中的任務娶靡,直到隊列為空才算執(zhí)行結束
- 微任務和宏任務是綁定的,每個宏任務執(zhí)行時看锉,會創(chuàng)建自己的微任務隊列
- 微任務的執(zhí)行時長會影響到當前宏任務的時長姿锭。
- 在一個宏任務中塔鳍,分別創(chuàng)建一個用于回調的宏任務和微任務,無論什么情況呻此,微任務都早于宏任務執(zhí)行
Promise
單線程架構決定了 web 頁面的異步回調轮纫,而多次回調會導致代碼的邏輯不連貫、不線性焚鲜。Promise 封裝異步代碼掌唾,讓處理流程變得線性
我們來看一下 Promise 的常用用法
let b = function(resolve, reject) {
let xhr = new XMLHttpRequest()
xhr.open('get', 'url', true)
xhr.ontimeout = function(e) {
reject(e)
}
xhr.onreadystatechange = function() {
resolve(this.responseText, this)
}
}
let a = new Promise(b)
a.then(res => {}).catch(e => {})
觀察上面代碼,可以發(fā)現:
- 構建 Promise 對象時忿磅,需要傳入一個 b 函數糯彬,業(yè)務流程都在 b 函數中執(zhí)行
- 如果運行 b 函數中的業(yè)務執(zhí)行成功了,會調用 resolve 函數葱她;如果執(zhí)行失敗撩扒,則調用 reject 函數
- 在 b 函數中調用 resolve 函數時,會觸發(fā) Promise.then 中設置的回調函數;調用 reject 函數時吨些,會觸發(fā) Promise.catch 設置的回調函數
首先搓谆,Promise 實現了回調函數的延時綁定
其在代碼上的提現就是先創(chuàng)建了 Promise,執(zhí)行業(yè)務邏輯豪墅;再使用 then 來設置回調函數泉手。resolve 函數會觸發(fā)設置的回調函數
function b(resolve, reject) {
resolve(100)
}
let a = new Promise(b)
function onResolve(value) {
console.log(value)
}
a.then(onResolve)
其次,需要將回調函數 onResolve 的返回值穿透到最外層
function resolve(value) {
let a2 = new Promise((resolve, reject) => {
resolve(value)
})
return a2 //then的返回值也是一個promise
}
這樣就可以實現透傳:a.then().then(value);實現透傳之后但校,Promise 對象的錯誤就具有冒泡性質螃诅,會一直向后傳遞,知道被 catch
我們來實現一個簡易版 Promise
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'
function MyPromise(fn) {
this.state = PENDING
this.value = null
this.resolveCallbacks = []
this.rejectCallbacks = []
const that = this
function resolve() {
// 這個地方實際用的是微任務状囱,目的是讓then中的回調先執(zhí)行
setTimeout(function() {
if (that.state === PENDING) {
that.resolveCallbacks.forEach(item => {
item(that.value)
})
}
}, 0)
}
function reject() {
同上
}
try {
fn(resolve, reject)
} catch (e) {}
}
// 簡易版
MyPromise.prototype.then = function(cb) {
const that = this
// 簡易的透傳术裸,cb不存在時
cb = typeof cb === 'function' ? cb : v => v
if (that.state === PENDING) {
that.resolvedCallbacks.push(cb)
}
if (that.state === RESOLVED) {
cb(that.value)
}
}
// then函數返回Promise
MyPromise.prototype.then = function(cb) {
const that = this
if ((that.state = PENDING)) {
return new MyPromise((resolve, reject) => {
that.resolvedCallbacks.push(() => {
const x = cb(that.value) //此時執(zhí)行了then1的回調
resolve(x) //返回上一步的返回值
})
})
}
}