一文了解你不知道的JavaScript異步篇
事件循環(huán)
先通過一段偽代碼了解一下事件循環(huán)這個(gè)概念
//eventLoop是一個(gè)用作隊(duì)列的數(shù)組 var eventLoop = [] var event; //永遠(yuǎn)執(zhí)行 while(true){ if(eventLoop.length>0){} //拿到隊(duì)列中的下一個(gè)事件 event = eventLoop.shift(); //現(xiàn)在,執(zhí)行下一個(gè)事件 try{ event() } catch(err){ reportError(err) } }
這是一段繼續(xù)簡化的代碼,你可以看到有一個(gè)用while循環(huán)實(shí)現(xiàn)的持續(xù)運(yùn)行的循環(huán),循環(huán)的每一輪稱為一個(gè)tick。對每個(gè)tick而言,如果在隊(duì)列中有等待事件,就會(huì)從隊(duì)列中摘下一個(gè)事件并執(zhí)行,這些事件就是所謂的回調(diào)函數(shù)。
一定要清楚,setTimeout()并沒有把你的回調(diào)函數(shù)掛在事件循環(huán)隊(duì)列中。他所做的是設(shè)定一個(gè)定時(shí)器。當(dāng)定時(shí)器到時(shí)后,環(huán)境會(huì)把你的回調(diào)函數(shù)放在事件循環(huán)中,這樣,在未來某個(gè)時(shí)刻的tick會(huì)摘下并執(zhí)行這個(gè)回調(diào)。所以這也是為什么setTimeout時(shí)間精度可能不太高,它只能確保你的回調(diào)函數(shù)不會(huì)在指定的時(shí)間間隔之前運(yùn)行,但可能會(huì)在事件循環(huán)隊(duì)列中20個(gè)項(xiàng)目后才執(zhí)行。取決于你事件隊(duì)列的項(xiàng)目與狀態(tài),畢竟JavaScript一次只能處理一個(gè)事件。這也引出了另一個(gè)概念“并發(fā)”。
當(dāng)兩個(gè)或多個(gè)“進(jìn)程”同時(shí)執(zhí)行就出現(xiàn)了并發(fā),也許瀏覽器會(huì)發(fā)出很多請求,當(dāng)發(fā)出第二個(gè)請求時(shí),第一個(gè)請求返回響應(yīng),當(dāng)發(fā)出第三個(gè)請求時(shí),第二個(gè)請求返回響應(yīng)。這里請求2與響應(yīng)1并發(fā)運(yùn)行,請求3與響應(yīng)2并發(fā)運(yùn)行,但是他們的各個(gè)事件是在事件循環(huán)隊(duì)列中依次運(yùn)行的。
更常見一點(diǎn)的情況是,并發(fā)的“進(jìn)程”需要相互交流,如果出現(xiàn)這樣的交互,就需要對他們的交互進(jìn)行協(xié)作以避免競態(tài)的出現(xiàn)。下面是兩個(gè)并發(fā)的進(jìn)程通過隱含的順序相互影響,這個(gè)順序有時(shí)會(huì)被破壞:
var res = []; function response(data){ res.push(data) } ajax("http://url1",response); ajax("http://url2",response);
這里的兩個(gè)ajax去調(diào)用response函數(shù),但不確定哪一會(huì)先執(zhí)行完成,這種不確定性很有可能就是一個(gè)競態(tài)條件bug。
在es6中,有一個(gè)新的概念建立在事件循環(huán)隊(duì)列之上,解決這種不確定性的執(zhí)行,叫做任務(wù)隊(duì)列。
任務(wù)隊(duì)列
對任務(wù)隊(duì)列最好的理解方式就是,它是掛在事件循環(huán)隊(duì)列的每個(gè)tick之后的一個(gè)隊(duì)列。在事件循環(huán)的每個(gè)tick中??赡艹霈F(xiàn)的異步動(dòng)作不會(huì)導(dǎo)致一個(gè)完整的新事件添加到事件循環(huán)隊(duì)列中,而會(huì)在當(dāng)前tick的任務(wù)隊(duì)列末尾添加一個(gè)任務(wù)。
任務(wù)隊(duì)列意思是:哦?原來這里還有一件事情要做,但要在任何事情發(fā)生之前就完成它,立刻接著執(zhí)行它。
而事件循環(huán)類似于做完這件事情,需要重新到隊(duì)尾排隊(duì)才能再做這件事情。
console.log("A") setTimeout(()=>{ console.log("B") },0) task(()=>{ console.log("C") task(()=>{ console.log('D') }) })
可能你認(rèn)為這里會(huì)打印出ABCD,但其實(shí)打印結(jié)果是ACDB,因?yàn)槎〞r(shí)器觸發(fā)是在所有同步事件隊(duì)列清空之后才開始執(zhí)行的。
回調(diào)
到目前位置,回調(diào)是編寫和處理JavaScript程序異步邏輯的最常用的方式,也是最基礎(chǔ)的異步模式。
我們的大腦可以看作類似于單線程運(yùn)行的事件循環(huán)隊(duì)列,就像JavaScript引擎那樣。用正在寫博客的我寫作進(jìn)行類比。此刻我心里就是計(jì)劃寫啊寫一直寫,一次完成我腦海中已經(jīng)按順序排好的一系列要點(diǎn)。我沒有將任何終端或非線性的行為納入到我的寫作計(jì)劃中。然而盡管如此,實(shí)際上我的大腦還是在不停的切換狀態(tài)。即使我的大腦在以異步事件方式運(yùn)行,但我的寫作還是以順序、同步的進(jìn)行,“先寫這里,再寫那里”。
所以,如果說同步的大腦計(jì)劃能夠很好地映射到同步代碼語句,那么我們大腦在規(guī)劃異步方面又是怎樣的呢?
答案是回調(diào)。即使在腦海中有許多事件出現(xiàn),如果真的想到什么就做什么去那恐怕我這篇博客也無法完成。但在實(shí)際執(zhí)行方面,我的大腦就是這樣運(yùn)作了。不是多任務(wù),而是快速的上下文切換。
嵌套回調(diào)
listen("click",()=>{ setTimeout(()=>{ setTimeout(()=>{ ajax("http:url",()=>{ console.log("響應(yīng)結(jié)果") }) },1000) },1000) })
你可能非常熟悉這樣的代碼,好幾個(gè)函數(shù)嵌套在一起構(gòu)成的鏈,這種代碼常常被稱為回調(diào)地獄問題,在大型項(xiàng)目中他引起的問題要比這些嚴(yán)重得多。為了避免回調(diào)地獄問題,產(chǎn)生了偉大的promise。
promise
在promise中,傳入的函數(shù)會(huì)立刻執(zhí)行,它有兩個(gè)參數(shù),在本例中我們將其分別稱為resolve和reject。前者代表完成,后者代表拒絕。
new Promise((resolve,reject)=>{ //最終調(diào)用resolve還是reject }).then( function(){ console.log("then") } )
promise調(diào)度技巧
如果兩個(gè)promise都已經(jīng)決議,那么p1.then和p2.then應(yīng)該最終會(huì)先調(diào)用p1的回調(diào),然后是p2的哪些,但還有一些可能微妙的場景:
p.then(function(){ p.then(function(){ console.log("C") }) console.log("A") }) p.then(function(){ console.log("B") })
這里的輸出結(jié)果是 A B C
一個(gè)promise決議后,這個(gè)promise上所有的通過then注冊的回調(diào)都會(huì)在下一個(gè)異步時(shí)機(jī)點(diǎn)上一次調(diào)用。所以在這里'C'無法搶占或打斷‘B’,因?yàn)檫@是promise的運(yùn)作方式。
錯(cuò)誤處理
對于大多數(shù)開發(fā)者來說,最自然的錯(cuò)誤處理就是try...catch結(jié)構(gòu),遺憾的是它只能是同步的,無法用于異步代碼模式。
function(){ setTimeout(()=>{ bar() },1000) } try{ foo() }catch(err){ //永遠(yuǎn)不會(huì)到達(dá)這里 }
try...catch當(dāng)然很好,但是無法跨越異步操作工作,所以catch無法攔截定時(shí)器內(nèi)異步的錯(cuò)誤。
方法1. 可以在then(resolve,reject)中第二個(gè)回調(diào)內(nèi)處理錯(cuò)誤,可以throw傳遞一個(gè)error。
方法2. finally捕獲
不管promise最后的狀態(tài),在執(zhí)行完then或catch指定的回調(diào)函數(shù)之后,都會(huì)執(zhí)行finally方法指定的回調(diào)函數(shù)。
function fn(val){ return new Promise((resolve,reject)=>{ if(val){ resolve({name:"111"}) }else{ reject("404") } }) } //執(zhí)行函數(shù) fn(true) .then(data=>{ console.log(data) //打印name:111鍵值對 return fn(false) }) .catch(e=>{ console.log(e) //打印404 return fn(false) }) .finally(()=>{ console.log("finally") //會(huì)打印的! })
promise.all([...])
假如你想同時(shí)發(fā)送兩個(gè)請求,等他們不管以什么順序完成之后再發(fā)送第三個(gè)請求
Promise.all([p1,p2]) .then(function(){ return request("http:url3") }) .then(function(msg){ console.log(msg) })
Promise.all需要一個(gè)參數(shù),是一個(gè)數(shù)組,通常由promise實(shí)例組成,從promise.all調(diào)用返回的promise會(huì)收到一個(gè)完整消息(msg)。這是一個(gè)由數(shù)組完成后傳入的消息,與順序無關(guān)。
另外,當(dāng)數(shù)組內(nèi)有且僅有所有成員promise都完成后才算完成。如果這些promise有任何一個(gè)被拒絕那all就會(huì)立刻被拒絕,并丟棄已經(jīng)成功的來自其他數(shù)組成員的promise結(jié)果。
promise.race([...])
盡管promise.all協(xié)調(diào)多個(gè)并發(fā)promise的運(yùn)行,并假定所有都需要完成,但有時(shí)候你會(huì)只想響應(yīng)第一個(gè)完成promise的結(jié)果,并直接拋棄其他promise。
這種在promise中被稱之為競態(tài)。
promise.race()也接受一個(gè)數(shù)組做參數(shù)。這個(gè)數(shù)組有一個(gè)/多個(gè)promise組成。一旦有任何一個(gè)promise為成功resolve,promise.race()就會(huì)完成;一旦有任何一個(gè)為reject被拒絕,它就會(huì)拒絕。
(如果你傳入一個(gè)空數(shù)組,那race永遠(yuǎn)不會(huì)resolve,永遠(yuǎn)不要傳遞空數(shù)組)
Promise.race([p1,p2]) .then(function(){ //p1和p2其中之一會(huì)完成這場競賽,突破重圍 return request("http:url3") }) .then(function(msg){ console.log(msg) })
因?yàn)橹挥幸粋€(gè)promise能夠取勝,所以完成值是單個(gè)消息,而不是像all一樣是一個(gè)數(shù)組。
all和race的變體
· none([...])
這個(gè)模式類似于all([...]),不過完成和拒絕的情況互換了,而是所有的promise都要被拒絕,即拒絕轉(zhuǎn)化為完成值。
· any([...])
這個(gè)模式與all([...])類似,但是會(huì)忽略拒絕,所以只需要完成一個(gè)而不是全部。
· first([...])
這個(gè)模式類似于與any([...])的競爭,即只要第一個(gè)promise完成,就會(huì)忽略后續(xù)的任何完成和拒絕。
· last([...])
這個(gè)模式類似于first([...]),但卻是只有最后一個(gè)完成勝出。
無法取消的promise
一旦創(chuàng)建了一個(gè)promise并為其注冊了完成/拒絕的處理函數(shù),如果出現(xiàn)某種情況使得這個(gè)任務(wù)懸而未決的話,你也沒有辦法從外部停止它的進(jìn)程。
以上就是一文了解你不知道的JavaScript異步篇的詳細(xì)內(nèi)容,更多關(guān)于JavaScript異步的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript中數(shù)字轉(zhuǎn)字符串的6種方式以及性能比較
在JavaScript中將字符串轉(zhuǎn)換為數(shù)字有多種方法,下面這篇文章主要給大家介紹了關(guān)于JavaScript中數(shù)字轉(zhuǎn)字符串的6種方式以及性能比較的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-04-04微信小程序用戶授權(quán)環(huán)節(jié)實(shí)現(xiàn)過程
這篇文章主要介紹了微信小程序用戶授權(quán)環(huán)節(jié)實(shí)現(xiàn)過程,在商城項(xiàng)目中,我們需要對部分的頁面,進(jìn)行一個(gè)授權(quán)的判別,例如購物車,及個(gè)人中心,需要完成用戶信息的授權(quán)后,獲取到相關(guān)信息2023-01-01對Layer彈窗使用及返回?cái)?shù)據(jù)接收的實(shí)例詳解
今天小編就為大家分享一篇對Layer彈窗使用及返回?cái)?shù)據(jù)接收的實(shí)例詳解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09JavaScript實(shí)現(xiàn)頁面跳轉(zhuǎn)的八種方式
這篇文章介紹了JavaScript實(shí)現(xiàn)頁面跳轉(zhuǎn)的八種方式,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06JS設(shè)計(jì)模式之狀態(tài)模式的用法使用方法
JavaScript狀態(tài)模式是一種行為型設(shè)計(jì)模式,核心是對象在其內(nèi)部狀態(tài)改變時(shí)改變其行為,狀態(tài)模式將對象的行為封裝到不同的狀態(tài)類中,使得對象在不同狀態(tài)下可以選擇不同的行為,本文給大家詳細(xì)的介紹一下狀態(tài)設(shè)計(jì)模式在Js中的使用,需要的朋友可以參考下2023-08-08javascript實(shí)現(xiàn)自由編輯圖片代碼詳解
這篇文章主要介紹了javascript實(shí)現(xiàn)自由編輯圖片代碼詳解,在當(dāng)下的的前端項(xiàng)目中,圖片功能可以說是非常常見的,圖片的展示、圖片的裁剪編輯、圖片的上傳等,那么我們的項(xiàng)目便來了個(gè)需求。,需要的朋友可以參考下2019-06-06