本文的示例代碼參考這裏的async

目錄

  • 引言

  • callback

    • async
  • Promise

    • Promise對象

    • bluebird

  • Generator

    • co
  • async/await

  • 小結

引言

眾所周知 JavaScript語言的執行環境是”單線程” 這一點大大降低了併發編程的門檻

但是 如何在降低門檻的同時保證性能呢? 答應就是 異步

因此 本文就來詳細討論JavaScript異步編程的方法

callback

callback又稱為回調 是JavaScript編程中最基本的異步處理方法

例如 下面讀取文件的代碼

// callback.js
var fs = require('fs');

fs.readFile('file1.txt', function (err, data) {
    console.log("file1.txt: " + data.toString());

    fs.readFile('file2.txt', function (err, data) {
        console.log("file2.txt: " + data.toString());

        fs.readFile('file3.txt', function (err, data) {
            console.log("file3.txt: " + data.toString());
        });
    });
});

其中 測試文件的內容分別是

// file1.txt
file1

// file2.txt
file2

// file3.txt
file3

使用babel-node執行callback.js文件 打印結果如下

file1.txt: file1
file2.txt: file2
file3.txt: file3

關於babel-node的更多介紹請參考JavaScript學習 之 版本

async

上述只是順序執行異步回調的簡單示例 為了實現更複雜的異步控制 我們可以藉助第三方庫async

async最基本的有以下三個控制流程

series

parallel

waterfall

<!–more–>

  • series 順序執行 但沒有數據交互

例如上述讀取文件的例子 使用async這樣實現

// async.js
var fs = require('fs');
var async = require('async');

async.series([
    function (callback) {
        fs.readFile('file1.txt', function (err, data) {
            callback(null, 'file1.txt: ' + data.toString());
        });
    },
    function (callback) {
        fs.readFile('file2.txt', function (err, data) {
            callback(null, 'file2.txt: ' + data.toString());
        });
    },
    function (callback) {
        fs.readFile('file3.txt', function (err, data) {
            callback(null, 'file3.txt: ' + data.toString());
        });
    }
],
    function (err, results) {
        console.log(results);
    });

在使用async之前 需要安裝依賴: npm i –save async

使用babel-node執行async.js文件 打印結果如下

[ 'file1.txt: file1', 'file2.txt: file2', 'file3.txt: file3' ]
  • parallel 并行執行

如果想實現同時讀取多個文件的功能 使用async這樣實現

// async.js
async.parallel([
    function (callback) {
        fs.readFile('file1.txt', function (err, data) {
            callback(null, 'file1.txt: ' + data.toString());
        });
    },
    function (callback) {
        fs.readFile('file2.txt', function (err, data) {
            callback(null, 'file2.txt: ' + data.toString());
        });
    },
    function (callback) {
        fs.readFile('file3.txt', function (err, data) {
            callback(null, 'file3.txt: ' + data.toString());
        });
    }
],
    function (err, results) {
        console.log(results);
    });

使用babel-node執行async.js文件 打印結果如下

[ 'file1.txt: file1', 'file2.txt: file2', 'file3.txt: file3' ]

由於這裏的文件內容都比較小 所以結果看起來還是順序執行 但其實是并行執行的

  • waterfall 順序執行 且有數據交互
// async.js
var fs = require('fs');
var async = require('async');

async.waterfall([
    function (callback) {
        fs.readFile('file1.txt', function (err, data) {
            callback(null, 'file1.txt: ' + data.toString());
        });
    },
    function (n, callback) {
        fs.readFile('file2.txt', function (err, data) {
            callback(null, [n, 'file2.txt: ' + data.toString()]);
        });
    },
    function (n, callback) {
        fs.readFile('file3.txt', function (err, data) {
            callback(null, [n[0], n[1], 'file3.txt: ' + data.toString()]);
        });
    }
],
    function (err, results) {
        console.log(results);
    });

