異步編程上篇

16章異步編程node

隨着計算機的不斷髮展,用戶對計算機應用的要求愈來愈高,須要提供更多、更智能、響應速度 更快的功能。這就離不開異步編程的話題。同時,隨着互聯網時代的崛起,網絡應用要求可以支 持更多的併發量,這顯然也要用到大量的異步編程。那麼從這節課開始,咱們會學習到底什麼是 異步編程,以及在JS中如何實現異步編程。編程

本章咱們將學習以下內容:安全

・什麼是異步編程。網絡

•回調和Promise。多線程

生成器 Generator。併發

• ES7中的異步實現Async和Await。異步

16-1異步編程概述異步編程

16-1-1什麼是異步編程?函數

咱們先來看看到底什麼是異步。提到異步就不得不提另一個概念:同步。那什麼又叫同步呢。 不少初學者在剛接觸這個概念時會想固然的認爲同步就是同時進行。顯然,這樣的理解是錯誤 的,咱不能按字面意思去理解它。同步,英文全稱叫作Synchronization。它是指同一時間只能作 —件事,也就是說一件事情作完了才能作另一件事。學習

好比我們去火車站買票,假設窗口只有1個,那麼同一時間只能處理1我的的購票業務,其他的需 要進行排隊。這種one by one的動做就是同步。這種同步的狀況其實有不少,任何須要排隊的情 況均可以理解成同步。那若是在程序中呢,咱們都知道代碼的執行是一行接着一行的,好比下面 這段代碼:

let ary = [];

for(let i = 0;i < 100;i++){

ary[i] = i;

}

console.log(ary);

這段代碼的執行就是從上往下依次執行,循環沒執行完,輸出的代碼就不會執行,這就是典型的 同步。在程序中,絕大多數代碼都是同步的。

同步操做的優勢在於作任何事情都是依次執行,井井有理,不會存在你們同時搶一個資源的問
題。你想一想,若是火車站取消排隊機制,那麼你們勢必會爭先恐後去搶着買票,形成的結果就是 秩序大亂,甚至可能引起一系列安全問題。若是代碼不是同步執行的又會發生什麼呢?有些代碼 須要依賴前面代碼執行後的結果,但如今你們都是同時執行,那結果就不必定能獲取到。並且這 些代碼可能在對同一數據就進行操做,也會讓這個數據的值出現不肯定的狀況。

固然同步也有它的缺點。因爲是依次進行,假如其中某一個步驟花的時間比較長,那麼後續動做 就會等待它的完成,從而影響效率。

不過,在有些時候咱們仍是但願可以在效率上有所提高,也就是說可讓不少操做同時進行。這 就是另一個概念:異步。假設火車站有10我的須要買票,如今只有1個窗口提供服務,若是平 均每一個人耗費5分鐘,那麼總共須要50分鐘才能辦完全部人的業務。火車站爲了提升效率,加開 9個窗口,如今一共有10個窗口提供服務,那麼這10我的就能夠同時辦理了,總共只須要5 鍾,他們全部人的業務均可以辦完。這就是異步帶來的優點。

16-1-2異步的實現

  1. 多線程

像剛纔例子中開多個窗口的方式稱爲多線程。線程能夠理解成一個應用程序中的執行任務,每一個 應用程序至少會有一個線程,它被稱爲主線程。若是你想實現異步處理,就能夠經過開啓多個線 程,這些線程能夠同時執行。這是異步實現的一種方式。不過這種方式仍是屬於阻塞式的。

什麼叫作阻塞式呢。你想一想,開10個窗口能夠知足10我的同時買票。可是如今有100我的呢?不 可能再開90個窗口吧,因此每一個窗口實際上仍是須要排隊。也就是說雖然我能夠經過開啓多個線 程來同時執行不少任務,可是每一個任務中的代碼仍然是同步的。當某個任務的代碼執行時間過 長,也只會影響到當前線程的代碼,而其餘線程的代碼不會受到影響。

  1. 單線程非阻塞式

假設如今火車站不想開那麼多窗口,仍是隻有1個窗口提供服務,那如何可以提升購票效率呢? 咱們能夠這樣作,把購票的流程分爲兩步,第一步:預約及付款。第二步:取票。其中,第一步 可讓購票者在網上操做。第二步到火車站的窗口取票。這樣,最耗時的工做已經提早完成,不 須要排隊。到火車站時,雖然只有1個窗口,1次也只能接待1我的,可是取票的動做很快,平均 每一個人耗時不到1分鐘,10我的也就不到10分鐘就能夠處理完成。這樣既提升了效率,又少開了 窗口。這也是一種異步的實現。咱們能夠看到,開1個窗口,就至關於只有1個線程。而後把耗時 的一些操做分紅兩部分,先把快速能作完的事情作了,這樣保證它不會阻塞其餘代碼的運行。剩 下耗時的部分再單獨執行。這就是單線程阻塞式的異步實現機制。

