細說JS異步發展歷程

知其然知其所以然,首先了解三個概念:

1.什麼是同步?

所謂同步,就是在發出一個"調用"時,在沒有得到結果之前,該“調用”就不返回。但是一旦調用返回,就得到返回值了。換句話說,就是由“調用者”主動等待這個“調用”的結果。此調用執行完之前,阻塞之後的代碼執行。

2.什麼是異步?

"調用"在發出之後,這個調用就直接返回了,所以沒有返回結果。換句話說,當一個異步過程調用發出後,調用者不會立刻得到結果。而是在"調用"發出後,"被調用者"通過狀態、通知來通知調用者,或通過回調函數處理這個調用。異步調用發出後,不影響後面代碼的執行。

3.JavaScript 中爲什麼需要異步?

首先我們知道JavaScript是單線程的(即使新增了webworker,但是本質上JS還是單線程)。同步代碼意味着什麼呢?意味着有可能會阻塞,當我們有一個任務需要時間較長時,如果使用同步方式,那麼就會阻塞之後的代碼執行。而異步則不會,我們不會等待異步代碼的之後,繼續執行異步任務之後的代碼。

概念瞭解完了,我們就要進入今天的正題了。首先大家思考一下:平時在工作中,主要使用了哪些異步解決方案,這些異步方案有什麼優缺點?

異步最早的解決方案是回調函數,如事件的回調,setInterval/setTimeout中的回調。但是回調函數有一個很常見的問題,就是回調地獄的問題(稍後會舉例說明);

爲了解決回調地獄的問題,社區提出了Promise解決方案,ES6將其寫進了語言標準。Promise一定程度上解決了回調地獄的問題,但是Promise也存在一些問題,如錯誤不能被try catch,而且使用Promise的鏈式調用,其實並沒有從根本上解決回調地獄的問題,只是換了一種寫法。

ES6中引入 Generator 函數,Generator是一種異步編程解決方案,Generator 函數是協程在 ES6 的實現,最大特點就是可以交出函數的執行權,Generator 函數可以看出是異步任務的容器,需要暫停的地方,都用yield語句註明。但是 Generator 使用起來較爲複雜。

ES7又提出了新的異步解決方案:async/await,async是 Generator 函數的語法糖,async/await 使得異步代碼看起來像同步代碼,異步編程發展的目標就是讓異步邏輯的代碼看起來像同步一樣。

回調函數 ---> Promise ---> Generator ---> async/await.

1.回調函數: callback

//node讀取文件
fs.readFile(xxx, 'utf-8', function(err, data) {
    //code
});

回調函數的使用場景(包括但不限於):

  1. 事件回調
  2. Node API
  3. setTimeout/setInterval中的回調函數
  4. ajax 請求
回調函數的優點: 簡單。

回調函數的缺點:

異步回調嵌套會導致代碼難以維護,並且不方便統一處理錯誤,不能 try catch 和 回調地獄(如先讀取A文本內容,再根據A文本內容讀取B再根據B的內容讀取C...)。

fs.readFile(A, 'utf-8', function(err, data) {
    fs.readFile(B, 'utf-8', function(err, data) {
        fs.readFile(C, 'utf-8', function(err, data) {
            fs.readFile(D, 'utf-8', function(err, data) {
                //....
            });
        });
    });
});

2.Promise

Promise 一定程度上解決了回調地獄的問題,Promise 最早由社區提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。

那麼我們看看Promise是如何解決回調地獄問題的,仍然以上文的readFile 爲例(先讀取A文本內容,再根據A文本內容讀取B再根據B的內容讀取C)。

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});
Promise 的優點:
  1. 一旦狀態改變,就不會再變,任何時候都可以得到這個結果
  2. 可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數
缺點:
  1. 無法取消 Promise
  2. 當處於pending狀態時,無法得知目前進展到哪一個階段
  3. 錯誤不能被 try catch

假設有這樣一個需求:讀取A,B,C三個文件內容,都讀取成功後,再輸出最終的結果。在Promise之前,我們一般可以藉助發佈訂閱模式去實現:

let pubsub = {
    arry: [],
    emit() {
        this.arry.forEach(fn => fn());
    },
    on(fn) {
        this.arry.push(fn);
    }
}

let data = [];
pubsub.on(() => {
    if(data.length === 3) {
        console.log(data);
    }
});
fs.readFile(A, 'utf-8', (err, value) => {
    data.push(value);
    pubsub.emit();
});
fs.readFile(B, 'utf-8', (err, value) => {
    data.push(value);
    pubsub.emit();
});
fs.readFile(C, 'utf-8', (err, value) => {
    data.push(value);
    pubsub.emit();
});

