隨著計算機的不斷發展,用戶對計算機應用的要求越來越高,需要提供更多、更智能、響應速度更快的功能。這就離不開非同步編程的話題。同時,隨著互聯網時代的崛起,網路應用要求能夠支持更多的並發量,這顯然也要用到大量的非同步編程。接下來我將從以下幾方面來闡述:

什麼是非同步編程。回調和Promise。生成器Generator。ES7中的非同步實現Async和Await。

什麼是非同步編程?

我們先來看看到底什麼是非同步。提到非同步就不得不提另外一個概念:同步。那什麼又叫同步呢。很多初學者在剛接觸這個概念時會想當然的認為同步就是同時進行。顯然,這樣的理解是錯誤的,咱不能按字面意思去理解它。同步,英文全稱叫做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分鐘,他們所有人的業務都可以辦完。這就是非同步帶來的優勢。

非同步的實現

多線程

像剛才例子中開多個窗口的方式稱為多線程。線程可以理解成一個應用程序中的執行任務,每個應用程序至少會有一個線程,它被稱為主線程。如果你想實現非同步處理,就可以通過開啟多個線程,這些線程可以同時執行。這是非同步實現的一種方式。不過這種方式還是屬於阻塞式的。什麼叫做阻塞式呢。你想想,開10個窗口可以滿足10個人同時買票。但是現在有100個人呢?不可能再開90個窗口吧,所以每個窗口實際上還是需要排隊。也就是說雖然我可以通過開啟多個線程來同時執行很多任務,但是每個任務中的代碼仍然是同步的。當某個任務的代碼執行時間過長,也只會影響到當前線程的代碼,而其他線程的代碼不會受到影響。

單線程非阻塞式

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

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

從結果可以看到setTimeout的計時應該是早就開始了,但是JS是單線程運行,那誰在計時呢?要解釋這個問題,大家一定要先搞明白一件事。JS的單線程並不是指整個JS引擎只有1個線程。它是指運行代碼只有1個線程,但是它還有其他線程來執行其他任務。比如時間函數的計時、AJAX技術中的和後台交互等操作。所以,實際情況應該是:JS引擎中執行代碼的線程開始運行代碼,當執行到非同步方法時,把非同步的回調方法放入到隊列中,然後由專門計時的線程開始計時。代碼線程繼續運行。如果計時的時間已到,那麼它會通知代碼線程來執行隊列中對應的回調函數。當然,前提是代碼線程已經把同步代碼執行完後。否則需要繼續等待,就像這個例子中一樣。

最後,大家一定要注意一件事情,由於執行代碼只有1個線程,所以在任何同步代碼中出現死循環,那麼它後續的同步代碼以及非同步的回調函數都無法執行,比如:

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

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

回調函數

在調用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);

這段代碼能夠實現想要的功能,但是總覺得哪裡不對。如果輸出的內容越來越多,嵌套的代碼也會增多。那無論是編寫還是閱讀起來都會很恐怖。造成這種情況的罪魁禍首就是回調函數。因為你想在前面的非同步操作完成後再進行接下來的動作,那隻能在它的回調函數中進行,這樣就會越套越多,代碼越來越來複雜,俗稱「回調地獄」。

Promise

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

new Promise(function(resolve,reject){
//dosomething
});

首先需要創建一個Promise對象,該對象的構造函數中接收一個回調函數,回調函數中可以接收兩個參數,resolve和reject。注意,這個回調函數是在Promise創建後就會調用。它實際上就是非同步操作的第一步。那第二步操作再在哪裡做呢?Promise把兩個步驟分開了,第二步通過Promise對象的then方法實現。

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);
});

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("出現錯誤",err);
});

下面,我把剛才時間函數的非同步操作用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");
return timeout(2000);
}).then(function(){
console.log("fourth");
return timeout(2000);
});

由於需要多次創建Promise對象,所以用了timeout函數將它封裝起來,每次調用它都會返回一個新的Promise對象。當then方法調用後,其內部的回調函數默認會將當前的Promise對象返回。當然也可以手動返回一個新的Promise對象。我們這裡就手動返回了一個新的計時對象,因為需要重新開始計時。後面繼續用then方法來觸發非同步完成的回調函數。這樣就可以做到同步的效果,從而避免了過多的回調嵌套帶來的「回調地獄」問題。

實際上Promise的應用還是比較多,比如前面講到的fetch,它就利用了Promise來實現AJAX的非同步操作:

let pm = fetch("/users"); // 獲取Promise對象
pm.then((response) => response.text()).then(text => {
test.innerText = text; // 將獲取到的文本寫入到頁面上
})
.catch(error => console.log("出錯了"));

注意:response.text()返回的不是文本,而是Promise對象。所以後面又跟了一個then,然後從新的Promise對象中獲取文本內容。

Promise作為ES6提供的一種新的非同步編程解決方案,但是它也有問題。比如,代碼並沒有因為新方法的出現而減少,反而變得更加複雜,同時理解難度也加大。因此它並不是非同步實現的最終形態,後續我們還會繼續介紹其他的非同步實現方法。

迭代器和生成器

Promise來實現非同步也會存在一些問題,比如代碼量增多,不易理解。接下來看看另外一種非同步操作的方法—-生成器。這是ES6新增的方法,在講它之前,咱們還得理解另外一個東西:迭代器。

迭代器(Iterator)

迭代器是一種介面,也可以說是一種規範。它提供了一種統一的遍曆數據的方法。我們都知道數組、集合、對象都有自己的循環遍歷方法。比如數組的循環:

let ary = [1,2,3,4,5,6,7,8,9,10];

//for循環
for(let i = 0;i < ary.length;i++){
console.log(ary[i]);
}

//forEach循環
ary.forEach(function(ele){
console.log(ele);
});

//for-in循環
for(let i in ary){
console.log(ary[i]);
}

//for-of循環
for(let ele of ary){
console.log(ele);
}

集合的循環:

let list = new Set([1,2,3,4,5,6,7,8,9,10]);
for(let ele of list){
console.log(ele);
}

對象的循環:

let obj = {
name : tom,
age : 25,
gender : 男,
intro : function(){
console.log(my name is +this.name);
}
}

for(let attr in obj){
console.log(attr);
}

從以上的代碼可以看到,數組可以用for、forEach、for-in以及for-of來遍歷。集合能用for-of。對象能用for-in。也就是說,以上數據類型的遍歷方式都各有不同,那麼有沒有統一的方式遍歷這些數據呢?這就是迭代器存在的意義。它可以提供統一的遍曆數據的方式,只要在想要遍歷的數據結構中添加一個支持迭代器的屬性即可。這個屬性寫法是這樣的:

const obj = {
[Symbol.iterator]:function(){}
}

[Symbol.iterator]屬性名是固定的寫法,只要擁有了該屬性的對象,就能夠用迭代器的方式進行遍歷。迭代器的遍歷方法是首先獲得一個迭代器的指針,初始時該指針指向第一條數據之前。接著通過調用next方法,改變指針的指向,讓其指向下一條數據。每一次的next都會返回一個對象,該對象有兩個屬性。其中value代表想要獲取的數據,done是個布爾值,false表示當前指針指向的數據有值。true表示遍歷已經結束。

let ary = [1,2,3];
let it = ary[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 }

數組是支持迭代器遍歷的,所以可以直接獲取其中的迭代器。集合也是一樣。

let list = new Set([1,2,3]);
let it = list.entries(); // 獲取set集合中的迭代器
console.log(it.next()); // { value: [ 1, 1 ], done: false }
console.log(it.next()); // { value: [ 2, 2 ], done: false }
console.log(it.next()); // { value: [ 3, 3 ], done: false }
console.log(it.next()); // { value: undefined, done: true }

set集合中每次遍歷出來的值是一個數組,裡面的第一和第二個元素都是一樣的。

由於數組和集合都支持迭代器,所以它們都可以用同一種方式來遍歷。es6中提供了一種新的循環方法叫做for-of。它實際上就是使用迭代器來進行遍歷,換句話說只有支持了迭代器的數據結構才能使用for-of循環。在JS中,默認支持迭代器的結構有:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數的 arguments 對象
  • NodeList 對象

這裡面並沒有包含自定義的對象,所以當我們創建一個自定義對象後,是無法通過for-of來循環遍歷它。除非將iterator介面加入到該對象中:

let obj = {
name : tom,
age : 25,
gender : 男,
intro : function(){
console.log(my name is +this.name);
},
[Symbol.iterator]:function(){
let i = 0;
let keys = Object.keys(this); // 獲取當前對象的所有屬性並形成一個數組
return {
next: function(){
return {
value:keys[i++], // 外部每次執行next都能得到數組中的第i個元素
done:i > keys.length // 如果數組的數據已經遍歷完則返回true
}
}
}
}
}

