本文的示例代码参考这里的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的怀抱吧
参考
更多文章, 请支持我的个人博客