16-1-3 JS中的異步實現

咱們知道JS引擎就是以單線程的機制來運行代碼。那麼在JS代碼中想要實現異步就只有採用單

 

線程非阻塞式的方式。好比下面這段代碼:

console.log("start");

setTimeout(function(){ console.log("timeout");

},5000);

console.log("end");

這段代碼先輸出一個字符串"start",而後用時間延遲函數,等到5000秒鐘後輸出"timeout",在代 碼的最後輸出"end"。最後的執彳丁結果是:

start

end

//等待5秒後

timeout

從結果能夠看到end的輸出並無等待時間函數執行完,實際上setTimeout就是異步的實現。代 碼的執行流程以下:

首先執行輸出字符串"start",而後開始執行setTimeout函數。因爲它是一個異步操做,因此它會 被分爲兩部分來執行,先調用setTimeout方法,而後把要執行的函數放到一個隊列中。代碼繼續 往下執行,當把全部的代碼都執行完後,放到隊列中的函數纔會被執行。這樣,全部異步執行的 函數都不會阻塞其餘代碼的執行。雖然,這些代碼都不是同時執行,可是因爲任何代碼都不會被 阻塞,因此執行效率會很快。

 

 

你們認真看這個圖片,而後思考一個問題:當setTimeout執行後,何時開始計時的呢?因爲 單線程的緣由,不可能在setTimeout後就開始執行,由於一個線程同一時間只能作一件事情。執 行後續代碼的同時就不可能又去計時。那麼只多是在全部代碼執行完後纔開始計時,而後5 後執行隊列中的回調函數,是這樣嗎?咱們用一段代碼來驗證下:

console.log("start");

setTimeout(function(){

console.log("timeout");

},5000);

for(let i = 0;i <= 500000;i++){

console.log("i:",i);

}

console.log("end");

這段代碼在以前的基礎上加了一個循環,循環次數爲50萬次,而後每次輸出i的值。這段循環是 比較耗時的,從實際運行來看,大概須要14秒左右(具體時間可自行測算)。這個時間已經遠遠 大於setTimeout的等待時間。按照以前的說法,應該先把全部同步的代碼執行完,而後再執行異 步的回調方法,結果應該是:

start

i:1

(...)//一直輸出到500000

//耗時 14秒左右

end

//等待5秒後

timeout

但實際的運行結果是:

start

i:1

(...)//一直輸出到500000

//耗時 14秒左右

end

//沒有等待

timeout

從結果能夠看UsetTimeout的計時應該是早就開始了,可是JS是單線程運行,那誰在計時呢?要 解釋這個問題,你們必定要先搞明白一件事。JS的單線程並非指整個JS引擎只有1個線程。它 是指運行代碼只有1個線程,可是它還有其餘線程來執行其餘任務。好比時間函數的計時、AJAX 技術中的和後臺交互等操做。因此,實際狀況應該是:JS引擎中執行代碼的線程開始運行代碼,
當執行到異步方法時,把異步的回調方法放入到隊列中,而後由專門計時的線程開始計時。代碼 線程繼續運行。若是計時的時間已到,那麼它會通知代碼線程來執行隊列中對應的回調函數。當 然,前提是代碼線程已經把同步代碼執行完後。不然須要繼續等待,就像這個例子中同樣。

 

 

最後,你們必定要注意一件事情,因爲執行代碼只有1個線程,因此在任何同步代碼中出現死循 環,那麼它後續的同步代碼以及異步的回調函數都沒法執行,好比:

console.log("start");

setTimeout(function(){

console.log("timeout"); },5000); console.log("end"); for(;;){}

 

timeout用於也不會輸出,由於執行代碼的線程已經陷入死循環中。

 

 

16-2 Promise實現異步

前面一講中咱們瞭解了什麼是異步,以及JS中實現異步的原理。這一節我們將學習JS中實現異 步的具體方法。前面咱們已經看到了一個用setTimeout實現的異步操做:

console.log("start");

setTimeout(function(){

console.log("timeout");

},5000);

console.log("end");