for(let attr of obj){
console.log(attr);
}

通過自定義迭代器就能讓自定義對象使用for-of循環。

迭代器的概念及使用方法我們清楚了,接下來就是生成器。

生成器(Generator)

生成器也是ES6新增加的一種特性。它的寫法和函數非常相似,只是在聲明時多了一個」*」號。

function* say(){}
const say = function*(){}

注意:這個」*」只能寫在function關鍵字的後面。

生成器函數和普通函數並不只是一個「*」號的區別。普通函數在調用後,必然開始執行該函數,直到函數執行完或遇到return為止。中途是不可能暫停的。但是生成器函數則不一樣,它可以通過yield關鍵字將函數的執行掛起,或者理解成暫停。它的外部在通過調用next方法,讓函數繼續執行,直到遇到下一個yield,或函數執行完畢。

function* say(){
yield "開始";
yield "執行中";
yield "結束";
}
let it = say(); // 調用say方法,得到一個迭代器
console.log(it.next()); // { value: 開始, done: false }
console.log(it.next()); // { value: 執行中, done: false }
console.log(it.next()); // { value: 結束, done: false }
console.log(it.next()); // { value: undefined, done: true }

調用say函數,這句和普通函數的調用沒什麼區別。但是此時say函數並沒有執行,而是返回了一個該生成器的迭代器對象。接下來就和之前一樣,執行next方法,say函數執行,當遇到yield時,函數被掛起,並返回一個對象。對象中包含value屬性,它的值是yield後面跟著的數據。並且done的值為false。再次執行next,函數又被激活,並繼續往下執行,直到遇到下一個yield。當所有的yield都執行完了,再次調用next時得到的value就是undefined,done的值為true。

如果你能理解剛才講的迭代器,那麼此時的生成器也就很好理解了。它的yield,其實就是next方法執行後掛起的地方,並得到你返回的數據。那麼這個生成器有什麼用呢?它的yield關鍵字可以將執行的代碼掛起,外部通過next方法讓它繼續運行。這和非同步操作的原理非常類似,把一個操作分為兩部分,先執行一部分,然後再執行另外一部分。所以生成器可以處理和非同步相關的操作。我們知道,非同步操作主要是依靠回調函數實現。但是純回調函數的方式去處理同步效果會帶來「回調地域「的問題.Promise可以解決這個問題。但是Promise寫起來代碼比較複雜,不易理解。而生成器又提供了一種解決方案。看下面這個例子:

function* delay(){
yield new Promise((resolve,reject) => {setTimeout(()=>{resolve()},2000)})
console.log("go on");
}
let it = delay();
it.next().value.then(()=>{
it.next();
});

這個例子實現了等待2秒鐘後,列印字元串」go on」。下面我們來分析下這段代碼。在delay這個生成器中,yield後面跟了一個Promise對象。這樣,當外部調用next時就能得到這個Promise對象。然後調用它的then函數,等待2秒鐘後Promise中會調用resolve方法,接著then中的回調函數被調用。也就是說,此時指定的等待時間已到。然後在then的回調函數中繼續調用生成器的next方法,那麼生成器中的代碼就會繼續往下執行,最後輸出字元串」go on」。

例子中時間函數外面為什麼要包裹一個Promise對象呢?這是因為時間函數本身就是一個非同步方法,給它包裹一個Promise對象後,外部就可以通過then方法來處理非同步操作完成後的動作。這樣,在生成器中,就可以像寫同步代碼一樣來實現非同步操作。比如,利用fetch來獲取遠程伺服器的數據(為了測試方便,我將用MockJS來攔截請求)。

Mock.mock(/.json/,{
stuents|5-10 : [{
id|+1 : 1,
name : @cname,
gender : /[男女]/, //在正則表達式匹配的範圍內隨機
age|15-30 : 1, //年齡在15-30之間生成,值1隻是用來確定數據類型
phone : /1d{10}/,
addr : @county(true), //隨機生成中國的一個省、市、縣數據
date : "@date(yyyy-MM-dd)"
}]
});
function* getUsers(){
let data = yield new Promise((resolve,reject) => {
$.ajax({
type:"get",
url:"/users.json",
success:function(data){
resolve(data)
}
});
});
console.log("data",data);
}
let it = getUsers();
it.next().value.then((data) => {
it.next(data);
});

