【ES6复习】异步函数

项目当中我们会遇到各种个样的异步操作,我们使用最多的方式就回调函数。那么在ES6中,给我们带来了什么样的解决方案呢?那当然是Promise和async/await了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 越来越深的回掉地狱
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function (err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

jQuery中使用Deferred

Deferred延迟对象是在 jQuery 1.5 中引入的,该对象提供了一系列的方法,可以将多个回调函数注册进一个回调队列里、调用回调队列,以及将同步或异步函数执行结果的成功还是失败传递给对应的处理函数。Deferred让我们的回调方法和异步函数变得更加可读了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var p1 = $.post('/domainLogin'),
p2 = $.post('/domainLogin'),
p3 = $.post('/domainLogin');

// 串行
p1.done(p2).done(p3).done(function(){})
.fail(function(){});

// 并行
$.when(p1,p2,p3).done(function(){}).fail(function(){});

// Deferred
var def = $.Deferred();
def.done(...).fail(...);
def.resolve();
//def.reject();

Promise

Promise 表示一个异步操作的最终结果,与之进行交互的方式主要是 then 方法,该方法可以注册两个回调函数,一个函数用于接收该 promise 的成功结果,第二个函数用于处理该 promise 失败的操作。

Promise的状态

  • pending:初始状态,也就是Promise刚被创建时的状态。
  • fulfilled/resolved:成功的操作。
  • rejected:失败的操作。

Promise的对象只能由pending变成resolved或者由pending变成rejected,且不可逆转。

1
2
3
4
5
6
7
8
9
10
11
12
let p = new Promise((resolve, reject) => {
resolve('resolve p')
// reject('reject p');
});

console.log(p);

p.then((res) => {
console.log('then done ', res)
}, (res) => {
console.log('then fail ', res)
});

Promise流程图

Promise的方法

Promise.prototype.then()

then 方法是处理Promise状态变换的回掉函数,第一个参数是resolved状态时的回调函数,第二个参数时rejected状态时的回调函数,其中第二个参数可以忽略。

不管是resolved还是rejected,都会返回一个新的Promise实例,也就是说then后面可以继续链式的调用then

1
2
3
4
5
6
7
8
Promise
.resolve()
.then(()=>{
console.log('resolve');
// 这里 reject 会触发第二个then的rejected函数
return Promise.reject();
}, console.log.bind(null, 'reject'))
.then(...);

Promise.prototype.catch()

catch方法等于.then(null, rejected)

rejected的状态都会进入到catch中,包含代码异常。

如果整个Promise中有异常没有被catch,就会抛出一个错误unhandled rejection。

1
2
3
Promise.reject();
Promise.reject().catch(()=>{conso.log('catch')});
// Uncaught (in promise)

Promise.prototype.finally()

和他名字一样,无论Promise的状态是什么,都会执行的回调函数。⚠️注意的是它始终返回的是原来Promise的状态。

finally等同于:

1
2
3
4
5
6
7
8
9
10
promise
.then(
result => {
return result;
},
error => {
// 注意是抛出异常,也就是reject状态,而不是默认的resolve
throw error;
}
);

Promise.all() / Promise.race()

他们都接受一个数组作为参数,这个数组当中包含多个Promise实例。该函数会把这些Promise包装成一个新的Promise实例。

不同的是:

Promise.all:需要等待所有Promise状态都变成resolved,该实例的状态才会是resolved,且他的结果是一个数组;如果其中有一个Promise的状态变成rejected,该实例的状态立即会变成rejected,且reject原因就是该Promise失败的原因。

Promise.race:只要有一个实例状态变化之后,整个Promise实例都变成该状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Promise
.all([1,Promise.resolve(2), Promise.resolve(3)])
.then(console.log)
.catch(console.error);

Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
// 设置3000秒的结果呢?
}),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(2);
}, 2000);
})
]).then(console.log)
.catch(console.error);

Promise.resolve()

接受一个参数,可以把它装成Promise对象

1、当参数为Promise实例:

直接返回该实例。

2、当参数为一个thenable对象(包含then方法)

1
2
3
4
5
let thenable = {
then: (resolve, reject)=>{
resolve();
}
}

Promise.resolve会把它包装成一个Promise对象,并执行它的then方法,等同于:

1
2
3
Promise.resolve(thenable);
// 等同于
new Promise(thenable);

3、其它类型(基本类型/对象/函数/…)

直接返回一个状态为resolved的Promise实例,并把该参数作为值传递给then

