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