JavaScript学习 之 异步

本文的示例代码参考这里的async

目录

引言

众所周知 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
  • 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的怀抱吧

参考

更多文章, 请支持我的个人博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容