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