您的位置:首页 > Web前端 > JavaScript

ES6---JS异步编程的几种解决方法及其优缺点

2016-07-09 18:29 871 查看

前言

因项目需要从LiveScript转为ES6, 所以最近看了阮一峰的ES6教程,主要感兴趣的是ES6对JS的异步编程新的解决方案,ES6增加了promise和Generator等解决方法。现在我们来大致理清一下到ES6为止的JS异步解决的思路以及他们各自的优缺点。

起因

我们都知道JS是单线程的,这也正是异步编程对于JS很重要的原因,因为它无法忍受耗时太长的操作。

正因如此有一系列的实现异步的方法

方法一 setTimeout

常用于:定时器,动画效果

用法: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对象,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 函数

方法五 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模块

Generator函数之–用Thunk函数实现自动化流程管理的例子

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函数。

Generator函数之–用CO模块来实现自动化流程管理的例子

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 等知名开源项目的创建和贡献者。有兴趣的可以自行了解一下该大神的成就。

膜拜至极。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: