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