16-2-1回調函數

在調用setTimeout函數時咱們傳遞了一個函數進去,這個函數並無當即被調用,而是在5秒後 被調用。這種函數也被稱爲回調函數(關於回調函數請參看前面的內容)。因爲JS中的函數是一 等公民,它和其餘數據類型同樣,能夠做爲參數傳遞也能夠做爲返回值返回,因此常常可以看到 回調函數使用。

回調地獄

在異步實現中,回調函數的使用是不可避免的。以前我不是講過嗎,JS的異步是單線程非阻塞式 的。它將一個異步動做分爲兩步,第一步執行異步方法,而後代碼接着往下執行。而後在後面的 某個時刻調用第二步的回調函數,完成後續動做。有的時候,咱們但願在異步操做中加入同步的 行爲。好比,我想打印4句話,可是每句話都在前一句話的基礎上延遲2秒輸出。代碼以下:

setTimeout(function(){

console.log("first");

setTimeout(function(){

console.log("second");

setTimeout(function(){

console.log("third");

setTimeout(function(){

console.log("fourth");

},2000);

},2000);

},2000);

},2000);

這段代碼可以實現想要的功能,可是總以爲哪裏不對。若是輸出的內容愈來愈多,嵌套的代碼也 會增多。那不管是編寫仍是閱讀起來都會很恐怖。形成這種狀況的罪魁禍首就是回調函數。由於
你想在前面的異步操做完成後再進行接下來的動做,那隻能在它的回調函數中進行,這樣就會越 套越多,代碼愈來愈來複雜,俗稱"回調地獄"。

 

16-2-2 Promise

爲了解決這個問題,在ES6中加入了一個新的對象Promise。Promise提供了一種更合理、更強大 的異步解決方案。接下來咱們來看看它的用法。

new Promise(function(resolve,reject){

//dosomething

});

首先須要建立一個Promise對象,該對象的構造函數中接收一個回調函數,回調函數中能夠接收 兩個參數,resolve和reject。注意,這個回調函數是在Promise建立後就會調用。它實際上就是異 步操做的第一步。那第二步操做再在哪裏作呢? Promise把兩個步驟分開了,第二步經過Promise 對象的the n方法實現。

let pm = new Promise(function(resolve,reject){

//dosomething

});

console.log("go on");

pm.then(function(){ console.log("異步完成");

});

不過要注意的是,then方法的回調函數不是說只要then方法一調用它就會調用,而是在Promise 的回調函數中經過調用resolve觸發的。

let pm = new Promise(function(resolve,reject){

resolve();

});

console.log("go on");

pm.then(function(){ console.log("異步完成");

});

實際上Promise實現異步的原理和以前純用回調函數的原理是同樣的。只是Promise的作法是顯示 的將兩個步驟分開來寫。then方法的回調函數一樣會先放入隊列中,等待全部的同步方法執行完 後,同時Promise中的resolve也被調用後,該回調函數纔會執行。

 

 

調用resolve時還能夠把數據傳遞給then的回調函數。

 

let pm = new Promise(function(resolve,reject){ resolve("this is data");

});

console.log("go on"); pm.then(function(data){

console.log("異步完成",data);

});

效果:

Jie-Xie:desktop Jie$ node 1

go on

異步完成this is data

reject是出現錯誤時調用的方法。它觸發的不是then中的回調函數,而是catch中的回調函數。比 如:

let err = false;

let pm = new Promise(function(resolve,reject){

if(!err){

resolve("this is data");

 

}else{ reject("fail");

}

}); console.log("go on"); pm.then(function(data){ console.log("異步完成",data);

});

pm.catch( function(err){ console.log("出現錯誤",e rr);

});

下面,我把剛纔時間函數的異步操做用Promise實現一次。固然,其中setTimeout仍是須要使 用,只是在它外面包裹一個Promise對象。

let pm = new Promise(function(resolve,reject){ setTimeout( function(){

resolve();

},2000);

});

console.log("go on");

pm.then(function(){ console.log("異步完成");

});

效果和以前同樣,可是代碼複雜了很多,感受有點畫蛇添足。接下來作作同步效果。

let timeout = function(time){ return new Promise(function(resolve,reject){ setTimeout( function(){

resolve(); },time);

});

}

console.log("go on"); timeout(2000).then(function(){ console.log("first"); return timeout(2000);

}).then(function(){ console.log("second"); return timeout(2000);

}).then(function(){ console.log("third");

相關文章
相關標籤/搜索