同步與非同步
- 通常,程式碼是由上而下依次執行的。如果有多個任務,就必須排隊,前一個任務完成,後一個任務才能執行。這種連續的執行模式就叫做同步。
a();
b();
c();
複製程式碼
上面程式碼中,a、b、c是三個不同的函式,每個函式都是一個不相關的任務。在同步模式會先執行 a 任務,再執行 b 任務,最後執行 c 任務。當b任務是一個耗時很長的請求時,而c任務是展現新頁面時,就會導致網頁卡頓。
- 所謂非同步,就是一個任務不是連續完成的。比如,有一個讀取檔案處理的任務,任務的第一段的向作業系統發出請求,要求讀取檔案,然後程式執行其他任務,等到作業系統返回檔案,再去處理檔案。這種不連續的執行模式就叫做非同步。
a();
//立即傳送請求
ajax('url',(b)=>{
//請求回來執行
});
c();
複製程式碼
上面程式碼中,就是將b任務分成了兩部分。一部分立即執行,另一部分再請求回來後執行。也就解決了上面的問題。
總結: 同步就是大家排隊工作,非同步就是大家同時工作。
非同步的解決方案
1、CallBack
CallBack,即回撥函式,回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。即非同步操作執行完成後觸發執行的函式。
//當請求完成時就會觸發回撥函式
$.get('url',callback);
複製程式碼
回撥可以完成非同步操作,但用過 jquery 的 PE 都對下面的程式碼折磨過。
$.ajax({
url:"type",
data:1,
success:function(a){
url:"list",
data:a,
success:function(b){
$.ajax({
url:"content",
data:b,
success:function(c){
console.log(c);
}
})
}
}
})
複製程式碼
上面程式碼就是傳說中的回撥金字塔,又叫回撥地獄。這裡還只是單純的巢狀程式碼,如若再加上業務程式碼,程式碼可讀性可想而知。自己開發起來還好,如果這是別人的程式碼,你要改其中一部分足以讓人瘋掉。
2、事件釋出訂閱
我們想讀取兩個檔案時,希望這兩個檔案都被讀取完後,拿到結果。我們可以通過 node 中的 EventEmitter 類來實現,它有兩個核心方法,一個是 on(表示註冊監聽事件),一個是emit(表示發射事件)。
let fs=require('fs');
let EventEmitter=require('event');
let eve=new EventEmitter();
let arr=[];//儲存讀取內容
//監聽資料獲取成功事件,然後呼叫回撥函式
eve.on('ready',function(d){
arr.push(d);
if(arr.length==2){
//兩個檔案的資料
console.log(arr);
}
});
fs.readFile('./a.txt','utf8',function(err,data){
eve.emit('ready',data);
});
fs.readFile('./b.txt','utf8',function(err,data){
eve.emit('ready',data);
});
複製程式碼
請求 a.txt 和 b.txt 檔案資料,當成功後釋出ready事件。on 訂閱了 ready 事件,當監聽到觸發的時候,on 方法執行。
哨兵變數
let fs=require('fs');
function after(times,callback){
let arr=[];
return function(d){
arr.push(d);
if(arr.length==times){
callback(arr);
}
}
}
//2是一個哨兵變數,將讀取檔案資料成功後執行的方法作為回撥函式傳給after方法
let out=after(2,function(data){
console.log(data);
})
fs.readFile('./a.txt','utf8',function(err,data){
out(data);
});
fs.readFile('./b.txt','utf8',function(err,data){
out(data);
});
複製程式碼
上面程式碼 after 方法執行時傳入的 2 相當於一個哨兵變數,需要讀取幾個檔案的資料就傳幾。將需要讀取的檔案數量,和讀取全部檔案成功後的方法作為回撥函式傳入 after。out 為 after 執行後返回的函式,每次獲取檔案成功後執行 out 方法可以後去到最終全部檔案的資料。
不使用回撥地獄遇到的問題是:不知道讀取檔案的函式什麼時候執行完。只有當全部讀取完成後才能執行需要檔案資料的方法。
3、Generator函式
Generator 函式要用*號來標識,yield 關鍵字表示暫停執行的標記。Generator 函式是一個狀態機,封裝了多個內部狀態。呼叫一次 next 就會繼續向下執行,返回的結果是一個迭代器,所以 Generator 函式還是一個遍歷器物件生成函式。
function* read(){
let a=yield '123';
console.log(a);
let b=yield 4;
console.log(b);
}
let it = read();
console.log(it.next('321')); // {value:'123',done:false}
console.log(it.next(1)); // 1 {value:4,done:false}
console.log(it.next(2)); // 2 {value:2,done:true}
console.log(it.next(3)); // {value:undefined,done:true}
複製程式碼
上面程式碼可以看出,呼叫 Generator 函式後,返回的不是函式執行的結果,而是一個指向內部狀態的指標物件,也就是遍歷器物件。必須呼叫遍歷器物件的 next 方法,讓指標移動的下一個狀態。內部指標就會從函式開始或上次定下來的地方開始執行,直到遇到下一個 yield 語句或 return 語句為止。value 屬性表示當前的內部狀態值,是 yield 語句後面那個表達值;done 屬性是一個布林值,表示是否遍歷結束。
4、Promise
在 JavaScript 的非同步發展史中,出現了一系列解決 callback 弊端的庫,而 promise 成為了勝者,併成功地被加入了ES6標準。Promise 函式接受一個函式作為引數,該函式有兩個引數 resolve 和 reject。promise 就像一箇中介,而它只返回可信的資訊給 callback,所以 callback 一定會被非同步呼叫,且只會被呼叫一次。
let p=new Promise((resolve,reject)=>{
//to do...
if(/*非同步操作成功*/){
resolve(value);
}else{
reject(err);
}
});
p.then((value)=>{
//success
},(err)=>{
//failure
})
複製程式碼
這樣 Promise 就解決了回撥地獄的問題,比如我們連續讀取多個檔案時,寫法如下:
let fs=require('fs');
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(err,data){
if(err) reject(err);
resolve(data);
})
}
}
read('a.txt').then((data)=>{
console.log(data);
}).then(()=>{
return read('b.txt');
}).then((data)=>{
console.log(data);
}).catch((err)=>{
console.log(err);
})
複製程式碼
如此不斷的返回一個新的 Promise,這種不斷的鏈式呼叫,就擺脫了callback回撥地獄的問題和非同步程式碼非線性執行的問題。
Promise 還解決了 callback 只能捕獲當前錯誤異常。Promise 和 callback 不同,Promise 代理著所有的 callback 的報錯,可以由 Promise 統一處理。所以,可以通過catch來捕獲之前未捕獲的異常。
Promise解決了callback的回撥地獄問題,但Promise並沒有擺脫callback。所以,有沒有更好的寫法呢?
5、Async Await
async函式是ES7中的一個新特性,它結合了Promise,讓我們擺脫callback的束縛,直接用類同步的線性方式寫非同步函式,使得非同步操作變得更加方便。
let fs=require('fs');
function read(url){
return new Promise((resolve,reject)=>{
fs.readFile(url,'utf8',function(err,data){
if(err) reject(err);
resolve(data);
})
}
}
async function r(){
let a=await read('a.txt');
let b=await read('b.txt');
return a+b;
}
r().then((data)=>{
console.log(data);
});
複製程式碼
至此,非同步的 await 函式已經可以讓我們滿意。目前使用 Babel 已經支援 ES7 非同步函式的轉碼了,大家可以在自己的專案中開始嘗試。以後會不會出現更優秀的方案?以我們廣大程式群體的創造力,相信一定會有的。