摘要:本文從入手,系統(tǒng)的回顧的異步機制及發(fā)展歷程。需要提醒的是,文本沒有討論的異步機制。這就是之前提到的事件觸發(fā)線程。其實無論是請求還是定時器還是事件,我們都可以統(tǒng)稱它們?yōu)槭录?。第二階段,引擎線程專注于處理事件。將外元素的事件回調(diào)放入調(diào)用棧。
本文從EventLoop、Promise、Generator、asyncawait入手,系統(tǒng)的回顧JavaScript的異步機制及發(fā)展歷程。
需要提醒的是,文本沒有討論nodejs的異步機制。
本文是『horseshoe·Async專題』系列文章之一,后續(xù)會有更多專題推出
GitHub地址(持續(xù)更新):horseshoe
博客地址(文章排版真的很漂亮):matiji.cn
如果覺得對你有幫助,歡迎來GitHub點Star或者來我的博客親口告訴我
事件循環(huán)
也許我們都聽說過JavaScript是事件驅(qū)動的這種說法。各種異步任務(wù)通過事件的形式和主線程通信,保證網(wǎng)頁流暢的用戶體驗。而異步可以說是JavaScript最偉大的特性之一(也許沒有之一)。
現(xiàn)在我們就從Chrome瀏覽器的主要進程入手,深入的理解這個機制是如何運行的。
Chrome瀏覽器的主要進程我們看一下Chrome瀏覽器都有哪些主要進程。
Browser進程。這是瀏覽器的主進程。
第三方插件進程。
GPU進程。
Renderer進程。
大家都說Chrome瀏覽器是內(nèi)存怪獸,因為它的每一個頁面都是一個Renderer進程,其實這種說法是不對的。實際上,Chrome支持好幾種進程模型。
Process-per-site-instance。每打開一個網(wǎng)站,然后從這個網(wǎng)站鏈開的一系列網(wǎng)站都屬于一個進程。這也是Chrome的默認進程模型。
Process-per-site。同域名范疇的網(wǎng)站屬于一個進程。
Process-per-tab。每一個頁面都是一個獨立的進程。這就是外界盛傳的進程模型。
SingleProcess。傳統(tǒng)瀏覽器的單進程模型。
瀏覽器內(nèi)核現(xiàn)在我們知道,除了相關(guān)聯(lián)的頁面可能會合并為一個進程外,我們可以簡單的認為每個頁面都會開啟一個新的Renderer進程。那么這個進程里跑的程序又是什么呢?就是我們常常說的瀏覽器內(nèi)核,或者說渲染引擎。確切的說,是瀏覽器內(nèi)核的一個實例。Chrome瀏覽器的渲染引擎叫Blink。
由于瀏覽器主要是用來瀏覽網(wǎng)頁的,所以雖然Browser進程是瀏覽器的主進程,但它充當?shù)闹皇且粋€管家的角色,真正的一線業(yè)務(wù)大拿還得看Renderer進程。這也是跑在Renderer進程里的程序被稱為瀏覽器內(nèi)核(實例)的原因。
介紹Chrome瀏覽器的進程系統(tǒng)只是為了引出Renderer進程,接下來我們只需要關(guān)注瀏覽器內(nèi)核與Renderer進程就可以了。
Renderer進程的主要線程Renderer進程手下又有好多線程,它們各司其職。
GUI渲染線程。
JavaScript引擎線程。對于Chrome瀏覽器而言,這個線程上跑的就是威震海內(nèi)的V8引擎。
事件觸發(fā)線程。
定時器線程。
異步HTTP請求線程。
調(diào)用棧進入主題之前,我們先引入調(diào)用棧(callstack)的概念,調(diào)用棧是JavaScript引擎執(zhí)行程序的一種機制。為什么要有調(diào)用棧呢?我們舉個例子。
conststr= "biu"; console.log( "1"); function a( ){ console.log( "2"); b(); console.log( "3"); } function b( ){ console.log( "4"); } a();
我們都知道打印的順序是1243。
問題在于,當執(zhí)行到b函數(shù)的時候,我需要記住b函數(shù)的調(diào)用位置信息,也就是執(zhí)行上下文。否則執(zhí)行完b函數(shù)之后,引擎可能就忘了執(zhí)行console.log("3")了。調(diào)用棧就是用來干這個的,每調(diào)用一層函數(shù),引擎就會生成它的棧幀,棧幀里保存了執(zhí)行上下文,然后將它壓入調(diào)用棧中。棧是一個后進先出的結(jié)構(gòu),直到最里層的函數(shù)調(diào)用完,引擎才開始將最后進入的棧幀從棧中彈出。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|
- | - | - | - | console.log("4") | - | - | - |
- | - | console.log("2") | b() | b() | b() | console.log("3") | - |
console.log("1") | a() | a() | a() | a() | a() | a() | a() |
可以看到,當有嵌套函數(shù)調(diào)用的時候,棧幀會經(jīng)歷逐漸疊加又逐漸消失的過程,這就是所謂的后進先出。
同時也要注意,諸如conststr="biu"的變量聲明是不會入棧的。
調(diào)用棧也要占用內(nèi)存,所以如果調(diào)用棧過深,瀏覽器會報UncaughtRangeError:Maximumcallstacksizeexceeded錯誤。
webAPI現(xiàn)在我們進入主題。
JavaScript引擎將代碼從頭執(zhí)行到尾,不斷的進行壓棧和出棧操作。除了ECMAScript語法組成的代碼之外,我們還會寫哪些代碼呢?不錯,還有JavaScript運行時給我們提供的各種webAPI。運行時(runtime)簡單講就是JavaScript運行所在的環(huán)境。
我們重點討論三種webAPI。
consturl= "https://api.github.com/users/veedrin/repos"; fetch(url).then( res=>res.json()).then( console.log);
consturl= "https://api.github.com/users/veedrin/repos"; constxhr= newXMLHttpRequest(); xhr.open( "GET",url, true); xhr.onload= ()=>{ if(xhr.status=== 200){ console.log(xhr.response); } } xhr.send();
發(fā)起異步的HTTP請求,這幾乎是一個網(wǎng)頁必要的模塊。我們知道HTTP請求的速度和結(jié)果取決于當前網(wǎng)絡(luò)環(huán)境和服務(wù)器的狀態(tài),JavaScript引擎無法原地等待,所以瀏覽器得另開一個線程來處理HTTP請求,這就是之前提到的異步HTTP請求線程。
consttimeoutId=setTimeout( ()=>{ console.log( Date.now()); clearTimeout(timeoutId); }, 5000);
constintervalId=setInterval( ()=>{ console.log( Date.now()); }, 1000);
constimmediateId=setImmediate( ()=>{ console.log( Date.now()); clearImmediate(immediateId); });
定時器也是一個棘手的問題。首先,JavaScript引擎同樣無法原地等待;其次,即便不等待,JavaScript引擎也得執(zhí)行后面的代碼,根本無暇給定時器定時。所以于情于理,都得為定時器多帶帶開一個線程,這就是之前提到的定時器線程。
const$btn= document.getElementById( "btn"); $btn.addEventListener( "click", console.log);
按道理來講,DOM事件沒什么異步動作,直接綁定就行了,不會影響后面代碼的執(zhí)行。
別急,我們來看一個例子。
const$btn= document.getElementById( "btn"); $btn.addEventListener( "click", console.log); consttimeoutId=setTimeout( ()=>{ for( leti= 0;i< 10000;i++){ console.log( "biu"); } clearTimeout(timeoutId); }, 5000);
運行代碼,先綁定DOM事件,大約5秒鐘后開啟一個循環(huán)。注意,如果在循環(huán)結(jié)束之前點擊按鈕,瀏覽器控制臺會打印什么呢?
結(jié)果是先打印10000個biu,接著會打印Event對象。
試想一下,你點擊按鈕的時候,JavaScript引擎還在處理該死的循環(huán),根本沒空理你。那為什么點擊事件能夠被響應(yīng)呢(雖然有延時)?肯定是有另外一個線程在監(jiān)聽DOM事件。這就是之前提到的事件觸發(fā)線程。
任務(wù)隊列好的,現(xiàn)在我們知道有幾類webAPI是多帶帶的線程在處理。但是,處理完之后的回調(diào)總歸是要由JavaScript引擎線程來執(zhí)行的吧?這些線程是如何與JavaScript引擎線程通信的呢?
這就要提到大名鼎鼎的任務(wù)隊列(TaskQueue)。
其實無論是HTTP請求還是定時器還是DOM事件,我們都可以統(tǒng)稱它們?yōu)槭录:芎?,各自的線程把各自的webAPI處理完,完成之后怎么辦呢?它要把相應(yīng)的回調(diào)函數(shù)放入一個叫做任務(wù)隊列的數(shù)據(jù)結(jié)構(gòu)里。隊列和棧不一樣,隊列是先進先出的,講究一個先來后到的順序。
事件循環(huán)有很多文章認為任務(wù)隊列是由JavaScript引擎線程維護的,也有很多文章認為任務(wù)隊列是由事件觸發(fā)線程維護的。
根據(jù)上文的描述,事件觸發(fā)線程是專門用來處理DOM事件的。
然后我們來論證,為什么任務(wù)隊列不是由JavaScript引擎線程維護的。假如JavaScript引擎線程在執(zhí)行代碼的同時,其他線程要給任務(wù)隊列添加事件,這時候它哪忙得過來呢?
所以根據(jù)我的理解,任務(wù)隊列應(yīng)該是由一個專門的線程維護的。我們就叫它任務(wù)隊列線程吧。
等JavaScript引擎線程把所有的代碼執(zhí)行完了一遍,現(xiàn)在它可以歇著了嗎?也許吧,接下來它還有一個任務(wù),就是不停的去輪詢?nèi)蝿?wù)隊列,如果任務(wù)隊列是空的,它就可以歇一會,如果任務(wù)隊列中有回調(diào),它就要立即執(zhí)行這些回調(diào)。
這個過程會一直進行,它就是事件循環(huán)(EventLoop)。
我們總結(jié)一下這個過程:
第一階段,JavaScript引擎線程從頭到尾把腳本代碼執(zhí)行一遍,碰到需要其他線程處理的代碼則交給其他線程處理。
第二階段,JavaScript引擎線程專注于處理事件。它會不斷的去輪詢?nèi)蝿?wù)隊列,執(zhí)行任務(wù)隊列中的事件。這個過程又可以分解為輪詢?nèi)蝿?wù)隊列-執(zhí)行任務(wù)隊列中的事件-更新頁面視圖的無限往復(fù)。對,別忘了更新頁面視圖(如果需要的話),雖然更新頁面視圖是GUI渲染線程處理的。
這些事件,在任務(wù)隊列里面也被稱為任務(wù)。但是事情沒這么簡單,任務(wù)還分優(yōu)先級,這就是我們常聽說的宏任務(wù)和微任務(wù)。
既然任務(wù)分為宏任務(wù)和微任務(wù),那是不是得有兩個任務(wù)隊列呢?
此言差矣。
首先我們得知道,事件循環(huán)可不止一個。除了windoweventloop之外,還有workereventloop。并且同源的頁面會共享一個windoweventloop。
Awindoweventloopistheeventloopusedbysimilar-originwindowagents.Useragentsmayshareaneventloopacrosssimilar-originwindowagents.
其次我們要區(qū)分任務(wù)和任務(wù)源。什么叫任務(wù)源呢?就是這個任務(wù)是從哪里來的。是從addEventListener來的呢,還是從setTimeout來的。為什么要這么區(qū)分呢?比如鍵盤和鼠標事件,就要把它的響應(yīng)優(yōu)先級提高,以便盡可能的提高網(wǎng)頁瀏覽的用戶體驗。雖然都是任務(wù),命可分貴賤呢!
所以不同任務(wù)源的任務(wù)會放入不同的任務(wù)隊列里,瀏覽器根據(jù)自己的算法來決定先取哪個隊列里的任務(wù)。
總結(jié)起來,宏任務(wù)有至少一個任務(wù)隊列,微任務(wù)只有一個任務(wù)隊列。
哪些異步事件是微任務(wù)?Promise的回調(diào)、MutationObserver的回調(diào)以及nodejs中process.nextTick的回調(diào)。
< div id= "outer"> < div id= "inner">請點擊 div> div>
const$outer= document.getElementById( "outer"); const$inner= document.getElementById( "inner"); newMutationObserver( ()=>{ console.log( "mutate"); }).observe($inner,{ childList: true, }); function onClick( ){ console.log( "click"); setTimeout( ()=> console.log( "timeout"), 0); Promise.resolve().then( ()=> console.log( "promise")); $inner.innerHTML= "已點擊"; } $inner.addEventListener( "click",onClick); $outer.addEventListener( "click",onClick);
我們先來看執(zhí)行順序。
click promise mutate click promise mutate timeout timeout
整個執(zhí)行過程是怎樣的呢?
從頭到尾初始執(zhí)行腳本代碼。給DOM元素添加事件監(jiān)聽。
用戶觸發(fā)內(nèi)元素的DOM事件,同時冒泡觸發(fā)外元素的DOM事件。將內(nèi)元素和外元素的DOM事件回調(diào)添加到宏任務(wù)隊列中。
因為此時調(diào)用棧中是空閑的,所以將內(nèi)元素的DOM事件回調(diào)放入調(diào)用棧。
執(zhí)行回調(diào),此時打印click。同時將setTimeout的回調(diào)放入宏任務(wù)隊列,將Promise的回調(diào)放入微任務(wù)隊列。因為修改了DOM元素,觸發(fā)MutationObserver事件,將MutationObserver的回調(diào)放入微任務(wù)隊列?;仡櫼幌?,現(xiàn)在宏任務(wù)隊列里有兩個回調(diào),分別是外元素的DOM事件回調(diào)和setTimeout的回調(diào);微任務(wù)隊列里也有兩個回調(diào),分別是Promise的回調(diào)和MutationObserver的回調(diào)。
依次將微任務(wù)隊列中的回調(diào)放入調(diào)用棧,此時打印promise和mutate。
將外元素的DOM事件回調(diào)放入調(diào)用棧。執(zhí)行回調(diào),此時打印click。因為兩個DOM事件回調(diào)是一樣的,過程不再重復(fù)。再次回顧一下,現(xiàn)在宏任務(wù)隊列里有兩個回調(diào),分別是兩個setTimeout的回調(diào);微任務(wù)隊列里也有兩個回調(diào),分別是Promise的回調(diào)和MutationObserver的回調(diào)。
依次將微任務(wù)隊列中的回調(diào)放入調(diào)用棧,此時打印promise和mutate。
最后依次將setTimeout的回調(diào)放入調(diào)用棧執(zhí)行,此時打印兩次timeout。
規(guī)律是什么呢?宏任務(wù)與宏任務(wù)之間,積壓的所有微任務(wù)會一次性執(zhí)行完畢。這就好比超市排隊結(jié)賬,輪到你結(jié)賬的時候,你突然想順手買一盒岡本。難道超市會要求你先把之前的賬結(jié)完,然后重新排隊嗎?不會,超市會順便幫你把岡本的賬也結(jié)了。這樣效率更高不是么?雖然不知道內(nèi)部的處理細節(jié),但是我覺得標準區(qū)分兩種任務(wù)類型也是出于性能的考慮吧。
$inner.click();
如果DOM事件不是用戶觸發(fā)的,而是程序觸發(fā)的,會有什么不一樣嗎?
click click promise mutate promise timeout timeout
嚴格的說,這時候并沒有觸發(fā)事件,而是直接執(zhí)行onClick函數(shù)。翻譯一下就是下面這樣的效果。
onClick(); onClick();
這樣就解釋了為什么會先打印兩次click。而MutationObserver會合并多個事件,所以只打印一次mutate。所有微任務(wù)依然會在下一個宏任務(wù)之前執(zhí)行,所以最后才打印兩次timeout。
我們再來看一個例子。
const$btn= document.getElementById( "btn"); function onClick( ){ setTimeout( ()=>{ new Promise( resolve=>resolve( "promise1")).then( console.log); new Promise( resolve=>resolve( "promise2")).then( console.log); console.log( "timeout1"); $btn.style.color= "#f00"; }, 1000); setTimeout( ()=>{ new Promise( resolve=>resolve( "promise1")).then( console.log); new Promise( resolve=>resolve( "promise2")).then( console.log); console.log( "timeout2"); }, 1000); setTimeout( ()=>{ new Promise( resolve=>resolve( "promise1")).then( console.log); new Promise( resolve=>resolve( "promise2")).then( console.log); console.log( "timeout3"); }, 1000); setTimeout( ()=>{ new Promise( resolve=>resolve( "promise1")).then( console.log); new Promise( resolve=>resolve( "promise2")).then( console.log); console.log( "timeout4"); //alert(1); }, 1000); setTimeout( ()=>{ new Promise( resolve=>resolve( "promise1")).then( console.log); new Promise( resolve=>resolve( "promise2")).then( console.log); console.log( "timeout5"); //alert(1); }, 1000); setTimeout( ()=>{ new Promise( resolve=>resolve( "promise1")).then( console.log); new Promise( resolve=>resolve( "promise2")).then( console.log); console.log( "timeout6"); }, 1000); newMutationObserver( ()=>{ console.log( "mutate"); }).observe($btn,{ attributes: true, }); } $btn.addEventListener( "click",onClick);
當我在第4個setTimeout添加alert,瀏覽器被阻斷時,樣式還沒有生效。
有很多人說,每一個宏任務(wù)執(zhí)行完并附帶執(zhí)行完累計的微任務(wù)(我們稱它為一個宏任務(wù)周期),這時會有一個更新頁面視圖的窗口期,給更新頁面視圖預(yù)留一段時間。
但是我們的例子也看到了,每一個setTimeout都是一個宏任務(wù),瀏覽器被阻斷時事件循環(huán)都好幾輪了,但樣式依然沒有生效??梢娺@種說法是不準確的。
而當我在第5個setTimeout添加alert,瀏覽器被阻斷時,有很大的概率(并不是一定)樣式會生效。這說明什么時候更新頁面視圖是由瀏覽器決定的,并沒有一個準確的時機。
JavaScript引擎首先從頭到尾初始執(zhí)行腳本代碼,不必多言。
如果初始執(zhí)行完畢后有微任務(wù),則執(zhí)行微任務(wù)(為什么這里不屬于事件循環(huán)?后面會講到)。
之后就是不斷的事件循環(huán)。
首先到宏任務(wù)隊列里找宏任務(wù),宏任務(wù)隊列又分好多種,瀏覽器自己決定優(yōu)先級。
被放入調(diào)用棧的某個宏任務(wù),如果它的代碼中又包含微任務(wù),則執(zhí)行所有微任務(wù)。
更新頁面視圖沒有一個準確的時機,是每個宏任務(wù)周期后更新還是幾個宏任務(wù)周期后更新,由瀏覽器決定。
也有一種說法認為:從頭到尾初始執(zhí)行腳本代碼也是一個任務(wù)。
如果我們認可這種說法,則整個代碼執(zhí)行過程都屬于事件循環(huán)。
初始執(zhí)行就是一個宏任務(wù),這個宏任務(wù)里面如果有微任務(wù),則執(zhí)行所有微任務(wù)。
瀏覽器自己決定更新頁面視圖的時機。
不斷的往復(fù)這個過程,只不過之后的宏任務(wù)是事件回調(diào)。
第二種解釋好像更說得通。因為第一種解釋會有一段微任務(wù)的執(zhí)行不在事件循環(huán)里,這顯然是不對的。
遲到的承諾
Promise是一個表現(xiàn)為狀態(tài)機的異步容器。
它有以下幾個特點:
狀態(tài)不受外界影響。Promise只有三種狀態(tài):pending(進行中)、fulfilled(已成功)和rejected(已失敗)。狀態(tài)只能通過Promise內(nèi)部提供的resolve()和reject()函數(shù)改變。
狀態(tài)只能從pending變?yōu)?b>fulfilled或者從pending變?yōu)?b>rejected。并且一旦狀態(tài)改變,狀態(tài)就會被凍結(jié),無法再次改變。
new Promise( ( resolve,reject)=>{ reject( "reject"); setTimeout( ()=>resolve( "resolve"), 5000); }).then( console.log, console.error); //不要等了,它只會打印一個reject
如果狀態(tài)發(fā)生改變,任何時候都可以獲得最終的狀態(tài),即便改變發(fā)生在前。這與事件監(jiān)聽完全不一樣,事件監(jiān)聽只能監(jiān)聽之后發(fā)生的事件。
constpromise= new Promise( resolve=>resolve( "biu")); promise.then( console.log); setTimeout( ()=>promise.then( console.log), 5000); //打印biu,相隔大約5秒鐘后又打印biu
正是源于這些特點,Promise才敢于稱自己為一個承諾。
同步代碼與異步代碼Promise是一個異步容器,那哪些部分是同步執(zhí)行的,哪些部分是異步執(zhí)行的呢?
console.log( "kiu"); new Promise( ( resolve,reject)=>{ console.log( "miu"); resolve( "biu"); console.log( "niu"); }).then( console.log, console.error); console.log( "piu");
我們看執(zhí)行結(jié)果。
kiu miu niu piu biu
可以看到,Promise構(gòu)造函數(shù)的參數(shù)函數(shù)是完完全全的同步代碼,只有狀態(tài)改變觸發(fā)的then回調(diào)才是異步代碼。為啥說Promise是一個異步容器?它不關(guān)心你給它裝的是啥,它只關(guān)心狀態(tài)改變后的異步執(zhí)行,并且承諾給你一個穩(wěn)定的結(jié)果。
從這點來看,Promise真的只是一個異步容器而已。
Promise.prototype.then()then方法接受兩個回調(diào)作為參數(shù),狀態(tài)變成fulfilled時會觸發(fā)第一個回調(diào),狀態(tài)變成rejected時會觸發(fā)第二個回調(diào)。你可以認為then回調(diào)是Promise這個異步容器的界面和輸出,在這里你可以獲得你想要的結(jié)果。
then函數(shù)可以實現(xiàn)鏈式調(diào)用嗎?可以的。
但你想一下,then回調(diào)觸發(fā)的時候,Promise的狀態(tài)已經(jīng)凍結(jié)了。這時候它就是被打開盒子的薛定諤的貓,它要么是死的,要么是活的。也就是說,它不可能再次觸發(fā)then回調(diào)。
那then函數(shù)是如何實現(xiàn)鏈式調(diào)用的呢?
原理就是then函數(shù)自身返回的是一個新的Promise實例。再次調(diào)用then函數(shù)的時候,實際上調(diào)用的是這個新的Promise實例的then函數(shù)。
既然Promise只是一個異步容器而已,換一個容器也不會有什么影響。
constpromiseA= new Promise( ( resolve,reject)=>resolve( "biu")); constpromiseB=promiseA.then( value=>{ console.log(value); returnvalue; }); constpromiseC=promiseB.then( console.log);
結(jié)果是打印了兩個biu。
constpromiseA= new Promise( ( resolve,reject)=>resolve( "biu")); constpromiseB=promiseA.then( value=>{ console.log(value); return Promise.resolve(value); }); constpromiseC=promiseB.then( console.log);
Promise.resolve()我們后面會講到,它返回一個狀態(tài)是fulfilled的Promise實例。
這次我們手動返回了一個狀態(tài)是fulfilled的新的Promise實例,可以發(fā)現(xiàn)結(jié)果和上一次一模一樣。說明then函數(shù)悄悄的將return"biu"轉(zhuǎn)成了returnPromise.resolve("biu")。如果沒有返回值呢?那就是轉(zhuǎn)成returnPromise.resolve(),反正得轉(zhuǎn)成一個新的狀態(tài)是fulfilled的Promise實例返回。
這就是then函數(shù)返回的總是一個新的Promise實例的內(nèi)部原理。
想要讓新Promise實例的狀態(tài)從pending變成rejected,有什么辦法嗎?畢竟then方法也沒給我們提供reject方法。
constpromiseA= new Promise( ( resolve,reject)=>resolve( "biu")); constpromiseB=promiseA.then( value=>{ console.log(value); returnx; }); constpromiseC=promiseB.then( console.log, console.error);
查看這里的輸出結(jié)果。
biu ReferenceError:xisnotdefined at:6:5
只有程序本身發(fā)生了錯誤,新Promise實例才會捕獲這個錯誤,并把錯誤暗地里傳給reject方法。于是狀態(tài)從pending變成rejected。
Promise.prototype.catch()catch方法,顧名思義是用來捕獲錯誤的。它其實是then方法某種方式的語法糖,所以下面兩種寫法的效果是一樣的。
new Promise( ( resolve,reject)=>{ reject( "biu"); }).then( undefined, error=> console.error(error), );
new Promise( ( resolve,reject)=>{ reject( "biu"); }).catch( error=> console.error(error), );
Promise內(nèi)部的錯誤會靜默處理。你可以捕獲到它,但錯誤本身已經(jīng)變成了一個消息,并不會導(dǎo)致外部程序的崩潰和停止執(zhí)行。
下面的代碼運行中發(fā)生了錯誤,所以容器中后面的代碼不會再執(zhí)行,狀態(tài)變成rejected。但是容器外面的代碼不受影響,依然正常執(zhí)行。
new Promise( ( resolve,reject)=>{ console.log(x); console.log( "kiu"); resolve( "biu"); }).then( console.log, console.error); setTimeout( ()=> console.log( "piu"), 5000);
所以大家常常說"Promise會吃掉錯誤"。
如果狀態(tài)已經(jīng)凍結(jié),即便運行中發(fā)生了錯誤,Promise也會忽視它。
new Promise( ( resolve,reject)=>{ resolve( "biu"); console.log(x); }).then( console.log, console.error); setTimeout( ()=> console.log( "piu"), 5000);
Promise的錯誤如果沒有被及時捕獲,它會往下傳遞,直到被捕獲。中間沒有捕獲代碼的then函數(shù)就被忽略了。
Promise.prototype.finally()new Promise( ( resolve,reject)=>{ console.log(x); resolve( "biu"); }).then( value=> console.log(value), ).then( value=> console.log(value), ).then( value=> console.log(value), ).catch( error=> console.error(error), );
所謂finally就是一定會執(zhí)行的方法。它和then或者catch不一樣的地方在于,finally方法的回調(diào)函數(shù)不接受任何參數(shù)。也就是說,它不關(guān)心容器的狀態(tài),它只是一個兜底的。
new Promise( ( resolve,reject)=>{ //邏輯 }).then( value=>{ //邏輯 console.log(value); }, error=>{ //邏輯 console.error(error); } );
new Promise( ( resolve,reject)=>{ //邏輯 }).finally( ()=>{ //邏輯 } );
如果有一段邏輯,無論狀態(tài)是fulfilled還是rejected都要執(zhí)行,那放在then函數(shù)中就要寫兩遍,而放在finally函數(shù)中就只需要寫一遍。
另外,別被finally這個名字帶偏了,它不一定要定義在最后的。
new Promise( ( resolve,reject)=>{ resolve( "biu"); }).finally( ()=> console.log( "piu"), ).then( value=> console.log(value), ).catch( error=> console.error(error), );
finally函數(shù)在鏈條中的哪個位置定義,就會在哪個位置執(zhí)行。從語義化的角度講,finally不如叫anyway。
Promise.all()它接受一個由Promise實例組成的數(shù)組,然后生成一個新的Promise實例。這個新Promise實例的狀態(tài)由數(shù)組的整體狀態(tài)決定,只有數(shù)組的整體狀態(tài)都是fulfilled時,新Promise實例的狀態(tài)才是fulfilled,否則就是rejected。這就是all的含義。
Promise.all([ Promise.resolve( 1), Promise.resolve( 2), Promise.resolve( 3)]).then( values=> console.log(values), ).catch( error=> console.error(error), );
Promise.all([ Promise.resolve( 1), Promise.reject( 2), Promise.resolve( 3)]).then( values=> console.log(values), ).catch( error=> console.error(error), );
數(shù)組中的項目如果不是一個Promise實例,all函數(shù)會將它封裝成一個Promise實例。
Promise.race()Promise.all([ 1, 2, 3]).then( values=> console.log(values), ).catch( error=> console.error(error), );
它的使用方式和Promise.all()類似,但是效果不一樣。
Promise.all()是只有數(shù)組中的所有Promise實例的狀態(tài)都是fulfilled時,它的狀態(tài)才是fulfilled,否則狀態(tài)就是rejected。
而Promise.race()則只要數(shù)組中有一個Promise實例的狀態(tài)是fulfilled,它的狀態(tài)就會變成fulfilled,否則狀態(tài)就是rejected。
就是&&和||的區(qū)別是吧。
它們的返回值也不一樣。
Promise.all()如果成功會返回一個數(shù)組,里面是對應(yīng)Promise實例的返回值。
而Promise.race()如果成功會返回最先成功的那一個Promise實例的返回值。
function fetchByName( name){ consturl= `https://api.github.com/users/ ${name}/repos`; returnfetch(url).then( res=>res.json()); } consttimingPromise= new Promise( ( resolve,reject)=>{ setTimeout( ()=>reject( new Error( "網(wǎng)絡(luò)請求超時")), 5000); }); Promise.race([fetchByName( "veedrin"),timingPromise]).then( values=> console.log(values), ).catch( error=> console.error(error), );
上面這個例子可以實現(xiàn)網(wǎng)絡(luò)超時觸發(fā)指定操作。
Promise.resolve()它的作用是接受一個值,返回一個狀態(tài)是fulfilled的Promise實例。
Promise.resolve( "biu");
new Promise( resolve=>resolve( "biu"));
它是以上寫法的語法糖。
Promise.reject()它的作用是接受一個值,返回一個狀態(tài)是rejected的Promise實例。
Promise.reject( "biu");
new Promise( ( resolve,reject)=>reject( "biu"));
它是以上寫法的語法糖。
嵌套Promise如果Promise有嵌套,它們的狀態(tài)又是如何變化的呢?
constpromise= Promise.resolve( ( ()=>{ console.log( "a"); return Promise.resolve( ( ()=>{ console.log( "b"); return Promise.resolve( ( ()=>{ console.log( "c"); return new Promise( resolve=>{ setTimeout( ()=>resolve( "biu"), 3000); }); })() ) })() ); })() ); promise.then( console.log);
可以看到,例子中嵌套了四層Promise。別急,我們先回顧一下沒有嵌套的情況。
constpromise= Promise.resolve( "biu"); promise.then( console.log);
我們都知道,它會在微任務(wù)時機執(zhí)行,肉眼幾乎看不到等待。
但是嵌套了四層Promise的例子,因為最里層的Promise需要等待幾秒才resolve,所以最外層的Promise返回的實例也要等待幾秒才會打印日志。也就是說,只有最里層的Promise狀態(tài)變成fulfilled,最外層的Promise狀態(tài)才會變成fulfilled。
如果你眼尖的話,你就會發(fā)現(xiàn)這個特性就是Koa中間件機制的精髓。
Koa中間件機制也是必須得等最后一個中間件resolve(如果它返回的是一個Promise實例的話)之后,才會執(zhí)行洋蔥圈另外一半的代碼。
function compose( middleware){ return function( context,next){ letindex= -1; returndispatch( 0); function dispatch( i){ if(i<=index) return Promise.reject( new Error( "next()calledmultipletimes")); index=i; letfn=middleware[i]; if(i===middleware.length)fn=next; if(!fn) return Promise.resolve(); try{ return Promise.resolve(fn(context, function next( ){ returndispatch(i+ 1); })); } catch(err){ return Promise.reject(err); } } } }
狀態(tài)機
Generator簡單講就是一個狀態(tài)機。但它和Promise不一樣,它可以維持無限個狀態(tài),并且提出它的初衷并不是為了解決異步編程的某些問題。
一個線程一次只能做一件任務(wù),并且任務(wù)與任務(wù)之間不能間斷。而Generator開了掛,它可以暫停手頭的任務(wù),先干別的,然后在恰當?shù)臅r機手動切換回來。
這是一種纖程或者協(xié)程的概念,相比線程切換更加輕量化的切換方式。
Iterator在講Generator之前,我們要先和Iterator遍歷器打個照面。
Iterator對象是一個指針對象,它是一種類似于單向鏈表的數(shù)據(jù)結(jié)構(gòu)。JavaScript通過Iterator對象來統(tǒng)一數(shù)組和類數(shù)組的遍歷方式。
constarr=[ 1, 2, 3]; constiteratorConstructor=arr[ Symbol.iterator]; console.log(iteratorConstructor); //?values(){[nativecode]}
constobj={ a: 1, b: 2, c: 3}; constiteratorConstructor=obj[ Symbol.iterator]; console.log(iteratorConstructor); //undefined
constset= new Set([ 1, 2, 3]); constiteratorConstructor=set[ Symbol.iterator]; console.log(iteratorConstructor); //?values(){[nativecode]}
我們已經(jīng)見到了Iterator對象的構(gòu)造器,它藏在Symbol.iterator下面。接下來我們生成一個Iterator對象來了解它的工作方式吧。
constarr=[ 1, 2, 3]; constit=arr[ Symbol.iterator](); console.log(it.next()); //{value:1,done:false} console.log(it.next()); //{value:2,done:false} console.log(it.next()); //{value:3,done:false} console.log(it.next()); //{value:undefined,done:true} console.log(it.next()); //{value:undefined,done:true}
既然它是一個指針對象,調(diào)用next()的意思就是把指針往后挪一位。挪到最后一位,再往后挪,它就會一直重復(fù)我已經(jīng)到頭了,只能給你一個空值。
GeneratorGenerator是一個生成器,它生成的到底是什么呢?
對咯,他生成的就是一個Iterator對象。
function* gen( ){ yield 1; yield 2; return 3; } constit=gen(); console.log(it.next()); //{value:1,done:false} console.log(it.next()); //{value:2,done:false} console.log(it.next()); //{value:3,done:false} console.log(it.next()); //{value:undefined,done:true} console.log(it.next()); //{value:undefined,done:true}
Generator有什么意義呢?普通函數(shù)的執(zhí)行會形成一個調(diào)用棧,入棧和出棧是一口氣完成的。而Generator必須得手動調(diào)用next()才能往下執(zhí)行,相當于把執(zhí)行的控制權(quán)從引擎交給了開發(fā)者。
所以Generator解決的是流程控制的問題。
它可以在執(zhí)行過程暫時中斷,先執(zhí)行別的程序,但是它的執(zhí)行上下文并沒有銷毀,仍然可以在需要的時候切換回來,繼續(xù)往下執(zhí)行。
最重要的優(yōu)勢在于,它看起來是同步的語法,但是卻可以異步執(zhí)行。
yield對于一個Generator函數(shù)來說,什么時候該暫停呢?就是在碰到yield關(guān)鍵字的時候。
function* gen( ){ console.log( "a"); yield 13* 15; console.log( "b"); yield 15- 13; console.log( "c"); return 3; } constit=gen();
看上面的例子,第一次調(diào)用it.next()的時候,碰到了第一個yield關(guān)鍵字,然后開始計算yield后面表達式的值,然后這個值就成了it.next()返回值中value的值,然后停在這。這一步會打印a,但不會打印b。
以此類推。return的值作為最后一個狀態(tài)傳遞出去,然后返回值的done屬性就變成true,一旦它變成true,之后繼續(xù)執(zhí)行的返回值都是沒有意義的。
這里面有一個狀態(tài)傳遞的過程。yield把它暫停之前獲得的狀態(tài)傳遞給執(zhí)行器。
那么有沒有可能執(zhí)行器傳遞狀態(tài)給狀態(tài)機內(nèi)部呢?
function* gen( ){ consta= yield 1; console.log(a); constb= yield 2; console.log(b); return 3; } constit=gen();
當然是可以的。
默認情況下,第二次執(zhí)行的時候變量a的打印結(jié)果是undefined,因為yield關(guān)鍵字就沒有返回值。
但是如果給next()傳遞參數(shù),這個參數(shù)就會作為上一個yield的返回值。
it.next("biu");
別急,第一次執(zhí)行沒有所謂的上一個yield,所以這個參數(shù)是沒有意義的。
it.next("piu"); //打印piu。這個piu是console.log(a)打印出來的。
第二次執(zhí)行就不同了。a變量接收到了next()傳遞進去的參數(shù)。
這有什么用?如果能在執(zhí)行過程中給狀態(tài)機傳值,我們就可以改變狀態(tài)機的執(zhí)行條件。你可以發(fā)現(xiàn),Generator是可以實現(xiàn)值的雙向傳遞的。
為什么要作為上一個yield的返回值?你想啊,作為上一個yield的返回值,才能改變當前代碼的執(zhí)行條件,這樣才有價值不是嘛。這地方有點繞,仔細想一想。
自動執(zhí)行好吧,既然引擎把Generator的控制權(quán)交給了開發(fā)者,那我們就要探索出一種方法,讓Generator的遍歷器對象可以自動執(zhí)行。
function* gen( ){ yield 1; yield 2; return 3; } function run( gen){ constit=gen(); letstate={ done: false}; while(!state.done){ state=it.next(); console.log(state); } } run(gen);
不錯,竟然這么簡單。
但想想我們是來干什么的,我們是來探討JavaScript異步的呀。這個簡陋的run函數(shù)能夠執(zhí)行異步操作嗎?
function fetchByName( name){ consturl= `https://api.github.com/users/ ${name}/repos`; fetch(url).then( res=>res.json()).then( res=> console.log(res)); } function* gen( ){ yieldfetchByName( "veedrin"); yieldfetchByName( "tj"); } function run( gen){ constit=gen(); letstate={ done: false}; while(!state.done){ state=it.next(); } } run(gen);
事實證明,Generator會把fetchByName當做一個同步函數(shù)來執(zhí)行,沒等請求觸發(fā)回調(diào),它已經(jīng)將指針指向了下一個yield。我們的目的是讓上一個異步任務(wù)完成以后才開始下一個異步任務(wù),顯然這種方式做不到。
我們已經(jīng)讓Generator自動化了,但是在面對異步任務(wù)的時候,交還控制權(quán)的時機依然不對。
什么才是正確的時機呢?
在回調(diào)中交還控制權(quán)哪個時間點表明某個異步任務(wù)已經(jīng)完成?當然是在回調(diào)中咯。
我們來拆解一下思路。
首先我們要把異步任務(wù)的其他參數(shù)和回調(diào)參數(shù)拆分開來,因為我們需要多帶帶在回調(diào)中扣一下扳機。
然后yieldasyncTask()的返回值得是一個函數(shù),它接受異步任務(wù)的回調(diào)作為參數(shù)。因為Generator只有yield的返回值是暴露在外面的,方便我們控制。
最后在回調(diào)中移動指針。
function thunkify( fn){ return ( ...args)=>{ return ( done)=>{ args.push(done); fn(...args); } } }
這就是把異步任務(wù)的其他參數(shù)和回調(diào)參數(shù)拆分開來的法寶。是不是很簡單?它通過兩層閉包將原過程變成三次函數(shù)調(diào)用,第一次傳入原函數(shù),第二次傳入回調(diào)之前的參數(shù),第三次傳入回調(diào),并在最里一層閉包中又把參數(shù)整合起來傳入原函數(shù)。
是的,這就是大名鼎鼎的thunkify。
以下是暖男版。
function thunkify( fn){ return ( ...args)=>{ return ( done)=>{ letcalled= false; args.push( ( ...innerArgs)=>{ if(called) return; called= true; done(...innerArgs); }); try{ fn(...args); } catch(err){ done(err); } } } }
寶刀已經(jīng)有了,咱們?nèi)ネ例埌伞?/p>
constfs= require( "fs"); constthunkify= require( "./thunkify"); constreadFileThunk=thunkify(fs.readFile); function* gen( ){ constvalueA= yieldreadFileThunk( "/Users/veedrin/a.md"); console.log( "a.md的內(nèi)容是: ",valueA.toString()); constvalueB= yieldreadFileThunk( "/Users/veedrin/b.md"); console.log( "b.md的內(nèi)容是: ",valueB.toString()); } 文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/6788.html
摘要:本文從入手,系統(tǒng)的回顧的異步機制及發(fā)展歷程。需要提醒的是,文本沒有討論的異步機制。本文是專題系列文章之一,后續(xù)會有更多專題推出地址持續(xù)更新博客地址文章排版真的很漂亮如果覺得對你有幫助,歡迎來點或者來我的博客親口告訴我本文從Event Loop、Promise、Generator、async await入手,系統(tǒng)的回顧 JavaScript 的異步機制及發(fā)展歷程。 需要提醒的是,文本沒有討論 ...
摘要:跨域請求詳解從繁至簡前端掘金什么是為什么要用是的一種使用模式,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題。異步編程入門道典型的面試題前端掘金在界中,開發(fā)人員的需求量一直居高不下。 jsonp 跨域請求詳解——從繁至簡 - 前端 - 掘金什么是jsonp?為什么要用jsonp?JSONP(JSON with Padding)是JSON的一種使用模式,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問的問題...
摘要:模塊化是隨著前端技術(shù)的發(fā)展,前端代碼爆炸式增長后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調(diào)也不等同于異步。將會討論安全的類型檢測惰性載入函數(shù)凍結(jié)對象定時器等話題。 Vue.js 前后端同構(gòu)方案之準備篇——代碼優(yōu)化 目前 Vue.js 的火爆不亞于當初的 React,本人對寫代碼有潔癖,代碼也是藝術(shù)。此篇是準備篇,工欲善其事,必先利其器。我們先在代...
摘要:今天同學去面試,做了兩道面試題全部做錯了,發(fā)過來給道典型的面試題前端掘金在界中,開發(fā)人員的需求量一直居高不下。 排序算法 -- JavaScript 標準參考教程(alpha) - 前端 - 掘金來自《JavaScript 標準參考教程(alpha)》,by 阮一峰 目錄 冒泡排序 簡介 算法實現(xiàn) 選擇排序 簡介 算法實現(xiàn) ... 圖例詳解那道 setTimeout 與循環(huán)閉包的經(jīng)典面...
摘要:忍者級別的函數(shù)操作對于什么是匿名函數(shù),這里就不做過多介紹了。我們需要知道的是,對于而言,匿名函數(shù)是一個很重要且具有邏輯性的特性。通常,匿名函數(shù)的使用情況是創(chuàng)建一個供以后使用的函數(shù)。 JS 中的遞歸 遞歸, 遞歸基礎(chǔ), 斐波那契數(shù)列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執(zhí)行機制 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機制,如果...
閱讀 6219·2021-11-22 15:32
閱讀 833·2021-11-11 16:54
閱讀 3174·2021-10-13 09:40
閱讀 2176·2021-09-03 10:35
閱讀 1849·2021-08-09 13:47
閱讀 1882·2019-08-30 15:55
閱讀 1943·2019-08-30 15:43
閱讀 2466·2019-08-29 17:06