前言
众所周知Javascript
是“单线程”语言,在实际开发中我们又不得不面临异步逻辑的处理,这时候异步编程就变得十分必要。所谓异步,就是指在执行一件任务,这件任务分A、B两个阶段,执行完A阶段后,需要去做另外一个任务得到结果后才能执行B阶段。异步编程有以下几种常用方式:callback
、Promise
、Generator
、async
。
callback函数
callback函数是指通过函数传参
传递到其他执行代码的,某一块可执行代码的引用,被主函数调用后又回到主函数,如下例:
function add(a, b, callback){
var num = a + b;
callback(num)
}
add(1, 2, function(num){
console.log(num); # 3
# ...
})
复制代码
如果是有个任务队列,里面包含多个任务的话,那就需要层层嵌套了
var readFile = require('fs-readfile-promise'); # 读取文件函数
readFile(fileA, function(data) {
readFile(fileB, function(data) {
# ...
})
})
复制代码
如上如果我存在n个任务,那需要层层嵌套n层,这样代码显得非常冗余庞杂并且耦合度很高,修改其中某一个函数的话,会影响上下函数代码块的逻辑。这种情况被称为“回调地狱”(callback hell)
Promise
Promise是我们常用来解决异步回调问题的方法。允许将回调函数的嵌套,改为链式调用。以上多个任务的话,可以改造成如下例子:
function add(a, b){
return new Promise((resolve, reject) => {
var result = a+b;
resolve(result);
})
}
add(10, 20).then(res => {
return add(res, 20) # res = 30
}).then(res => {
return add(res, 20) # res = 50
}).then(res => {
// ...
}).catch(err => {
// 错误处理
})
复制代码
add函数执行后会返回一个Promise
,它的结果会进入then方法中,第一个参数是Promise
的resolve
结果,第二个参数(可选)是Promise
的reject
结果。我们可以把回调后的逻辑在then
方法中写,这样的链式写法有效的将各个事件的回调处理分割开来,使得代码结构更加清晰。另外我们可以在catch
中处理报错。
如果是我们的异步请求不是按照顺序A->B->C->D这种,而是[A,B,C]->D,先并行执行A、B、C完然后在执行D,我们可以用Promise.all();
# 生成一个Promise对象的数组
const promises = [2, 3, 5].map(function (id) {
return getJSON('/post/' + id + ".json"); # getJSON 是返回被Promise包装的数据请求函数
});
Promise.all(promises).then(function (posts) {
# promises里面装了三个Promise
# posts返回的是一个数组,对应三个Promise的返回数据
# 在这可以执行D任务
}).then(res => {
//...
}).catch(function(reason){
//...
});
复制代码
但是Promise
的代码还是有些多余的代码,比如被Promise
包装的函数有一堆new Promise
、then
、catch
。
Generator函数
Generator函数是ES6提供的一种异步编程解决方案,由每执行一次函数返回的是一个遍历器对象,返回的对象可以依次遍历Generator里面的每个状态,我们需要用遍历器对象的next
方法来执行函数。
先来个例子:
function* foo() {
yield 'stepone';
yield 'steptwo';
return 'stepthree';
}
var _foo = foo();
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
_foo.next(); #{value: 'stepone', done: false}
复制代码
Generator有三个特征:函数命名时function
后面需要加*
;函数内部有yield
;外部执行需要调用next
方法。每个yield会将跟在她后面的值包裹成一个对象的返回,返回的对象中包括返回值和函数运行状态,直到return
,返回done
为true
。
如果每次运行Generator函数我们都需要用next的话,你那就太麻烦了,我们需要一个可以自动执行器。co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。 运用co模块时,yield后面只能是 Thunk函数 或者Promise对象,co函数执行完成之后返回的是Promise。如下:
var co = require('co');
var gen = function* () {
var img1 = yield getImage('/image01');
var img2 = yield getImage('/image02');
...
};
co(gen).then(function (res){
console.log(res);
}).catch(err){
# 错误处理
};
复制代码
co模块的任务的并行处理,等多个任务并行执行完成之后再进行下一步操作:
# 数组的写法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).then(console.log).catch(onerror);
# 对象的写法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).then(console.log).catch(onerror);
复制代码
Generator
函数虽然相比Promise
在写法上更加精简且逻辑清晰,但是需要额外有个运行co
函数去执行,为了解决优化这个问题,async
函数出现了。
async函数
async函数是Generator
函数的语法糖。
var co = require('co');
var gen = function* () {
var img1 = yield getImage('/image01');
var img2 = yield getImage('/image02');
...
};
co(gen).then(function (res){
console.log(res);
}).catch(err){
# 错误处理
};
****
#以上Generator函数可以改为
var gen = async function () {
var img1 = await getImage('/image01');
var img2 = await getImage('/image02');
return [img1, img2];
...
};
gen().then(res => {
console.log(res) # [img1, img2]
});
复制代码
相比Generator
函数,async
函数在写法上的区别就是async
替代了*
,await
替代了yield
,并且async
自带执行器,只需gen()即可执行函数;拥有比较好的适应性,await
后面可以是Promise
也可以是原始类型的值;此外async
函数返回的是Promise
,便于我们更好的处理返回值。
async function gen() {
return '111';
# 等同于 return await '111';
};
gen().then(res => {
console.log(res) # 111
});
复制代码
如果是直接return值,这个值会自动成为then方法回调函数中的值。
async function gen() {
var a = await getA();
var b = await getB();
return a + b;
};
gen().then(res => {
console.log(res)
});
复制代码
async
函数返回的Promise
,必须等到函数体内所有await
后面的Promise
对象都执行完毕后,或者return
或者抛错
之后才能改变状态;也就是只有async
里面的异步操作全部操作完,才能回到主任务来,并且在then
方法里面继续执行主任务。
# 错误处理1
async function gen() {
await new Promise((resolve, reject) => {
throw new Error('出错了');
})
};
gen().then(res => {
console.log(res)
}).catch(err => {
console.log(err) # 出错了
});
# 错误处理2:如下处理,一个await任务的错误不会影响到后面await任务的执行
async function gen() {
try{
await new Promise((resolve, reject) => {
throw new Error('出错了');
})
}catch(e){
console.log(e); # 出错了
}
return Promise.resolve(1);
};
gen().then(res => {
console.log(res) # 1
});
复制代码
错误处理如上。
async function gen() {
# 写法一
let result = await Promise.all([getName(), getAddress()]);
return result;
# 写法二
let namePromise = getName();
let addressPromise = getAddress();
let name = await namePromise;
let address = await addressPromise;
return [name, address];
};
gen().then(res => {
console.log(res); # 一个数组,分别是getName和getAddress返回值
})
复制代码
多个异步任务互相没有依赖关系,需要并发时,可按照如上两种方法书写。
async与Promise、Generator函数之间的对比
function chainAnimationsPromise(elem, animations) {
# 变量ret用来保存上一个动画的返回值
let ret = null;
# 新建一个空的Promise
let p = Promise.resolve();
# 使用then方法,添加所有动画
for(let anim of animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
});
}
# 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
# 错误处理
}).then(function() {
return ret;
});
}
复制代码
Promise
虽然很好的解决了地狱回调的问题,但是代码中有很多与语义无关的then
、catch
等;
function chainAnimationsGenerator(elem, animations) {
return co(function*() {
let ret = null;
try {
for(let anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
# 错误处理
}
return ret;
});
}
复制代码
Generator
函数需要自动执行器来执行函数,且yield
后面只能是Promise
对象或者Thunk
函数。
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
for(let anim of animations) {
ret = await anim(elem);
}
} catch(e) {
# 错误处理
}
return ret;
}
复制代码
async
函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。与Generator
相比不需要程序员再提供一个执行器,async
本身自动执行,使用起来方便简洁。