摘要:如果在瀏覽器中線程阻塞了,瀏覽器可能會(huì)失去響應(yīng),從而造成不好的用戶體驗(yàn)。中也有可能會(huì)產(chǎn)生新的,會(huì)進(jìn)入尾部,并在本次前執(zhí)行。這就是所謂的,而把回調(diào)函數(shù)的嵌套邏輯替換成了符合正常人思維習(xí)慣的線性邏輯。
JS本身是一門單線程的語言,所以在執(zhí)行一些需要等待的任務(wù)(eg.等待服務(wù)器響應(yīng),等待用戶輸入等)時(shí)就會(huì)阻塞其他代碼。如果在瀏覽器中JS線程阻塞了,瀏覽器可能會(huì)失去響應(yīng),從而造成不好的用戶體驗(yàn)。幸運(yùn)的是JS語言本身和其運(yùn)行的環(huán)境(瀏覽器,Node)都提供了一些解決方案讓JS可以“異步”起來,在此梳理一下相關(guān)的知識(shí)點(diǎn),如果你讀完之后有所收獲,那更是極好的。
Event LoopJS中每個(gè)函數(shù)都伴有一個(gè)自身的作用域(execution context),這個(gè)作用域包含函數(shù)的一些信息(eg.參數(shù),局部變量等),在函數(shù)被調(diào)用時(shí),函數(shù)的作用域?qū)ο蟊煌迫雸?zhí)行棧(execution context stack),執(zhí)行完畢后出棧。當(dāng)執(zhí)行一些異步任務(wù)時(shí),JS僅調(diào)用相應(yīng)的API并不去等待任務(wù)結(jié)果而是繼續(xù)執(zhí)行后續(xù)代碼,這些異步任務(wù)被瀏覽器或者Node交由其他線程執(zhí)行(eg.定時(shí)器線程、http請(qǐng)求線程、DOM事件線程等),完成之后這些異步任務(wù)的回調(diào)函數(shù)會(huì)被推入相應(yīng)的隊(duì)列中,直到執(zhí)行棧為空時(shí),這些回調(diào)函數(shù)才會(huì)被依次執(zhí)行。
舉個(gè)例子:
function main() { console.log("A) setTimeout(function display() { console.log("B") }, 0) console.log("C") } main()
以上代碼在Event Loop中的執(zhí)行過程如下:
類似于setTimeout這樣的任務(wù)還有:setInterval, setImmediate, 響應(yīng)用戶操作的事件(eg. click, input等), 響應(yīng)網(wǎng)絡(luò)請(qǐng)求(eg. ajax的onload,image的onload等),數(shù)據(jù)庫操作等等。這些操作有一個(gè)統(tǒng)一的名字:task,所以上圖中的message queue其實(shí)是task queue,因?yàn)檫€存在一些像:Promise,process.nextTick, MutationObserver之類的任務(wù),這些任務(wù)叫做microtask,__microtask會(huì)在代碼執(zhí)行過程中被推入microtask queue而不是task queue__,microtask queue中的任務(wù)同樣也需要等待執(zhí)行棧為空時(shí)依次執(zhí)行。
一個(gè)task中可能會(huì)產(chǎn)生microtask和新的task,其中產(chǎn)生的microtask會(huì)在本次task結(jié)束后,即執(zhí)行棧為空時(shí)執(zhí)行,而新的task則會(huì)在render之后執(zhí)行。microtask中也有可能會(huì)產(chǎn)生新的microtask,會(huì)進(jìn)入microtask queue尾部,并在本次render前執(zhí)行。
這樣的流程是有它存在原因的,這里僅僅談下我個(gè)人的理解,如有錯(cuò)誤,還請(qǐng)指出:
瀏覽器中除了JS引擎線程,還存在GUI渲染線程,用以解析HTML, CSS, 構(gòu)建DOM樹等工作,然而這兩個(gè)線程是互斥的,只有在JS引擎線程空閑時(shí),GUI渲染線程才有可能執(zhí)行。在兩個(gè)task之間,JS引擎空閑,此時(shí)如果GUI渲染隊(duì)列不為空,瀏覽器就會(huì)切換至GUI渲染線程進(jìn)行render工作。而microtask會(huì)在render之前執(zhí)行,旨在以類似同步的方式(盡可能快地)執(zhí)行異步任務(wù),所以microtask執(zhí)行時(shí)間過長(zhǎng)就會(huì)阻塞頁面的渲染。
上文提到setTimeout,setInterval都屬于task,所以即便設(shè)置間隔為0:
setTimeout(function display() { console.log("B") }, 0)
回調(diào)也會(huì)異步執(zhí)行。
setTimeout,setInterval常被用于編寫JS動(dòng)畫,比如:
// setInterval function draw() { // ...some draw code } var intervalTimer = setInterval(draw, 500) // setTimeout var timeoutTimer = null function move() { // ...some move code timeoutTimer = setTimeout(move, 500) } move()
這其實(shí)是存在一定的問題的:
從event loop的角度分析:setInterval的兩次回調(diào)之間的間隔是不確定的,取決于回調(diào)中的代碼的執(zhí)行時(shí)間;
從性能的角度分析:無論是setInterval還是setTimeout都“無法感知瀏覽器當(dāng)前的工作狀態(tài)”,比如當(dāng)前頁面為隱藏tab,或者設(shè)置動(dòng)畫的元素不在當(dāng)前viewport,setInterval & setTimeout仍會(huì)照常執(zhí)行,實(shí)際是沒有必要的,雖然某些瀏覽器像Chrome會(huì)優(yōu)化這種情況,但不能保證所有的瀏覽器都會(huì)有優(yōu)化措施。再比如多個(gè)元素同時(shí)執(zhí)行不同的動(dòng)畫,可能會(huì)造成不必要的重繪,其實(shí)頁面只需要重繪一次即可。
在這種背景下,Mozilla提出了requestAnimationFrame,后被Webkit優(yōu)化并采用,requestAnimationFrame為編寫JS動(dòng)畫提供了原生API。
function draw() { // ...some draw code requestAnimationFrame(draw) } draw()
requestAnimationFrame為JS動(dòng)畫做了一些優(yōu)化:
大多數(shù)屏幕的最高幀率是60fps,requestAnimationFrame默認(rèn)會(huì)盡可能地達(dá)到這一幀率
元素不在當(dāng)前viewport時(shí),requestAnimationFrame會(huì)極大地限制動(dòng)畫的幀率以節(jié)約系統(tǒng)資源
使用requestAnimationFrame定義多個(gè)同時(shí)段的動(dòng)畫,頁面只會(huì)產(chǎn)生一次重繪。
當(dāng)然requestAnimationFrame存在一定的兼容性問題,具體可參考 can i use。
Promisefs.readdir(source, function (err, files) { if (err) { console.log("Error finding files: " + err) } else { files.forEach(function (filename, fileIndex) { console.log(filename) gm(source + filename).size(function (err, values) { if (err) { console.log("Error identifying file size: " + err) } else { console.log(filename + " : " + values) aspect = (values.width / values.height) widths.forEach(function (width, widthIndex) { height = Math.round(width / aspect) console.log("resizing " + filename + "to " + height + "x" + height) this.resize(width, height).write(dest + "w" + width + "_" + filename, function(err) { if (err) console.log("Error writing file: " + err) }) }.bind(this)) } }) }) } })
假設(shè)最初學(xué)JS時(shí)我看到的是上面的代碼,我一定不會(huì)想寫前端。這就是所謂的“callback hell”,而Promise把回調(diào)函數(shù)的嵌套邏輯替換成了符合正常人思維習(xí)慣的線性邏輯。
function fetchSomething() { return new Promise(function(resolved) { if (success) { resolved(res); } }); } fetchSomething().then(function(res) { console.log(res); return fetchSomething(); }).then(function(res) { console.log("duplicate res"); return "done"; }).then(function(tip) { console.log(tip); })async await
async await是ES2017引入的兩個(gè)關(guān)鍵字,旨在讓開發(fā)者更方便地編寫異步代碼,可是往往能看到類似這樣的代碼:
async function orderFood() { const pizzaData = await getPizzaData() // async call const drinkData = await getDrinkData() // async call const chosenPizza = choosePizza() // sync call const chosenDrink = chooseDrink() // sync call await addPizzaToCart(chosenPizza) // async call await addDrinkToCart(chosenDrink) // async call orderItems() // async call }
Promise的引入讓我們脫離了“callback hell”,可是對(duì)async函數(shù)的錯(cuò)誤用法又讓我們陷入了“async hell”。
這里其實(shí)getPizzaData和getDrinkData是沒有關(guān)聯(lián)的,而await關(guān)鍵字使得必須在getPizzaData resolve之后才能執(zhí)行g(shù)etDrinkData的動(dòng)作,這顯然是冗余的,包括addPizzaToCart和addDrinkToCart也是一樣,影響了系統(tǒng)的性能。所以在寫async函數(shù)時(shí),應(yīng)該清楚哪些代碼是相互依賴的,把這些代碼多帶帶抽成async函數(shù),另外Promise在聲明時(shí)就已經(jīng)執(zhí)行,提前執(zhí)行這些抽出來的async函數(shù),再await其結(jié)果就能避免“async hell”,或者也可以用Promise.all():
async function selectPizza() { const pizzaData = await getPizzaData() // async call const chosenPizza = choosePizza() // sync call await addPizzaToCart(chosenPizza) // async call } async function selectDrink() { const drinkData = await getDrinkData() // async call const chosenDrink = chooseDrink() // sync call await addDrinkToCart(chosenDrink) // async call } // return promise early async function orderFood() { const pizzaPromise = selectPizza() const drinkPromise = selectDrink() await pizzaPromise await drinkPromise orderItems() // async call } // or promise.all() Promise.all([selectPizza(), selectDrink()]).then(orderItems) // async call參考文章 && 拓展閱讀
JavaScript Event Loop Explained
How to escape async/await hell
Tasks, microtasks, queues and schedules
requestAnimationFrame
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/109131.html
摘要:回調(diào)函數(shù),一般在同步情境下是最后執(zhí)行的,而在異步情境下有可能不執(zhí)行,因?yàn)槭录]有被觸發(fā)或者條件不滿足。同步方式請(qǐng)求異步同步請(qǐng)求當(dāng)請(qǐng)求開始發(fā)送時(shí),瀏覽器事件線程通知主線程,讓線程發(fā)送數(shù)據(jù)請(qǐng)求,主線程收到 一直以來都知道JavaScript是一門單線程語言,在筆試過程中不斷的遇到一些輸出結(jié)果的問題,考量的是對(duì)異步編程掌握情況。一般被問到異步的時(shí)候腦子里第一反應(yīng)就是Ajax,setTimse...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。寫一個(gè)符合規(guī)范并可配合使用的寫一個(gè)符合規(guī)范并可配合使用的理解的工作原理采用回調(diào)函數(shù)來處理異步編程。 JavaScript怎么使用循環(huán)代替(異步)遞歸 問題描述 在開發(fā)過程中,遇到一個(gè)需求:在系統(tǒng)初始化時(shí)通過http獲取一個(gè)第三方服務(wù)器端的列表,第三方服務(wù)器提供了一個(gè)接口,可通過...
摘要:到這里,我已經(jīng)發(fā)出了一個(gè)請(qǐng)求買漢堡,啟動(dòng)了一次交易。但是做漢堡需要時(shí)間,我不能馬上得到這個(gè)漢堡,收銀員給我一個(gè)收據(jù)來代替漢堡。到這里,收據(jù)就是一個(gè)承諾保證我最后能得到漢堡。 同期異步系列文章推薦談一談javascript異步j(luò)avascript異步中的回調(diào)javascript異步之Promise.all()、Promise.race()、Promise.finally()javascr...
摘要:調(diào)用棧被清空,消息隊(duì)列中并無任務(wù),線程停止,事件循環(huán)結(jié)束。不確定的時(shí)間點(diǎn)請(qǐng)求返回,將設(shè)定好的回調(diào)函數(shù)放入消息隊(duì)列。調(diào)用棧執(zhí)行完畢執(zhí)行消息隊(duì)列任務(wù)。請(qǐng)求并發(fā)回調(diào)函數(shù)執(zhí)行順序無法確定。 異步編程 JavaScript中異步編程問題可以說是基礎(chǔ)中的重點(diǎn),也是比較難理解的地方。首先要弄懂的是什么叫異步? 我們的代碼在執(zhí)行的時(shí)候是從上到下按順序執(zhí)行,一段代碼執(zhí)行了之后才會(huì)執(zhí)行下一段代碼,這種方式...
摘要:從今天開始研究一下的異步相關(guān)內(nèi)容,感興趣的請(qǐng)關(guān)注同期異步系列文章推薦異步中的回調(diào)異步與異步之異步之異步之和異步之一異步之二異步實(shí)戰(zhàn)異步總結(jié)歸檔什么是異步我們知道的單線程的,這與它的用途有關(guān)。 從今天開始研究一下javascript的異步相關(guān)內(nèi)容,感興趣的請(qǐng)關(guān)注 同期異步系列文章推薦javascript異步中的回調(diào)javascript異步與promisejavascript異步之Prom...
摘要:在異步機(jī)制中,任務(wù)隊(duì)列就是用來維護(hù)異步任務(wù)回調(diào)函數(shù)的隊(duì)列。四對(duì)象對(duì)象是工作組提出的一種規(guī)范,目的是為異步編程提供統(tǒng)一接口。 異步 1.JavaScript單線程的理解 Javascript語言的執(zhí)行環(huán)境是單線程(single thread)。所謂單線程,就是指一次只能完成一件任務(wù)。如果有多個(gè)任務(wù),就必須排隊(duì),前面一個(gè)任務(wù)完成,再執(zhí)行后面一個(gè)任務(wù),以此類推。 2.JavaScript單線...
閱讀 3168·2021-10-12 10:11
閱讀 1877·2021-08-16 10:59
閱讀 2886·2019-08-30 15:55
閱讀 1254·2019-08-30 14:19
閱讀 2064·2019-08-29 17:03
閱讀 2499·2019-08-29 16:28
閱讀 3246·2019-08-26 13:47
閱讀 2918·2019-08-26 13:36