JavaScript非同步呼叫的發展歷程

afan發表於2018-06-27

同步與非同步

  • 通常,程式碼是由上而下依次執行的。如果有多個任務,就必須排隊,前一個任務完成,後一個任務才能執行。這種連續的執行模式就叫做同步。
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 非同步函式的轉碼了,大家可以在自己的專案中開始嘗試。以後會不會出現更優秀的方案?以我們廣大程式群體的創造力,相信一定會有的。

JavaScript非同步呼叫的發展歷程就到這裡了,如果您覺得文章有用,快去點贊(⊙﹏⊙)!

相關文章