本文的示例代碼參考這里的async
目錄
引言
眾所周知 JavaScript語(yǔ)言的執(zhí)行環(huán)境是"單線程" 這一點(diǎn)大大降低了并發(fā)編程的門檻
但是 如何在降低門檻的同時(shí)保證性能呢? 答應(yīng)就是 異步
因此 本文就來(lái)詳細(xì)討論JavaScript異步編程的方法
callback
callback又稱為回調(diào) 是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());
});
});
});
其中 測(cè)試文件的內(nèi)容分別是
// file1.txt
file1
// file2.txt
file2
// file3.txt
file3
使用babel-node執(zhí)行callback.js文件 打印結(jié)果如下
file1.txt: file1
file2.txt: file2
file3.txt: file3
關(guān)于babel-node的更多介紹請(qǐng)參考JavaScript學(xué)習(xí) 之 版本
async
上述只是順序執(zhí)行異步回調(diào)的簡(jiǎn)單示例 為了實(shí)現(xiàn)更復(fù)雜的異步控制 我們可以借助第三方庫(kù)async
async最基本的有以下三個(gè)控制流程
series
parallel
waterfall
- series 順序執(zhí)行 但沒有數(shù)據(jù)交互
例如上述讀取文件的例子 使用async這樣實(shí)現(xiàn)
// 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執(zhí)行async.js文件 打印結(jié)果如下
[ 'file1.txt: file1', 'file2.txt: file2', 'file3.txt: file3' ]
- parallel 并行執(zhí)行
如果想實(shí)現(xiàn)同時(shí)讀取多個(gè)文件的功能 使用async這樣實(shí)現(xiàn)
// 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執(zhí)行async.js文件 打印結(jié)果如下
[ 'file1.txt: file1', 'file2.txt: file2', 'file3.txt: file3' ]
由于這里的文件內(nèi)容都比較小 所以結(jié)果看起來(lái)還是?順序執(zhí)行 但其實(shí)是并行執(zhí)行的
- waterfall 順序執(zhí)行 且有數(shù)據(jù)交互
// 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執(zhí)行async.js文件 打印結(jié)果如下
[ 'file1.txt: file1', 'file2.txt: file2', 'file3.txt: file3' ]
當(dāng)然 async的功能還遠(yuǎn)不止這些 例如 auto等更強(qiáng)大的流程控制等 讀者想深入了解的話可以參考這里
Promise
對(duì)于簡(jiǎn)單項(xiàng)目來(lái)說 ?使用上述async的方式完全可以滿足需求
但是 基于回調(diào)的方法在較復(fù)雜的項(xiàng)目中 仍然不夠簡(jiǎn)潔
因此? 基于Promise的異步方法應(yīng)運(yùn)而生
在開始使用Promise之前 我們需要搞清楚 什么是Promise?
Promise是一種規(guī)范 目的是為異步編程提供統(tǒng)一接口
那么使用Promise時(shí) 接口是被統(tǒng)一成什么樣子了呢?
return step1().then(step2).then(step3).catch(function(err){
// err
});
從上面的例子 我們可以看出Promise有以下三個(gè)特點(diǎn)
返回Promise
鏈?zhǔn)讲僮?
then/catch流程控制
當(dāng)然 除了上述順序執(zhí)行的控制流程 Promise也支持并行執(zhí)行的控制流程
var promise123 = Promise.all([promise1, promise2, promise3]);
Promise對(duì)象
了解了Promise的原理和使用之后 我們就可以開始調(diào)用封裝成Promise的代碼了
但是 如果遇到需要自己封裝Promise的情況 怎么辦呢?
可以 使用?ES6提供的Promise對(duì)象
關(guān)于ES6以及JavaScript版本的詳細(xì)介紹 可以參考JavaScript學(xué)習(xí) 之 版本
例如 對(duì)于讀取文件的異步操作 可以封裝成Promise對(duì)象如下
// 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執(zhí)行promise.js文件 打印結(jié)果如下
file1.txt: file1
bluebird
除了上述自己封裝Promise對(duì)象的方法外 我們還可以借助第三方庫(kù)bluebird
除了bluebird 當(dāng)然還有其他的用于實(shí)現(xiàn)Promise的第三方庫(kù) 例如 q 關(guān)于q、bluebird的更多對(duì)比和介紹可以參考What's the difference between Q, Bluebird, and Async?
對(duì)于上述使用Promise對(duì)象實(shí)現(xiàn)的例子 使用bluebird實(shí)現(xiàn)如下
// 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執(zhí)行bluebird.js文件 打印結(jié)果如下
file1.txt: file1
Generator
Promise可以解決Callback Hell問題 但是鏈?zhǔn)降拇a看起來(lái)仍然不夠直觀
因此 ES6中還引入了Generator函數(shù) 又稱為生成器函數(shù)
Generator函數(shù)與普通函數(shù)的區(qū)別就是在function后面多加了一個(gè)星號(hào) 即: function *
例如 下面使用Generator函數(shù)實(shí)現(xiàn)的讀取文件的例子
// 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函數(shù)有以下兩個(gè)特點(diǎn)
調(diào)用Generator函數(shù)返回的是Generator對(duì)象 但代碼會(huì)在yield處暫停執(zhí)行
執(zhí)行Generator對(duì)象的next()方法 代碼繼續(xù)執(zhí)行至下一個(gè)yield處暫停
由于上述?代碼只執(zhí)行了一次next()方法 于是會(huì)在讀取file1.txt后暫停
因此 使用babel-node執(zhí)行g(shù)enerator.js文件 打印結(jié)果如下
file1.txt: file1
co
Generator函數(shù)雖然目的是好的 但是理解和使用并不方便 于是就有了神器co
它用于自動(dòng)執(zhí)行Generator函數(shù) 讓開發(fā)者不必手動(dòng)創(chuàng)建Generator對(duì)象并調(diào)用next()方法
使用co之后 異步的代碼看起來(lái)是這樣的
// 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執(zhí)行co.js文件 打印結(jié)果如下
file1.txt: file1
file2.txt: file2
file3.txt: file3
從上述的例子我們看出 co有以下兩個(gè)特點(diǎn)
co()返回的是Promise
co封裝的Generator函數(shù)中的yield后面必須是Promise!
除了上述co的基本用法之外 我們還可以使用co將Generator函數(shù)?封裝成普通函數(shù)
// 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執(zhí)行co-wrap.js文件 打印結(jié)果如下
file1.txt: file1
file2.txt: file2
file3.txt: file3
看到這里 筆者也不禁感慨 co配合Generator真的是異步開發(fā)的"終極"啊
而且 co這個(gè)庫(kù)的源碼僅僅只有200多行 其中還包含了很多注釋和空行
async/await
剛感慨完異步的"終極": co配合Generator 為什么故事還沒結(jié)束呢?
原因很簡(jiǎn)單 JavaScript語(yǔ)言原生也加入了一套類似co配合Generator的實(shí)現(xiàn): async/await
這里的async是JavaScript最新版本中實(shí)現(xiàn)異步的關(guān)鍵字 與前面介紹的第三方庫(kù)async不要混淆
總歸還是原裝的好 因此co官方也推薦大家使用async/await
這個(gè)事情讓我不禁想起的iPhone越獄插件 很多插件的功能都集成在了最新版本的iOS中 因此后來(lái)很多人對(duì)越獄興致不高了
廢話不多話 直接看看原裝的異步"終極神器"吧
在使用async/await之前 首先 需要配置babel并添加依賴
npm install --save-dev babel-preset-stage-3
然后 在根目錄添加.babelrc文件 內(nèi)容如下
{
"presets": [
"stage-3"
]
}
因?yàn)閍sync/await是在最新的JavaScript版本stage-3中才引入的 ES6并不支持
接著 就可以使用JavaScript語(yǔ)言原生的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有以下兩個(gè)特點(diǎn)
async/await和普通函數(shù)用法幾乎無(wú)異
唯一的區(qū)別就是在function前加上async 在函數(shù)內(nèi)的Promise前加上await
小結(jié)
最后 我們?cè)賮?lái)回顧一下JavScript異步編程的完整演進(jìn)過程
callback (async) -> Promsie (bluebird) -> Generator (co) -> async/await (stage-3)
聽co大神的話 其他方案都不要用了 大家盡早投入async/await的懷抱吧
參考
更多文章, 請(qǐng)支持我的個(gè)人博客