异步编程的前世今生

为了解决多重嵌套回调会使代码变得难以维护的问题,javascript一直在不但的寻找改善和解决这个问题的方案,一直在不但的演进着…

异步编程的终极目标:无招(异步)胜有招(异步)

ES6之前的解决方案

嵌套回调(洋葱语法)

最简单的回调处理就是这种多重嵌套,如果嵌套太多,还要做处理异常的话,代码将变得异常复杂。

$.post('data1.json',function(data){
$.post('data2.json', function(data){
$.post('data3.json', function(data){
$.post('data4.json', function(data){

})
})
})
})

具名函数

一种减少嵌套的方法就是使用具名函数

function getData1(){
$.post('data1.json',function(){
getData2();
})
}

function getData2(){
$.post('data2.json',function(){
getData3();
})
}

function getData3(){
$.post('data3.json',function(){
getData4()
})
}

function getData4(){
$.post('data1=4.json',function(){
//...
})
}

getData1()

使用具名函数虽然减少了嵌套层级,但是直观上无法反应各个函数之间的层级关系,以致逻辑不清

Pub发布/Sub订阅

类似于具名函数,使用事件的发布订阅模式也以达到同样的效果,但依然存在着相同的问题

var event = new EventEmitter();

//注册事件监听
event.on('getData2',getData2)
event.on('getData3',getData3)
event.on('getData4',getData4)

function getData1(){
$.post('data1.json',function(){
//触发事件
event.emit('getData2');
})
}

function getData2(){
$.post('data2.json',function(){
event.emit('getData3');
})
}

function getData3(){
$.post('data3.json',function(){
event.emit('getData4');
})
}

function getData4(){
$.post('data1=4.json',function(){
//...
})
}

Promise

使用 promise 可让回调写起来更优雅,将回调嵌套,以链式调用的方式来写,顺序层级也很直观

$.post('data1.json')
.then($.post('data2.json'))
.then($.post('data3.json'))
.then($.post('data4.json'))
.catch(function(err){
//...
})

使用 promise 仍然也少不了回调,只是改善了回调写法

ES6的解决方案

Generator

Generator 生成器允许你通过写一个可以保存自己状态的的简单函数来定义一个迭代算法。
Generator 是一种可以停止并在之后重新进入的函数。生成器的环境(绑定的变量)会在每次执行后被保存,下次进入时可继续使用。

Generator 字面上是“生成器”的意思,在 ES6 里是迭代器生成器,用于生成一个迭代器对象

function* gen(){
try{
yield asyncFn1;
yield asyncFn2;
yield asyncFn3;
yield asyncFn4;
}catch(err){
//...
}
}

使用Generator可以将异步封装为同步,但无法根据异步操作后的结果自动调用 next 方法执行下一个 yield 后面的异步操作。

Generator + Promise/Thunk

结合Promise或thunk函数可以做到自动执行Generator函数,在异步操作完成之后自动调用next方法,从而实现异步流程的自动管理。

在解释thunk函数之前,先弄清楚几个相近的概念:函数柯里化、bind函数、thunk函数

函数柯里化
是指把接受多个参数的函数转变成接受单一参数的函数,并且返回一个新的函数,这个新函数能够接受原函数剩余的参数。

柯里化有3个常见作用:1. 参数复用;2. 提前返回;3. 延迟计算/运行

var curry = function(fn){
var _args = [];
return function cb(){
if(arguments.length == 0){
return fn.apply(this, _args);
}
Array.prototype.push.apply(_args, arguments);
return cb;
}
}

var add = function(){
return [].slice.call(arguments, 0).reduce(function(a, b){
return a + b;
}, 0)
}

var curryAdd = curry(add);

curryAdd(1)(2)(3)() //6

bind函数
用于绑定函数执行的上下文,是利用柯里化的思想,实现固定函数内部的上下文 this ,返回一个接受剩余参数的函数。

Function.prototype.bind = Function.prototype.bind || function(context){
var _this = this, slice = Array.prototype.slice,
args = slice.call(arguments, 1);
return function(){
return _this.apply( context, args.concat(slice.call(arguments)) )
}
}
var cuslog = {
log:function(args){
console.log('cus-'+args)
},
error:function(args){
this.log('error: ' + args)
},
info: function(args){
this.log('info: ' + args)
}
}
//不改变上下文
cuslog.error('错误消息') //cus-error: 错误消息
//改变上下文
cuslog.error.bind(console)('错误消息') // error: 错误消息
cuslog.info.bind(console)('提示消息') // info: 提示消息

thunk函数
将多参函数转换为单参函数,且只接受函数做为参数。
任何函数,只要参数有回调函数,都可以转换为thunk函数。

var Thunk = function(fn){
return function(){
var args = Array.prototype.slice.call(arguments);
return function(callback){
args.push(callback);
return fn.apply(this, args);
}
}
}

