js異步處理
一、什麽是異步?
我們一般喜歡把異步和同步、並行拿出來比較,我以前的理解總是很模糊,總是生硬地記著“同步就是排隊執行,異步就是一起執行”,現在一看,當初簡直就是傻,所以我們第一步先把這三個概念搞清楚,我不太喜歡看網上有些博客裏很含糊地說“xxxx是同步,xxxx是異步”,還有舉什麽通俗的例子,其實對不懂的人來說還是懵逼。
首先我們要知道這一切的根源都是“Javascript是單線程”,也就是一次只能做一件事,那麽為什麽是單線程呢?因為js渲染在瀏覽器上,包含了許多與用戶的交互,如果是多線程,那麽試想一個場景:一個線程在某個DOM上添加內容,而另一個線程刪除這個DOM,那麽瀏覽器要如何反應呢?這就亂套了。
單線程下所有的任務都是需要排隊的,而這些任務分為兩種:同步任務和異步任務,同步任務就是在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程
、而進入任務隊列
(task queue)的任務,只有任務隊列
通知主線程
,某個異步任務可以執行了,該任務才會進入主線程
執行。所以說同步執行其實也是一種只有主線程的異步執行。這裏有一個視頻關於異步操作是如何被執行的,講得非常好《what the hack is event loop》,我給大家畫個圖再來理解一下。
這裏補充說明下不同的異步操作添加到任務隊列的時機不同,如 onclick, setTimeout, ajax 處理的方式都不同,這些異步操作是由瀏覽器內核的 webcore 來執行的,webcore 包含上面提到的3種 webAPI,分別是 DOM Binding、timer、network模塊。
onclick 由瀏覽器內核的 DOM Binding 模塊來處理,當事件觸發的時候,回調函數會立即添加到任務隊列中。
setTimeout 會由瀏覽器內核的 timer 模塊來進行延時處理,當時間到達的時候,才會將回調函數添加到任務隊列中。
ajax 則會由瀏覽器內核的 network 模塊來處理,在網絡請求完成返回之後,才將回調添加到任務隊列中。
最後再來說下並行,並行是關於能夠同時發生的事情,是一種多線程的運行機制,而不管同步異步都是單線程的。
二、為什麽要用異步操作
這個很好理解,同步下前一個事件執行完了才能執行後一個事件,那麽要是遇到Ajax請求這種耗時很長的,那頁面在這段時間就沒法操作了,卡在那兒,更有甚者,萬一這個請求由於某種原因一直沒有完成,那頁面就block了,很不友好。
三、如何實現異步
我們可以通過回調函數
、Promise
、生成器
、Async/Await
等來實現異步。
今天我們先說最基礎的回調函數處理方法來實現,列舉幾個大家熟悉的使用場景,比如:ajax請求、IO操作、定時器。
ajax(url, function(){
//這就是回調函數
});
setTimeOut(function(){
//回調函數
}, 1000)
回調本身是比較好用的,但是隨著Javascript越來越成熟,對於異步編程領域的發展,回調已經不夠用了,體現在以下幾點:
1、大腦處理程序是順序的,對於復雜的回調函數會不易理解,我們需要一種更同步、更順序的方式來表達異步。
舉例說明:
//回調函數實現兩數相加
function add(getX, getY, cb){
var x, y;
getX(function(xVal){
x=xVal;
if(y!=undefined){
cb(x+y);
}
});
getY(function(){
y=yVal;
if(x!=undefined){
cb(x+y);
}
});
}
add(fetchX, fetchY, function(sum){
console.log(sum);
})
//Promise實現兩數相加
function add(xPromise, yPromise){
return Promise.all([xPromise, yPromise])
.then(function(values){
return value[0] + value[1];
});
}
//fetchX()、fetchY()返回相應值的Promise
add(fetchX(), fetchY())
.then(function(sum){
console.log(sum);
})
只看結構是不是Promise的寫法更順序話一些。
2、回調一般會把控制權交給第三方,從而帶來信任問題,比如:
- 調用回調過早
- 調用回調過晚(或未調用)
- 調用回調次數過多或過少
- 未能傳遞所需的環境和參數
- 吞掉可能出現的錯誤和異常
而Promise的特性就有效地解決了這些問題,它是如何解決的呢?
調用回調過早
這種顧慮主要是代碼是否會引入類Zalgo效應,也就是一個任務有時會同步完地成,而有時會異步地完成,這將導致竟合狀態。
Promise被定義為不能受這種顧慮的影響,因為即便是立即完成的Promise(比如 new Promise(function(resolve){ resolve(42); }))也不可能被同步地 監聽。也就是說,但你在Promise上調用then(..)的時候,即便這個Promise已經被解析了,你給then(..)提供的回調也將總是被異步地調用。
調用回調過晚
當一個Promise被調用時,這個Promise 上的then註冊的回調函數都會在下一個異步時機點上,按順序地,被立即調用。這些回調中的任意一個都無法影響或延誤對其它回調的調用。
舉例說明:
p.then( function(){
p.then( function(){
console.log( "C" );
} );
console.log( "A" );
} );
p.then( function(){
console.log( "B" );
} );
// A B C
為什麽“C”沒有排到“B”的前面?因為因為“C”所處的.then回調函數是在下一個事件循環tick。
回調未調用
這是一個很常見的顧慮。Promise用幾種方式解決它。
首先,當Promise被解析後,在代碼不出錯的情況下它一定會告知你解析結果。如果代碼有錯誤,歸類於後面的“吞掉錯誤或異常”中。
那如果Promise本身不管怎樣永遠沒有被解析呢?那麽Promise會用Promise.race來解決。
看代碼示例:
// 一個使Promise超時的工具
function timeoutPromise(delay) {
return new Promise( function(resolve,reject){
setTimeout( function(){
reject( "Timeout!" );
}, delay );
} );
}
// 為`foo()`設置一個超時
Promise.race( [
foo(), // 嘗試調用`foo()`
timeoutPromise( 3000 ) // 給它3秒鐘
] )
.then(
function(){
// `foo(..)`及時地完成了!
},
function(err){
// `foo()`不是被拒絕了,就是它沒有及時完成
// 那麽可以考察`err`來知道是哪種情況
}
);
調用次數過少或過多
正常是調用一次,“過少”就是未被調用,參考上文;“過多”的情況也很容易理解。Promise的定義方式使得它只能被決議一次,如果出於某種情況決議了多次,Promise也只會接受第一次決議,並忽略後續調用。
未能傳遞所需的參數/環境值
Promise只會有一個解析結果(完成或拒絕)。如果沒有用一個值明確地解析它,它的值就是undefined,就像JS中常見的那樣。
吞掉錯誤或異常
Promise中異常會被捕獲,並且使這個Promise被拒絕。
舉個例子:
var p = new Promise( function(resolve,reject){
foo.bar(); // `foo`沒有定義,所以這是一個錯誤!
resolve( 42 ); // 永遠不會跑到這裏 :(
} );
p.then(
function fulfilled(){
// 永遠不會跑到這裏 :(
},
function rejected(err){
// `err`將是一個來自`foo.bar()`那一行的`TypeError`異常對象
}
);
Promise就先說到這裏,關於PromiseAPI及其源碼還有生成器、Async/Await 在後續文章中整理報道。
【寫得不好的地方請大膽吐槽,非常感謝大家帶我進步。】
參考資料:
阮一峰event-loop
王福朋深入理解javascript異步系列一
你不知道的javascript
你不懂JS: 異步與性能 第三章: Promise(上)
原作者:程序媛Wendy
鏈接:https://www.jianshu.com/p/f4abe8c4fc2f
js異步處理