在Promise中調用JQuery的AJAX方法,當數據返回後調用resolve,觸發外部then方法的回調函數,將數據返回給外部。外部的then方法接收到data數據後,再次調用next,移動生成器的指針,並將data數據傳遞給生成器。所以,在生成器中你可以看到,我聲明了一個data變數來接收非同步操作返回的數據,這裡的代碼就像同步操作一樣,但實際上它是個非同步操作。當非同步的數據返回後,才會執行後面的列印操作。這裡的關鍵代碼就是yield後面一定是一個Promise對象,因為只有這樣外部才能調用then方法來等待非同步處理的結果,然後再繼續做接下來的操作。

之前我們還講過一個替代AJAX的方法fetch,它本身就是用Promise的方法來實現非同步,所以代碼寫起來會更簡單:

function* getUsers(){
let response = yield fetch("/users");
let data = yield response.json();
console.log("data",data);
}
let it = getUsers();
it.next().value.then((response) => {
it.next(response).value.then((data) => {
it.next(data);
});
});

由於mock無法攔截fetch請求,所以我用nodejs+express搭建了一個mock-server伺服器。

這裡的生成器我用了兩次yield,這是因為fetch是一個非同步操作,獲得了響應信息後再次調用json方法來得到其中返回的JSON數據。這個方法也是個非同步操作。

從以上幾個例子可以看出,如果單看生成器的代碼,非同步操作可以完全做的像同步代碼一樣,比起之前的回調和Promise都要簡單許多。但是,生成器的外部還是需要做很多事情,比如需要頻繁調用next,如果要做同步效果依然需要嵌套回調函數,代碼依然很複雜。市面也有很多的插件可以輔助我們來執行生成器,比如比較常見的co模塊。它的使用很簡單:

co(getUsers);

引入co模塊後,將生成器傳入它的方法中,這樣它就能自動執行生成器了。關於co模塊這裡我就不再多講,有興趣的話可以參考這篇文章:es6.ruanyifeng.com/#

async和await

生成器這種方式需要編寫外部的執行器,而執行器的代碼寫起來一點也不簡單。當然也可以使用一些插件,比如co模塊來簡化執行器的編寫。

在ES7中,加入了async函數來處理非同步。它實際上只是生成器的一種語法糖而已,簡化了外部執行器的代碼,同時利用await替代yield,async替代生成器的(*)號。下面還是來看個例子:

async function delay(){
await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)});
console.log("go on);
}
delay();

這個例子我們之前用生成器也寫過,其中把生成器的(*)號被換成了async。async關鍵字必須寫在function的前面。如果是箭頭函數,則寫在參數的前面:

const delay = async () => {}

在函數中,第一句用了await。它替代了之前的yield。後面同樣需要跟上一個Promise對象。接下來的列印語句會在上面的非同步操作完成後執行。外部調用時就和正常的函數調用一樣,但它的實現原理和生成器是類似的。因為有了async關鍵字,所以它的外部一定會有相應的執行器來執行它,並在非同步操作完成後執行回調函數。只不過這一切都被隱藏起來了,由JS引擎幫助我們完成。我們需要做的就是加上關鍵字,在函數中使用await來執行非同步操作。這樣,可以大大的簡化非同步操作。同時,能夠像同步方法一樣去處理它們。

接下來我們再來看看更細節的一些問題。await後面必須是一個Promise對象,這個很好理解。因為該Promise對象會返回給外部的執行器,並在非同步動作完成後執行resolve,這樣外部就可以通過回調函數處理它,並將結果傳遞給生成器。

如圖:

那如果await後面跟的不是Promise對象又會發生什麼呢?

const delay = async () => {
let data = await "hello";
console.log(data);
}

這樣的代碼是允許的,不過await會自動將hello字元串包裝一個Promise對象。就像這樣:

let data = await new Promise((resolve,reject) => resolve("hello"));

創建了Promise對象後,立即執行resolve,並將字元串hello傳遞給外部的執行器。外部執行器的回調函數再將這個hello傳遞迴來,並賦值給data變數。所以,執行該代碼後,馬上就會輸出字元串hello。雖然代碼能夠這樣寫,但是await在這裡的意義並不大,所以await還是應該用來處理非同步方法,同時該非同步方法應該使用Promise對象。

async函數裡面除了有await關鍵字外,感覺和其他函數沒什麼區別,那它能有返回值嗎?答案是肯定的,

const delay = async () => {
await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)});
return "finish";
}
let result = delay();
console.log(result);