function add(num1, num2, num3, callback){
callback && callback(num1 + num2 + num3)
}

//将add转为thunk函数
var thunkAdd = Thunk(add);
//执行thunk函数,返回一个接收参数为函数的新函数
thunkAdd(1,2,3)(function(res){
console.log(res)
})
// 6

bindthunk 都是柯里化思想的一种实现,第一次调用,都是返回的都是一个函数,不同的是bind返回的函数对参数没有什么要求,thunk返回的函数要求参数必须是一个函数。

Generator + Thunk

Thunk函数的特点:返回的是一个接收函数参数的函数

依次获取文件内容

var fs = require('fs');

var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};

//将readFile方法包装成thunk函数
var readFile = Thunk(fs.readFile);

function* gen(){
var f1 = yield readFile('1.txt','utf8');
console.log(f1)
var f2 = yield readFile('2.txt','utf8');
console.log(f2)
var f3 = yield readFile('3.txt','utf8');
console.log(f3)
}
//yield 命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。
//这种方法就是 Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数。

//利用thunk函数返回函数并接收参数为函数的特点,递归调用执行next方法,封装执行器
function run(fn){
var gen = fn();
function next(err, data){
//将指针移到下一个yield
var res = gen.next(data);
if(res.done) return;
res.value(next)
}
next();
}

run(gen)
// 1.txt
// 2.txt
// 3.txt

Promise的特点

Promise.prototype.then(okFn, failFn) promise的then方法有两个回调,第一个是成功的回调,第二个是失败的回调,基于Promise的特点,结合Generator可以很方便控制执行流程

Generator + Promise

依次处理异步请求

function* gen(){
try{
var r1 = $.post('data1.json');
console.log(r1);
var r2 = $.post('data2.json');
console.log(r2);
var r3 = $.post('data3.json');
console.log(r3);
}catch(err){
console.log(err)
}
}

//根据Promise的特点,封装执行器
funciton run(fn){
var gen = fn;
//第一次移动指针
next();
function next(res){
var res = gen.next(res);
if(res.done) return res.value;
res.value.then(function(data){
//异步成功后,移动指针
next(data)
}, function(err){
//异步失败后,抛出错误
next(gen.throw(err))
})
}
}

run(gen)

从以上可以看出,无论是 Generator+Thunk 还是 Generator+Promise 都需要封装一个执行品来做Generator执行权的转接

co函数

co的作用是自动为Generator函数添加执行器

co函数库 其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个库。
使用 co 的前提条件是,Generator 函数的 yield 命令后面,只能是 Thunk 函数或 Promise 对象。

var co = require('co')

//依次执行
function* gen(){
var r1 = $.post('data1.json');
var r2 = $.post('data2.json');
var r3 = $.post('data3.json');
return [r1, r2, r3]
}

co(gen)
.then(res=>{ console.log(res) })
.catch(err=>{ console.log(err) })

//并发执行
con(function* (){
yield [$.post('data1.json'), $.post('data2.json'), $.post('data3.json')]
}).catch(err=>{ console.log(err) })

ES7的终极解决方案

async + await

异步编程的最高境界,就是根本不用关心它是不是异步。
Async就像是隧道尽头的光亮,被人们奉以为是异步编程的终极解决方案。

简单来说,Async解决方案就是Generator与co的合体版

//定义一个async函数
async function getData(){
try{
var r1 = await yield $.post('data1.json')
var r2 = await yield $.post('data2.json')
var r3 = await yield $.post('data3.json')
return [r1, r2, r3]
}catch(error){
return Promise.reject(error)
}
}

//执行async函数
getData()
.then(res=>{ console.log(res) })
.catch(function(err){
console.log(err)
});

sleep实现

function sleep(time){
return new Promise((resolve, reject)=>{
setTimeout(()=>resolve('sleep: '+time), time);
})
}

//每三秒执行一次异步请求
var task = async function(){
var d1 = await $.post('data1.json')
var msg1 = await sleep(3000)
console.log(msg1)
var d2 = await $.post('data2.json')
var msg2 = await sleep(3000)
console.log(msg2)
var d3 = await $.post('data3.json')
return [d1, d2, d3]
}
task()

并发执行

(async=>{
var tasks = [$.post('data1.json'), $.post('data2.json'), $.post('data3.json')]
var res = await Promise.all(tasks)
return res;
})()

async返回的是一个Promise对象,具有Promise相关特性。
await只能用于async函数中,且只能用于普通函数中。

对比其它方法的优点:

  • 内置执行器
  • 更好的语义 使用 asyncawait 替代 *yield ,使得语义更清楚,async 表示后面有异步操作, await 表示需要等待后面语句返回结果
  • 更广的适用性 co函数约定 yield后面必须跟thunk函数与promise函数,而async函数中,await后面可以跟Promise对象与其它任何类型

阅读参数