JavaScript学习 之 异步

本文的示例代码参考这里的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
  • 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 入门教程

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

你可能感兴趣的:(JavaScript学习 之 异步)