使用babel-node執行async.js文件 打印結果如下

[ 'file1.txt: file1', 'file2.txt: file2', 'file3.txt: file3' ]

當然 async的功能還遠不止這些 例如 auto等更強大的流程控制等 讀者想深入了解的話可以參考這裏

Promise

對於簡單項目來說 使用上述async的方式完全可以滿足需求

但是 基於回調的方法在較複雜的項目中 仍然不夠簡潔

因此 基於Promise的異步方法應運而生

在開始使用Promise之前 我們需要搞清楚 什麼是Promise?

Promise是一種規範 目的是為異步編程提供統一接口

那麼使用Promise時 接口是被統一成什麼樣子了呢?

return step1().then(step2).then(step3).catch(function(err){
  // err
});

從上面的例子 我們可以看出Promise有以下三個特點

返回Promise

鏈式操作

then/catch流程控制

當然 除了上述順序執行的控制流程 Promise也支持并行執行的控制流程

var promise123 = Promise.all([promise1, promise2, promise3]);

Promise對象

了解了Promise的原理和使用之後 我們就可以開始調用封裝成Promise的代碼了

但是 如果遇到需要自己封裝Promise的情況 怎麼辦呢?

可以 使用ES6提供的Promise對象

關於ES6以及JavaScript版本的詳細介紹 可以參考JavaScript學習 之 版本

例如 對於讀取文件的異步操作 可以封裝成Promise對象如下

// promise.js
var fs = require('fs');

var readFilePromise = function readFilePromise(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, function (err, data) {
            if (err) {
                reject(err);
            }
            resolve(file + ': ' + data.toString());
        });
    });
}

readFilePromise('file1.txt').then(
    function (data) {
        console.log(data);
    }
).catch(function (err) {
    // err
});

使用babel-node執行promise.js文件 打印結果如下

file1.txt: file1

bluebird

除了上述自己封裝Promise對象的方法外 我們還可以藉助第三方庫bluebird

除了bluebird 當然還有其他的用於實現Promise的第三方庫 例如 q 關於q、bluebird的更多對比和介紹可以參考What’s the difference between Q, Bluebird, and Async?

對於上述使用Promise對象實現的例子 使用bluebird實現如下

// bluebird.js
var Promise = require('bluebird');
var readFile = Promise.promisify(require('fs').readFile);

readFile('file1.txt', 'utf8').then(
    function (data) {
        console.log('file1.txt: ' + data);
    }
).catch(function (err) {
    // err
});

在使用bluebird之前 需要安裝依賴: npm i –save bluebird

使用babel-node執行bluebird.js文件 打印結果如下

file1.txt: file1

Generator

Promise可以解決Callback Hell問題 但是鏈式的代碼看起來仍然不夠直觀

因此 ES6中還引入了Generator函數 又稱為生成器函數

Generator函數與普通函數的區別就是在function後面多加了一個星號 即: function *

例如 下面使用Generator函數實現的讀取文件的例子

// generator.js
var fs = require('fs');

function* generator(cb) {
    yield fs.readFile('file1.txt', cb);

    yield fs.readFile('file2.txt', cb);

    yield fs.readFile('file3.txt', cb);
};

var g = generator(function (err, data) {
    console.log('file1.txt: ' + data);
});

g.next();

Generator函數有以下兩個特點

調用Generator函數返回的是Generator對象 但代碼會在yield處暫停執行

執行Generator對象的next()方法 代碼繼續執行至下一個yield處暫停

由於上述代碼只執行了一次next()方法 於是會在讀取file1.txt后暫停

因此 使用babel-node執行generator.js文件 打印結果如下

file1.txt: file1

co

Generator函數雖然目的是好的 但是理解和使用並不方便 於是就有了神器co

它用於自動執行Generator函數 讓開發者不必手動創建Generator對象並調用next()方法

使用co之後 異步的代碼看起來是這樣的