在delay函數中先執行等待2秒的非同步操作,然後返回字元串finish。外部調用時我用一個變數接收它的返回值。最後輸出的結果是:

// 沒有任何等待立即輸出
Promise { <pending> }
// 2秒後程序結束

我們可以看到,沒有任何等待立即輸出了一個Promise對象。而整個程序是在2秒鐘後才結束的。由此看出,獲取async函數的返回結果實際上是return出來的一個Promise對象。假如return後面跟著的本來就是一個Promise對象,那麼它會直接返回。但如果不是,則會像await一樣包裹一個Promise對象返回。所以,想要得到返回的具體內容應該這樣:

const delay = async () => {
await new Promise((resolve) => {setTimeout(()=>{resolve()},2000)});
return "finish";
}
let result = delay();
console.log(result);
result.then(function(data){
console.log("data:",data);
});

執行的結果:

// 沒有任何等待立即輸出
Promise { <pending> }
//等待2秒後輸出
data: finish

那如果函數沒有任何返回值,得到的又是什麼呢?我將上面代碼中取掉return,再次運行:

// 沒有任何等待立即輸出
Promise { <pending> }
//等待2秒後輸出
data: undefined

可以看到,仍然可以得到Promise對象,但由於函數沒有返回值,所以就不會有任何數據傳遞出來,那麼列印的結果就是undefined。

async的基本原理我們清楚了,下面我們把之前的AJAX例子用async重寫下:

Mock.mock(/.json/,{
stuents|5-10 : [{
id|+1 : 1,
name : @cname,
gender : /[男女]/, //在正則表達式匹配的範圍內隨機
age|15-30 : 1, //年齡在15-30之間生成,值1隻是用來確定數據類型
phone : /1d{10}/,
addr : @county(true), //隨機生成中國的一個省、市、縣數據
date : "@date(yyyy-MM-dd)"
}]
});
async function getUsers(){
let data = await new Promise((resolve,reject) => {
$.ajax({
type:"get",
url:"/users.json",
success:function(data){
resolve(data)
}
});
});
console.log("data",data);
}
getUsers();

這是用JQuery的AJAX方法實現。

async function getUsers(){
let response = await fetch("/users");
let data = await response.json();
console.log("data",data);
}
getUsers();

這是fetch方法的實現。

從這兩個例子可以看出,async和生成器兩種方式都很類似,但async可以不藉助任何的第三方模塊,也更易於理解,async表示該函數要做非同步處理。await表示後面的代碼是一個非同步操作,等待該非同步操作完成後再執行後面的動作。如果非同步操作有返回的數據,則在左邊用一個變數來接收它。我們知道,await可以讓非同步操作變為同步的效果。但是,有的時候為了提高效率,我們需要讓多個非同步操作同時進行怎麼辦呢?方法就是執行非同步方法時不加await,這樣它們就可以同時進行,然後在獲取結果時用await。比如:

function time(ms){
return new Promise((resolve,reject) => {
setTimeout(()=>{resolve()},ms);
});
}
const delay = async () => {
let t1 = time(2000);
let t2 = time(2000);
await t1;
console.log("t1 finish");
await t2;
console.log("t2 finish");
}
delay();

我先把時間函數的非同步操作封裝成了函數,並返回Promise對象。在delay函數中調用了兩次time方法,但沒有用await。也就是說這兩個時間函數的執行是「同時」(其實還是有先後順序)進行的。然後將它們的Promise對象分別用t1和t2表示。先用await t1。表示等待t1的非同步處理完成,然後輸出t1 finish。接著再用await t2,等待t2的非同步處理完成,最後輸出t2 finish。由於這兩個時間函數是同時執行,而且它們的等待時間也是一樣的。所以,當2秒過後,它們都會執行相應的回調函數。運行的結果就是:等待2秒後,先輸出t1 finish,緊接著立即輸出 t2 finish。

const delay = async () => {
await time(2000);
console.log("t1 finish");
await time(2000);;
console.log("t2 finish");
}

如果是這樣寫,那麼執行的結果會是等待2秒後輸出t1 finish。再等待2秒後輸出t2 finish。

async確實是一個既好用、又簡單的非同步處理方法。但是它的問題就是不兼容老的瀏覽器,只有支持了ES7的瀏覽器才能使用它。最後,還需要注意一個問題:await關鍵字必須寫在async定義的函數中。
推薦閱讀:
相关文章