JavaScript學(xué)習(xí) 之 異步

本文的示例代碼參考這里的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è)人博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市众眨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纪铺,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件股冗,死亡現(xiàn)場(chǎng)離奇詭異霹陡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門烹棉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)攒霹,“玉大人,你說我怎么就攤上這事浆洗〈呤” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵伏社,是天一觀的道長(zhǎng)抠刺。 經(jīng)常有香客問我,道長(zhǎng)摘昌,這世上最難降的妖魔是什么速妖? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮聪黎,結(jié)果婚禮上罕容,老公的妹妹穿的比我還像新娘。我一直安慰自己稿饰,他們只是感情好锦秒,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著喉镰,像睡著了一般旅择。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上侣姆,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天生真,我揣著相機(jī)與錄音,去河邊找鬼捺宗。 笑死汇歹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的偿凭。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼派歌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼弯囊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起胶果,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤匾嘱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后早抠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霎烙,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了悬垃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片游昼。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖尝蠕,靈堂內(nèi)的尸體忽然破棺而出烘豌,到底是詐尸還是另有隱情,我是刑警寧澤看彼,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布廊佩,位于F島的核電站,受9級(jí)特大地震影響靖榕,放射性物質(zhì)發(fā)生泄漏标锄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一茁计、第九天 我趴在偏房一處隱蔽的房頂上張望料皇。 院中可真熱鬧,春花似錦簸淀、人聲如沸瓶蝴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)舷手。三九已至,卻和暖如春劲绪,著一層夾襖步出監(jiān)牢的瞬間男窟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工贾富, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留歉眷,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓颤枪,卻偏偏與公主長(zhǎng)得像汗捡,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子畏纲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • 異步編程對(duì)JavaScript語(yǔ)言太重要扇住。Javascript語(yǔ)言的執(zhí)行環(huán)境是“單線程”的,如果沒有異步編程盗胀,根本...
    呼呼哥閱讀 7,298評(píng)論 5 22
  • 本文首發(fā)在個(gè)人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云閱讀 1,679評(píng)論 0 3
  • 弄懂js異步 講異步之前艘蹋,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,697評(píng)論 0 5
  • 單線程與異步 Javascript是單線程運(yùn)行票灰、支持異步機(jī)制的語(yǔ)言女阀。進(jìn)入正題之前宅荤,我們有必要先理解這種運(yùn)行方式。 ...
    貝聊科技閱讀 621評(píng)論 0 0
  • 異步編程在JavaScript中非常重要浸策。過多的異步編程也帶了回調(diào)嵌套的問題冯键,本文會(huì)提供一些解決“回調(diào)地獄”的方法...
    AlienZHOU閱讀 27,448評(píng)論 2 52