// co.js
var Promise = require('bluebird');
var readFile = Promise.promisify(require('fs').readFile);
var co = require('co');

co(function* () {
    var data = yield readFile('file1.txt', 'utf8');
    console.log('file1.txt: ' + data);

    data = yield readFile('file2.txt', 'utf8');
    console.log('file2.txt: ' + data);

    data = yield readFile('file3.txt', 'utf8');
    console.log('file3.txt: ' + data);
}).catch(function (err) {
    // err
});

在使用co之前 需要安裝依賴: npm i –save co

使用babel-node執行co.js文件 打印結果如下

file1.txt: file1
file2.txt: file2
file3.txt: file3

從上述的例子我們看出 co有以下兩個特點

co()返回的是Promise

co封裝的Generator函數中的yield後面必須是Promise!

除了上述co的基本用法之外 我們還可以使用co將Generator函數封裝成普通函數

// co-wrap.js
var Promise = require('bluebird');
var readFile = Promise.promisify(require('fs').readFile);
var co = require('co');

var fn = co.wrap(function* () {
    var data = yield readFile('file1.txt', 'utf8');
    console.log('file1.txt: ' + data);

    data = yield readFile('file2.txt', 'utf8');
    console.log('file2.txt: ' + data);

    data = yield readFile('file3.txt', 'utf8');
    console.log('file3.txt: ' + data);
});

fn();

使用babel-node執行co-wrap.js文件 打印結果如下

file1.txt: file1
file2.txt: file2
file3.txt: file3

看到這裏 筆者也不禁感慨 co配合Generator真的是異步開發的”終極”啊

而且 co這個庫的源碼僅僅只有200多行 其中還包含了很多註釋和空行

async/await

剛感慨完異步的”終極”: co配合Generator 為什麼故事還沒結束呢?

原因很簡單 JavaScript語言原生也加入了一套類似co配合Generator的實現: async/await

這裏的async是JavaScript最新版本中實現異步的關鍵字 與前面介紹的第三方庫async不要混淆

總歸還是原裝的好 因此co官方也推薦大家使用async/await

這個事情讓我不禁想起的iPhone越獄插件 很多插件的功能都集成在了最新版本的iOS中 因此後來很多人對越獄興緻不高了

廢話不多話 直接看看原裝的異步”終極神器”吧

在使用async/await之前 首先 需要配置babel並添加依賴

npm install --save-dev babel-preset-stage-3

然後 在根目錄添加.babelrc文件 內容如下

{
    "presets": [
        "stage-3"
    ]
}

因為async/await是在最新的JavaScript版本stage-3中才引入的 ES6並不支持

接着 就可以使用JavaScript語言原生的async/await了

// async/await.js
var Promise = require('bluebird');
var readFile = Promise.promisify(require('fs').readFile);

var fn = async function () {
    var data = await readFile('file1.txt', 'utf8');
    console.log('file1.txt: ' + data);

    data = await readFile('file2.txt', 'utf8');
    console.log('file2.txt: ' + data);

    data = await readFile('file3.txt', 'utf8');
    console.log('file3.txt: ' + data);
};

fn();

從上述的例子我們看出 async/await有以下兩個特點

async/await和普通函數用法幾乎無異

唯一的區別就是在function前加上async 在函數內的Promise前加上await

小結

最後 我們再來回顧一下JavScript異步編程的完整演進過程

callback (async) -> Promsie (bluebird) -> Generator (co) -> async/await (stage-3)

聽co大神的話 其他方案都不要用了 大家儘早投入async/await的懷抱吧

參考

  • Async詳解之一:流程控制

  • 一張圖學會使用Async組件進行異步流程控制

  • Promise 對象

  • Javascript異步編程的4種方法

  • Node.js最新技術棧之Promise篇

  • Generator 函數的含義與用法

  • yield 和 yield*

  • co 函數庫的含義和用法

  • Babel 入門教程

更多文章, 請支持我的個人博客