摘要:回調函數是處理異步邏輯最基礎的方法,但也有著各種的缺點。回調函數必須遵守的原則就是信任,但要核實。異步的進化一前面一部分已經描述到了回調函數的兩個問題分別是缺乏順序性和缺乏可信任性。
要帶著問題學,活學活用,學用結合,急用先學,立竿見影,在「用」字上狠下功夫。
廢話少說。
這是這個專題的第二部分內容,異步。主要總結了《你不知道的JavaScript(中卷)》中有關于異步的內容。顯然一下子寫完三個部分的內容不太可能,下篇會在不久之后放出。
由于前人之述備矣,所以有些地方會引用它山之石,它山之石可以攻玉嘛。 ?
首先明確,JavaScript是一種單線程語言,不會出現多線程。
1. 【異步的核心】程序中現在運行部分和將來運行部分的關系就是異步編程的核心。簡單來講,如果程序中出現了一部分要在現在運行(順序同步執行),一部分要在將來運行(可能是設置了timeout也可能是一個ajax的異步調用后執行的函數),那么兩者之間的關系的構建就構成了異步編程。
2. 【事件循環】相當于一個永遠執行的while(true)循環,循環的每一輪稱為一個tick。對于每個tick而言,如果隊列中有等待事件,那么從隊列中拿下這個事件執行。隊列中事件就是注冊的異步調用函數。
由于事件循環的原因,setTimeout只是在timeout的時間后將函數注冊到事件循環中,因為有被其他任務阻塞的可能,所以其時間不一定準確。setInterval同理可得。
setTimeout(…,0)可以進行異步調動,將函數放在事件隊列循環的末尾,是一種hack的方法。
具體可以參閱以下blog:你所不知道的setInterval | 晚晴幽草軒
Promise的then是基于任務的。任務和事件循環的區別,可以理解為任務代表的異步函數可以插隊進入當前事件之后。所以從理論上來說,任務循環(job loop)可能導致無限循環(一個任務添加另一個不需要排隊的任務,例如Promise中then的無限連接)使得無法進入到下一個tick中。
EX 事件循環和任務的認識
(function test() { setTimeout(function() {console.log(4)}, 0); new Promise(function executor(resolve) { console.log(1); for( var i=0 ; i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); })()
輸出是 1 2 3 5 4 而非 1 2 3 4 5
這就說明了Promise決議之后,先執行了then的這個任務(job),這個then沒有進入事件循環中排隊,因為如果排隊,應該會在setTimeout這個先注冊的function之后調用。所以then的任務隊列的優先級高于事件循環。并且磁力還說明了Promise的決議過程是同步執行的。
具體的原理說明:
https://github.com/creeperyan...
有時會由于兩個ajax調用的先后順序(或者其他操作的先后順序)的原因會導致運行結果的不同,為了控制進程的執行,有兩種控制的模式和兩種簡單的方式:
首先是門:這個可以控制兩個函數都完成之后才進行下一步工作,條件控制條件為if(a && b)
第二種是競態,也可稱為門閂。就是兩個函數只有一個能夠被調用,另一個會被忽略,其控制條件是設置一個undefined的變量a,調用后設為有值,并且判斷if(!a)
回調可以說是JavaScript的基礎了,這里不講回調的好處,只有回調的幾個明顯缺點(否則則么顯現出后面的進化呢(笑)):
1. 【回調函數】回調函數封裝了程序的延續(continuation)。回調函數是處理JavaScript異步邏輯最基礎的方法,但也有著各種的缺點。
2. 【嵌套回調和鏈式回調(回調地獄)】有下列代碼:
//《你不知道的JavaScript(中卷)》 listen( "click", function handler(evt){ setTimeout( function request(){ ajax( "http://some.url.1", function response(text){ if (text == "hello") { handler(); } else if (text == "world") { request(); } } ); }, 500) ; } );
這是一個由三個函數嵌套在一起的鏈式回調,每個函數代表了一個異步序列。
由于回調的特性,可能很難一下看出這個函數的執行邏輯(缺乏順序性),所以又被稱為回調地獄或者毀滅金字塔。
【回調地獄的缺陷】:
doA( function(){ doC(); doD( function(){ doF(); } ) doE(); } ); doB();
如果函數A和D是異步執行的,那么這個回調過程的執行步驟是A - F - B - C - E - D
除了難以閱讀以外,回調地獄真正的問題在于一旦指定了所有的可能時間和路徑,代碼就會變得十分復雜,無法維護和更新。因為一個進行的回調要是能夠覆蓋所有路徑,可能會寫上很多并行的回調函數,在代碼中看起來可能會十分凌亂和難以調試維護。
3. 【控制反轉】這牽涉到異步程序設計的信任問題。
控制反轉就是程序執行的主動權從自己的手中交了出去。如果僅僅是簡單的ajax調用,那么這個控制切換可能不會帶來什么大問題。但如果將一個回調函數交給一個外部的API,因為無法查看的具體代碼,所以可以看做是一個黑箱。這個黑箱導致問題是無法調試,不知道這個外部程序到底怎樣調用了這個回調函數,是一次都沒有,還是調用了很多次,亦或是比預想中過早過晚的調用,最終可能的后果就是程序執行的結果不如所愿。
教科書一點的定義就是把自己程序一部分的執行控制交給了某個第三方,且與這個第三方之間沒有一份明確表達的契約。
因為回調沒有機制來保障這個必然出現的控制反轉的問題,這就成為了回調的最大問題,會導致信任鏈的完全斷裂,是程序出錯。
回調函數必須遵守的原則就是:信任,但要核實。(Trust But Verify.)
4. 【error-first風格】回調函數的第一個參數留給錯誤處理,如果成功第一個參數就置為false,否則為true。回調執行時先進行判斷。
但是這個風格并沒有完全解決信任的問題,如果同時成功和失敗,就要另外寫代碼來處理。
回調會有同步回調調用和異步回調調用。這樣也會產生程序的運行問題,見下列代碼:
function result(data) { console.log( a ); } var a = 0; ajax( "..pre-cached-url..", result ); a++;
這端代碼會有0(同步回調調用)還是1(異步回調調用)的結果就要看情況而定了
對于可能同步調用也可能異步調用給出的回調函數的第三方工具而言,這個信任問題是明顯的。雖然可以用臃腫的附加代碼來解決,但并不優雅。
這樣的同步異步的混淆產生了另一條準則:
**永遠要異步調用回調,即使只在事件的下一輪。
(always invoke callbacks asynchronously, even if that"s "right away" on the next turn of the event loop)**
前面一部分已經描述到了回調函數的兩個問題分別是:缺乏順序性和缺乏可信任性。
那么這部分的Promise主要用來解決了可信任性的問題。
1. 【解決可信任問題的范式】不把程序的控制權交給第三方,而是希望第三方提供一個了解其任務何時結束的能力,然后由我們的代碼來決定接下來做什么。
2. 【未來值】A對于B有一個承諾,如果A給出了任務完成可以兌現承諾或者失敗不能兌現承諾的值,那么這個值就稱為未來值,簡單而言就是要在未來才能確定的值,但有承諾保證這個值存在。
由于未來值可能有兩個可能,要么成功,要么失敗。所以Promise值的then方法(在Promise值確定之后調用的函數)就可以接收兩個參數,第一個為成功的話執行的函數,第二個為失敗的話執行的函數。
舉個例子:
把x和y相加,如果有一個值沒有準備好,那就等待。一旦全部準備好就相加返回。
為了統一處理將來和現在,就把他們全部變成未來值,就全部異步調用。
回調模式下的代碼:
function add(getX,getY,cb) { var x, y; getX( function(xVal){ x = xVal; // both are ready? if (y != undefined) { cb( x + y ); // send along sum } } ); getY( function(yVal){ y = yVal; // both are ready? if (x != undefined) { cb( x + y ); // send along sum } } ); } // `fetchX()` and `fetchY()` are sync or async // functions add( fetchX, fetchY, function(sum){ console.log( sum ); // that was easy, huh? } );
Promise模式下的代碼:
function add(xPromise,yPromise) { // `Promise.all([ .. ])` takes an array of promises, // and returns a new promise that waits on them // all to finish return Promise.all( [xPromise, yPromise] ) // when that promise is resolved, let"s take the // received `X` and `Y` values and add them together. .then( function(values){ // `values` is an array of the messages from the // previously resolved promises return values[0] + values[1]; } ); } // `fetchX()` and `fetchY()` return promises for // their respective values, which may be ready // *now* or *later*. add( fetchX(), fetchY() ) // we get a promise back for the sum of those // two numbers. // now we chain-call `then(..)` to wait for the // resolution of that returned promise. .then( function(sum){ console.log( sum ); // that was easier! } );
通過比較明顯看出Promise模式的方法可以簡潔的表達一些操作。
Promise封裝了依賴于時間的狀態(等待未來值的產生,無論是現在還是未來產生,后續的步驟都是一樣的,解決了同步回調還是異步回調的問題),其本身與時間無關,所以可以按照可預測的方式組合。但Promise一旦決議,那么永遠將會保持在這個狀態,成為不變值,可以隨時查看。
3. 【revealing-constructor】一種產生Promise的模式,通常格式為
new Promise (function (…){…}) ,傳入的函數將會被立即執行。
識別Promise是否為真正的Promise很重要。定義某種稱為thenable的東西,將其定義為任何具有then(..)方法的對象和函數,任何這樣的值就是Promise一致的thenable。如果Promise決議遇到了這樣的thenable的值,那么就會被擱淺在這里,導致難以追蹤的bug。
5. 【Promise解決信任問題的方法】有五種回調導致的信任問題,分別來講:
調用過早: 由于一個任務有時候同步完成,有時候異步完成。如果使用回調會導致Zalgo出現,使用Promise無論是立即決議的revealing-constructor模式,還是異步執行的內容,都會基于最前面所講的任務隊列來進行異步調用,這樣就解決了調用過早的問題.
調用過晚:由于同步then調用時不被允許的,所以,一個Promise被決議之后,這個Promise上所有的通過then(…)注冊的回調都會下一個異步時機點一次被立即調用。任意一個都無法影響或延誤對其他回調的調用(不能插隊)
這里第一個function第一次注冊了打印出A的then方法,打印出B的then方法,注冊完畢后進行任務隊列的處理,因為A先注冊,所以先執行。這里又注冊了一個C的then方法,雖然p已經被決議,但是并不能立即調用(不能同步調用),還是加入到任務隊列的最后,不中斷對B的執行。所以執行結果是A B C。第二個是即使是p立即決議了,但是then中的內容還是被延遲到執行完所有同步內容之后運行。但是不同Promise值的回調順序是不可預測的,永遠不要依賴于不同Promise之間的回調順序來進行程序調度。
Ex:
p.then( function(){ p.then( function(){ console.log( "C" ); } ); console.log( "A" ); } ); p.then( function(){ console.log( "B" ); } ); // A B C function runme() { var i = 0; new Promise(function(resolve) { resolve(); }) .then(function() { i += 2; }); alert(i); } //0
回調未調用 : 沒有任何東西(包括JavaScript錯誤)可以組織Promise決議,它總會調用resolve和reject處理方法中的一個,即使是超時也有超時模式進行處理。(后續會講到)
調用次數過多或過少:由于Promise只能被決議一次,注冊的then只會被最多調用一次,所以過多的調用會直接無效。過少就是之前解釋的回調未調用的情況。
未能傳遞參數值、環境值:任何Promise都只能有一個決議值,如果resolve(…)或者reject(…)中傳遞了過多的參數,那都只會采納第一個,而忽略其他的,如果要有多個,那么就要封裝到數組或者對象中傳遞。
吞掉錯誤或異常:如果一個Promise產生了拒絕值并且給出了理由,那么這個就會被傳給拒絕回調,即使是JavaScript的異常也會這樣做。這里的會產生的另一個細節就是如果發生JavaScript錯誤會導致的同步調用,由于Promise的特性也會將其變為異步的調用。
但是試想,如果在then的正確處理函數中出現了錯誤會發生什么?
EX:
var p = new Promise( function(resolve,reject){ resolve( 42 ); } ); p.then( function fulfilled(msg){ foo.bar(); console.log( msg ); // never gets here :( }, function rejected(err){ // never gets here either :( } );
由于第一個then中未定義bar函數,所以會產生一個錯誤,但是并不會立即處理,而是會產生另一個Promise,這個新的Promise會由于錯誤而被拒絕,并沒有吞掉錯誤。因為p已經被決議為正確,所以不會因為fulfilled中間有錯誤而去調用rejected。
Promise.resolve()方法產生的Promise保證了返回內容的可信任性:
分別考慮resolve方法的參數,1)如果是一個非Promise,非thenable的 立即值,那么就會返回一個用這個值填充的Promise封裝,保證了內容的可信任。(即使是錯誤值) 2)如果是一個Promise,那么也只會產生一個Promise。3)如果傳遞了一個thenable的非Promise,那么就會試圖展開這個值,直到遇到了一個符合1條件的立即值,并封裝為Promise
通過這個方法,可以保證異步返回給回調函數的值為Promise可信任的。
6. 【鏈式流】鏈式流可以應用在會進行多次異步調用的方法中,可以加強代碼的清晰度可讀性和快速定位錯誤。
參見下面兩個代碼段:
//來自:http://imweb.io/topic/57a0760393d9938132cc8da9 getUserAdmin().then(function(result) { if ( /*管理員*/ ) { getProjectsWithAdmin().then(function(result) { /*根據項目id,獲取模塊列表*/ getModules(result.ids).then(function(result) { /*根據模塊id,獲取接口列表*/ getInterfaces(result.ids).then(function(result) { // ... }) }) }) } else { //... } }) //鏈式流 getUserAdmin().then(function(reult) { if ( /*管理員*/ ) { return getProjectsWithAdmin(); } else { return getProjectsWithUser(); } }).then(function(result) { /*獲取project id列表*/ return getModules(result.ids); }).then(function(result) { /*獲取project id列表*/ return getInterfaces(result.ids) }).then(function(result) { // ... })
能夠產生鏈式流基于以下兩個Promise的特性:
1.每次對Promise調用then(…),它都會產生一個新的Promise。
2.不管從then(…)調用的完成回調(第一個參數)返回的值是什么,它都會被自動設置為被連接Promise的完成,這句話表述了這個新的Promise的值就是這個then調用方法里的return語句,如果沒有,那么這個Promise的值就是undefined。
考慮以下代碼:
var p = Promise.resolve( 21 ); p .then( function(v){ console.log( v ); // 21 // fulfill the chained promise with value `42` return v * 2; } ) // here"s the chained promise .then( function(v){ console.log( v ); // 42 } );
上面的代碼充分展現了這兩條規則。另外兩條則充分說明了即使是返回一個Promise甚至返回中有異步調用(這里的異步調用不會被放入事件循環的最后,而是在這里直接延遲執行,后續的then會等待其執行完畢),這兩條規則都會正常工作:
var p = Promise.resolve( 21 ); p.then( function(v){ console.log( v ); // 21 // create a promise and return it return new Promise( function(resolve,reject){ // fulfill with value `42` resolve( v * 2 ); } ); } ) .then( function(v){ console.log( v ); // 42 } );
var p = Promise.resolve( 21 ); p.then( function(v){ console.log( v ); // 21 // create a promise to return return new Promise( function(resolve,reject){ // introduce asynchrony! setTimeout( function(){ // fulfill with value `42` resolve( v * 2 ); }, 100 ); } ); } ) .then( function(v){ // runs after the 100ms delay in the previous step console.log( v ); // 42 } );
如果鏈中有步驟出錯,會直接將這個錯誤封裝為Promise傳入到鏈中的下一個錯誤處理方法中(原因之前已經講過)。如果這個錯誤處理return了一個值,那么這個值會被帶入到下一個then處理的正確處理方法中,如果return了一個Promise那么就有可能會使得下一個then延遲調用。如果沒有return,那就默認return undefined,同樣也是正確處理中。
默認的拒絕處理函數:如果產生了錯誤,但沒有拒絕處理函數,那么就會有默認的,默認的所做的事情就是拋出錯誤,那么這個錯誤就會繼續向下直到有顯式的拒絕處理函數。
默認的接收處理函數:純粹將一個promise繼續向下傳遞。如果只有拒絕處理可以將簡寫為:catch(function(err){…})
由于Promise一旦被決議就不再更改的特性,以下代碼可能會導致沒有錯誤處理函數來處理:
var p = Promise.resolve( 42 ); p.then( function fulfilled(msg){ // numbers don"t have string functions, // so will throw an error console.log( msg.toLowerCase() ); }, function rejected(err){ // never gets here } );
幾種解決方案(除了1都未被ES6標準實現):
1) 在最后加catch,這樣會導致的問題就是catch中的函數如果也有錯誤就無法捕捉。
2)有個done函數,就算done函數有錯誤,也傳入done中。
之前介紹了兩種并發的模式,這里有Promise來直接實現:
1) 門:幾個均實現再繼續進行: Promise.all([….]),參數可以是由立即值,thenable或者Promise組成的數組。
注意:如果傳入空數組,那么接下來的內容就會被立即設定為完成。如果有Promise.all中有任意一個被拒絕,那么整個都被拒絕,進入到拒絕處理函數。這個模式傳入到完成處理函數中的參數是一個數組,數組中的順序與all中聲明的順序相同,與其產生的順序無關。
2) 競態:幾個中只有一個能執行:Promise.race([…]),參數與all相同,但是如果是立即值的競爭那就會顯得毫無意義,第一個立即值會勝出。
注意:一旦有一個Promise被完成,那就全部完成,如果第一個是拒絕,那么整個都被拒絕。如果傳遞空數組,那么Promise會永遠都不會被決議。
3)超時模式的實現:之前講到了會有超時模式,這里利用競態可以來實現:
// `foo()` is a Promise-aware function // `timeoutPromise(..)`, defined ealier, returns // a Promise that rejects after a specified delay // setup a timeout for `foo()` Promise.race( [ foo(), // attempt `foo()` timeoutPromise( 3000 ) // give it 3 seconds ] ) .then( function(){ // `foo(..)` fulfilled in time! }, function(err){ // either `foo()` rejected, or it just // didn"t finish in time, so inspect // `err` to know which } );
4)幾種變體:
none:所有的Promise都是拒絕才是完成 any:只要有一個完成就是完成 first:只要第一個Promise完成,那么整個就是完成 last:只有最后一個完成勝出9. 【Promise的問題】
講了那么多好處。。Promise當然也有問題:
1) 順序錯誤處理:可能會有錯誤被忽略而被全局拋出
2)單一值:只能有一個完成值、拒絕值,否則只能封裝解封,這樣會顯得有些笨重。(這個問題可以通過ES6中的...運算來方便處理~)
3) 單決議:如果講一個決議綁定到會重復進行的操作上,那么這個決議只會記住重復操作的第一次結果,如:
// `click(..)` binds the `"click"` event to a DOM element // `request(..)` is the previously defined Promise-aware Ajax var p = new Promise( function(resolve,reject){ click( "#mybtn", resolve ); } ); p.then( function(evt){ var btnID = evt.currentTarget.id; return request( "http://some.url.1/?id=" + btnID ); } ) .then( function(text){ console.log( text ); } ); //第二次按下就不會有任何操作,不會再次執行resolve
4) 慣性:已經有很多回調的代碼不會自然的進行Promise改寫
5)無法取消:如果Promise因為某些原因懸而未決的話,無法從外部阻止其繼續執行。
6)Promise會對性能有稍稍影響,但總體功大于過。
本文中的代碼除非有特別標注,均參考自:
https://github.com/getify/You...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/84808.html
摘要:前端日報精選免費的計算機編程類中文書籍英文技術文檔看不懂看印記中文就夠了的內部工作原理美團點評點餐前后端分離實踐讓你的動畫坐上時光機中文譯有多棒簡書譯別再使用圖片輪播了掘金譯如何在中使用掘金個讓增長成億美元公司的獨特方法眾成翻 2017-08-23 前端日報 精選 FPB 2.0:免費的計算機編程類中文書籍 2.0英文技術文檔看不懂?看印記中文就夠了!Virtual DOM 的內部工作...
摘要:如果沒有學習過計算機科學的程序員,當我們在處理一些問題時,比較熟悉的數據結構就是數組,數組無疑是一個很好的選擇。 showImg(https://segmentfault.com/img/bVTSjt?w=400&h=300); 1、常見 CSS 布局方式詳見: 一些常見的 CSS 布局方式梳理,涉及 Flex 布局、Grid 布局、圣杯布局、雙飛翼布局等。http://cherryb...
摘要:如果沒有學習過計算機科學的程序員,當我們在處理一些問題時,比較熟悉的數據結構就是數組,數組無疑是一個很好的選擇。 showImg(https://segmentfault.com/img/bVTSjt?w=400&h=300); 1、常見 CSS 布局方式詳見: 一些常見的 CSS 布局方式梳理,涉及 Flex 布局、Grid 布局、圣杯布局、雙飛翼布局等。http://cherryb...
摘要:原理身體會恢復,情緒會消散,不用擔心自己任務質量從此變差,人是無法容忍長期做無意義事情的,只要堅持,最終本能自我和感性自我會想辦法幫你把任務質量提上來 目標說明 1.主要面對java知識總結以及英文訓練 2.實驗期一年(2019.4.9 -2020.5.1 ) 原則 目標導向,主次分明——學習知識,不是為了懂更多知識,而是為了用知識創造價值,讓人類文明更加進步,所以,不要奢求什么都懂...
閱讀 1291·2021-09-22 15:00
閱讀 3309·2019-08-30 14:00
閱讀 1220·2019-08-29 17:27
閱讀 1220·2019-08-29 16:35
閱讀 689·2019-08-29 16:14
閱讀 2042·2019-08-26 13:43
閱讀 2117·2019-08-26 11:35
閱讀 2309·2019-08-23 15:34