因项目需要从LiveScript转为ES6, 所以最近看了阮一峰的ES6教程,主要感兴趣的是ES6对JS的异步编程新的解决方案,ES6增加了promise和Generator等解决方法。现在我们来大致理清一下到ES6为止的JS异步解决的思路以及他们各自的优缺点。
我们都知道JS是单线程的,这也正是异步编程对于JS很重要的原因,因为它无法忍受耗时太长的操作。
正因如此有一系列的实现异步的方法
常用于:定时器,动画效果
用法:setTimeout(func|code, delay)
缺点:
setTimeout 的主要问题在于,它并非那么精确。譬如通过setTimeout()设定一个任务在10毫秒后执行,但是在9毫秒之后,有一个任务占用了5毫秒的CPU时间片,再次轮到定时器执行时,时间就已经过期4毫秒
—-《深入浅出Nodejs》
为什么呢? 我们可以了解一下setTimeout执行的事件循环图
Javascript执行引擎的主线程运行的时候,产生堆(heap)和栈(stack)。程序中代码依次进入栈中等待执行,当调用setTimeout()方法时,即图中右侧WebAPIs方法时,浏览器内核相应模块开始延时方法的处理,当延时方法到达触发条件时,方法被添加到用于回调的任务队列(注意是任务队列),只要执行引擎栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些满足触发条件的回调函数。
任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
用法:f1.on(‘done’, f2);
优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”,有利于实现模块化。
缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。
注意:以下所有的例子中 a.md文件 存放的字符串为”b”, b.md文件存放的字符串为”this is b”;
什么是回调函数?
JavaScript语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
这里有一个误区
回调函数是实现JS异步的一种方法,并不是说回调函数就是异步的。
只是我们用的大多数回调函数都是用于异步
异步的定义:
在JavaScript中,回调函数具体的定义为:函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A。我们就说函数A叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。
因此callback 不一定用于异步,一般同步(阻塞)的场景下也经常用到回调,比如要求执行某些操作后执行回调函数。
简单的回调函数例子
var fs = require('fs');
fs.readFile("a.md", function(err, data) {
console.log(data.toString());
});
运行结果:
b
回调函数的缺点
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。
假定读取A文件之后,从A文件中获取B文件名,再读取B文件,代码如下。
var fs = require('fs');
fs.readFile("a.md", function (err, data) {
console.log(data.toString());
fs.readFile(data.toString() + ".md", function(err, data) {
console.log(data.toString());
});
});
运行结果:
b
this is b
想想,如果再嵌套多几层,代码会变得多么难以理解
这个被称之为“回调函数噩梦”(callback hell)!!!
为了解决上面的问题,我们开始介绍Promise对象,Promise原本只是社区提出的一个构想,一些外部函数库率先实现了这个功能。ECMAScript 6将其写入语言标准,因此目前JavaScript语言原生支持Promise对象
假设要依次读取多个文件,如果用普通的回调函数,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。
var readFile = require('fs-readfile-promise');
readFile("a.md")
.then(function(data) {
console.log(data.toString());
return readFile(data.toString() + ".md");
})
.then(function(data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
运行结果:
b
this is b
Promise的优缺点:
优点:Promise 的写法是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了。then将原来异步函数的嵌套关系转变为链式步骤
缺点:Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
所以,ES6在把Promise纳入标准的同时,也提供了另一种实现 => Generator 函数
特点: 带星号function,yield语句 ,next() 获取下一个yield表达式中yield后的值,拥有遍历器接口,与for..of可搭配使用
Generator实现斐波那契的例子:
function * fibonacci() {
let [prev, curr] = [1, 0];
for (;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
运行结果
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
Generator用于异步操作
下面代码中,Generator函数封装了一个异步操作,该操作先读取一个远程接口,然后从JSON格式的数据解析信息。这段代码非常像同步操作,除了加上了yield命令
var fetch = require('node-fetch');
function * gen() {
var url = 'http://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data) {
return data.json();
}).then(function (data) {
g.next(data);
});
执行结果:
How people build software.
执行过程:
首先执行Generator函数,获取遍历器对象,然后使用next 方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个Promise对象,因此要用then方法调用下一个next 方法。
缺点:
可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。即如何实现自动化的流程管理。
自此我们引出新的补充方案: Thunk函数和Co模块
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
var gen = function *() {
var f1 = yield readFile('a.md');
console.log(f1.toString());
var f2 = yield readFile(f1.toString() + ".md");
console.log(f2.toString());
};
run(gen);
运行结果:
b
this is b
执行过程:
上面代码的run函数,就是一个Generator函数的自动执行器。内部的next函数就是Thunk的回调函数。next函数先将指针移到Generator函数的下一步(gen.next方法),然后判断Generator函数是否结束(result.done 属性),如果没结束,就将next函数再传入Thunk函数(result.value属性),否则就直接退出。
Thunk函数的限制
有了这个执行器,执行Generator函数方便多了。不管有多少个异步操作,直接传入run函数即可。当然,前提是每一个异步操作,都要是Thunk函数,也就是说,跟在yield命令后面的必须是Thunk函数。
var fs = require('fs');
var thunkify = require('thunkify');
var readFile = thunkify(fs.readFile);
var co = require('co');
var gen = function* (){
var r1 = yield readFile('a.md');
console.log(r1.toString());
var r2 = yield readFile(r1.toString() + '.md');
console.log(r2.toString());
};
co(gen).then(function() {
console.log('Generator函数执行完毕');
});
运行结果:
b
this is b
Generator函数执行完毕
执行过程:
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
CO模块的限制:
co模块其实就是将两种自动执行器(Thunk函数和Promise对象),包装成一个模块。使用co的前提条件是,Generator函数的yield命令后面,只能是Thunk函数或Promise对象。
至此,介绍了几种JS用于处理异步的方法,我们也可以看到ES6对异步的处理要比之前的版本更加成熟。整理的几种方法的使用及其优缺点,有的地方可能还不完善,为了便于理解,使用的例子也比较简单。后续如果有新的理解会一一补充。
大神TJ
值得一提的是,这其中涉及到的一个极为优秀的程序员,TJ Holowaychuk,程序员兼艺术家,Koa、Co、Express、jade、mocha、node-canvas、commander.js 等知名开源项目的创建和贡献者。有兴趣的可以自行了解一下该大神的成就。
膜拜至极。