1
2
3
4
5
6
7
8
9
new Promise((resolve, reject)=>{
resolve('resolve 1');
}).then((result)=>{
console.log(result);
}).catch((e)=>{
console.error(e);
}).finally(()=>{
console.log('finally');
})

Promise.reject()

改方法返回一个状态为rejected的Promise实例,可以接受一个参数,这个参数或作为rejected的原因。

1
2
Promise.reject('err')
.catch(e=>console.log(e)); // err

Generator

暂无

async/await

个人觉得Generator函数的语法晦涩难懂,且使用起来不太方便,所以就不做他们之间的对比了。

接下来我们就积极的拥抱async/await吧。

async函数内部可以包含多个异步操作,通过await可以让异步操作像同步代码一样书写,并且async返回的还是一个Promise实例,可以用promise方法继续操作。注意,await命令只能在async函数中使用。

基本用法

1
2
3
4
5
6
7
async function doAsync() {
console.log('before');
const result = await fetch('https://api.github.com/');
console.log('after');
}

doAsync();

await命令的功能和Promise.resolve类似,如果await后面是一个promise实例或者thenable对象,就会返回他们的结果,如果后面是其他值,就直接返回该值。

错误处理

如果await后面的promise被rejectedasync函数就会抛出异常,可以使用try...catch处理异常。由于async函数返回的是promise对象,所以也可以在async后面使用catch方法捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function doAsync() {
console.log('before');
// try...catch捕获异常
try {
const result = await new Promise(() => {
throw 'error'
});
} catch (e) {
console.log('err:', e)
}

console.log('after');
}

doAsync();

异步函数问题分析

与setTimeout的的执行顺序

熟悉setTimeout和setInterval都知道,他并不是在指定时间内执行,而是在这个事件内把回调函数放到到事件队列中,具体知识点可以了解事件循环相关资料。根据事件循环中的定义,Promise是在本次循环的微任务中,而setTimeout是在次轮循环当中,所以Promise的then会优先与setTimeout执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log('a');

setTimeout(() => {
console.log('b')
});

new Promise((resolve) => {
console.log('c');
resolve()
}).then(() => {
console.log('d')
});

console.log('e');

// a c e d b

回调函数改写成async/await的注意点

虽然这不是async/await的问题,但这确实能在不经意间写出低效的代码。

案例1:成功的解决了回掉地狱的问题,并且效果是等晓得

案例2:改写完后,原来的a b并行执行顺序被改写成a b c d串行执行,整个函数的执行事件肯定大大增加。我们用改写2方法能够达到同样的效果,但是代码却变得复杂了,虽然这个案例本身有不合理的地方,但是我们在改写成async/await还是需要多注意实际的效果是否和我们预期相符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 四个函数 a b c d

// 案例1
a(()=>{
b();
})
// 案例1 改写
await a();
await b();

// 案例2
a(()=>{
b();
})
c(()=>{
d();
})
// 案例2 改写
await a();
await b();
await c();
await d();
// 案例2 改写2
(async () => {
await a();
b();
})();
(async () => {
await c();
d();
})();

async函数和Promise执行先后顺序

前段时间在知乎上看到讨论async函数的文章,有评论称“前端真TM事儿多,都已经是async函数了还要解决谁先执行”,😂虽然很有道理,但是孔乙己不也很自豪的炫耀“回”字的四种写法么。

分析下面的demo:

1、同步代码执行部分 a c b e i 没有什么异议

2、await后的异步代码的输出顺序,我们期望的可能是 d f g h ,事实并非如此,那么究竟是为什么呢?

原因在于ECMAScript规范导致目前每个await都会创建额外的两个promise对象(即使他本身已经是个promise对象),这就导致了await后面的内容至少需要在第三次微任务队列执行。

据介绍,优化后的变更已经提交,新版本将会判断await后面是不是promise对象,如果为promise对象就会直接返回该对象,从而减少额外的promise创建,我们在canary版 Chrome/73.0.3652.0中执行下面代码,测试结果为a c b e i d f g h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 结果来至:Chrome/71.0.3578.98  
console.log('a');

async function async01() {
console.log('b')
}

(async () => {
console.log('c');
await async01(); // 结果: a c b e i f g d h
// await Promise.resolve(); // 结果: a c b e i f g d h
// await 123; // 结果: a c e i d f g h
console.log('d');
})();

new Promise((resolve) => {
console.log('e');
resolve();
}).then(() => {
console.log('f');
}).then(() => {
console.log('g');
}).then(() => {
console.log('h');
});

console.log('i');

参考资料

更快的异步函数和 Promise

ECMAScript 6 入门 - Promise 对象

MDN web docs - Promise