Promise給我們提供了 Promise.all 的方法,對於這個需求,我們可以使用 Promise.all 來實現。

/**
 * 將 fs.readFile 包裝成promise接口
 */
function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}

/**
 * 使用 Promise
 * 
 * 通過 Promise.all 可以實現多個異步並行執行,同一時刻獲取最終結果的問題
 */
Promise.all([
    read(A),
    read(B),
    read(C)
]).then(data => {
    console.log(data);
}).catch(err => console.log(err));

可執行代碼可戳: https://github.com/YvetteLau/...

3.Generator

Generator 函數是 ES6 提供的一種異步編程解決方案,整個 Generator 函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用 yield 語句註明。

Generator 函數一般配合 yield 或 Promise 使用。Generator函數返回的是迭代器。對生成器和迭代器不瞭解的同學,請自行補習下基礎。下面我們看一下 Generator 的簡單使用:

function* gen() {
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
//next方法可以帶一個參數,該參數就會被當作上一個yield表達式的返回值
t.next(1); //第一次調用next函數時,傳遞的參數無效
t.next(2); //a輸出2;
t.next(3); //b輸出2; 
t.next(4); //c輸出3;
t.next(5); //d輸出3;

爲了讓大家更好的理解上面代碼是如何執行的,我畫了一張圖,分別對應每一次的next方法調用:

仍然以上文的 readFile (先讀取A文本內容,再根據A文本內容讀取B再根據B的內容讀取C)爲例,使用 Generator + co庫來實現:

const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
    yield readFile(A, 'utf-8');
    yield readFile(B, 'utf-8');
    yield readFile(C, 'utf-8');
    //....
}
co(read()).then(data => {
    //code
}).catch(err => {
    //code
});

Generator的缺點大約不用我說了,除非是找虐,不然一般不會直接使用 Generator 來解決異步的(當然也不排除是因爲我不熟練)~~~

不使用co庫,如何實現?能否自己寫一個最簡的 my_co,有助於理解 async/await 的實現原理 ?請戳: https://github.com/YvetteLau/...

PS: 如果你還不太瞭解 Generator/yield,建議閱讀ES6相關文檔。

4.async/await

ES7中引入了 async/await 概念。async 其實是一個語法糖,它的實現就是將 Generator函數和自動執行器(co),包裝在一個函數中。

async/await 的優點是代碼清晰,不用像 Promise 寫很多 then 鏈,就可以處理回調地獄的問題。並且錯誤可以被try catch。

仍然以上文的readFile (先讀取A文本內容,再根據A文本內容讀取B再根據B的內容讀取C) 爲例,使用 async/await 來實現:

const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);


async function read() {
    await readFile(A, 'utf-8');
    await readFile(B, 'utf-8');
    await readFile(C, 'utf-8');
    //code
}

read().then((data) => {
    //code
}).catch(err => {
    //code
});

使用 async/await 實現此需求:讀取A,B,C三個文件內容,都讀取成功後,再輸出最終的結果。

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}

async function readAsync() {
    let data = await Promise.all([
        read(A),
        read(B),
        read(C)
    ]);
    return data;
}

readAsync().then(data => {
    console.log(data);
});

所以JS的異步發展史,可以認爲是從 callback -> promise -> generator -> async/await。async/await 使得異步代碼看起來像同步代碼,異步編程發展的目標就是讓異步邏輯的代碼看起來像同步一樣。

因本人水平有限,文中內容未必百分百正確,如有不對的地方,請給我留言,謝謝。

邀請你加入 Step-By-Step 項目

不積跬步無以至千里。 我是公衆號【前端宇宙】作者劉小夕,我將和大家一起一步一個腳印,向前端專家邁進。
Step-By-Step

每個工作日我會發佈一個前端相關的問題(目的是爲了切實掌握相關的知識點),歡迎在 Issue 區留下你的答案。

節假日不會發布任何問題,希望大家能夠利用節假日回顧一週所學。每週末我會進行一次彙總(整理出最優答案),以便大家回顧。

參考文章:

[1] 珠峯架構課(牆裂推薦)

[2] 細說JavaScript異步函數發展歷程

[3] ES6 Promise

[4] ES6 Generator

[5] ES6 async

[6] JavaScript異步編程

謝謝各位小夥伴願意花費寶貴的時間閱讀本文,如果本文給了您一點幫助或者是啓發,請不要吝嗇你的贊和Star,您的肯定是我前進的最大動力。https://github.com/YvetteLau/...

關注小姐姐的公衆號,加入交流羣。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章