异步Promise以及Async/Await

本文是阅读这篇文章所写的笔记

Promise 链式写法

Promise 的 then 链式写法本质上是一直往下传递一个新的Promise,也就是说then在下一步接收的是上一步返回的Promise。

看以下代码:

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
32
33
const setDelay = millisecond => {
return new Promise((resolve, reject) => {
if ('number' != typeof millisecond) reject(new Error('参数必须是number类型'))
setTimeout(() => {
resolve(`我延迟了${ millisecond }毫秒后输出的`)
}, millisecond);
})
}

const setSecond = second => {
if ('number' != typeof second || 10 < second) reject(new Error('参数必须是number类型,并且必须小于10'))
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(`我延迟了${ second }秒后输出的`)
}, second * 1000);
})
}

setDelay(3000)
.then(res => {
console.log(res)
console.log('我进行到第一步')
return setSecond(2)
})
.then(result => {
console.log('现在是第二步')
console.log(result)
}) .catch(err => console.log(err))

// 我延迟了3000毫秒后输出的
// 我进行到第一步
// 现在是第二步
// 我延迟了2秒后输出的

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setDelay(2000)
.then(result => {
console.log(result)
console.log('我是进行到第一步的')
return setSecond(20)
})
.then(result => {
console.log('我是进行到第二步的')
console.log(result)
}, err => console.log(err))
.then(result => console.log('我还是继续执行的'))
.catch(err => console.log(err))

// 我延迟了2000毫秒后输出的
// 我是进行到第一步的
// 我出错啦,进到这里捕获错误,但是不经过catch了
// 我还是继续执行的

可以看到进到 then 的第二个参数 reject 中去了,而且不再经过 catch 了。

那么我们把 catch 挪上去,写到 then 错误之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setDelay(2000)
.then(result => {
console.log(result)
console.log('我是进行到第一步的')
return setSecond(20)
})
.catch(err => console.log(err))
.then(result => {
console.log('我是进行到第二步的')
console.log(result)
}, err => console.log('我出错啦,进到这里捕获错误,但是不经过catch了'))
.then(result => console.log('我还是继续执行的'))

// 我延迟了2000毫秒后输出的
// 我是进行到第一步的
// Error: 参数必须是number类型,并且必须小于10
// 我是进行到第二步的
// undefined
// 我还是继续执行的

可以看到先经过 catch 捕获,后面就没有错误了。

结论

  1. catch 写法是针对于整个链式写法的错误而捕获的,而 then 第二个参数是针对于上一个返回的 Promise 的。
  2. 两者的优先级别:写在前面的先捕获,后面就没有错误可以捕获了
  3. 两者都不是 break ,可以继续执行后续的操作而不受影响。
  4. 链式中的 catch 并不是终点,catch 完如果有 then 还会继续往下走。

如何跳出或停止 Promise 链式

  1. 直接拒绝某一链

    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
    setDelay(2000)
    .then(result => {
    console.log(result)
    console.log('我是进行到第一步的')
    return setSecond(2)
    })
    .then(result => {
    console.log('我是进行到第二步的')
    console.log(result)
    console.log('我在这一层主动跳出循环')
    return Promise.reject({
    isNotErrorException: true,
    msg: '跳出循环的信息'
    })
    })
    .then(res => console.log('我不执行'))
    .catch(mes => {
    console.log(mes)
    console.log('我跳出了')
    })

    // 我延迟了2000毫秒后输出的
    // 我是进行到第一步的
    // 我是进行到第二步的
    // 我延迟了2秒后输出的
    // 我在这一层主动跳出循环
    // { isNotErrorException: true, msg: '跳出循环的信息' }
    // 我跳出了

    但是这样存在一个问题,就是当我们的 catch 放在中间,不是末尾,上述方法中止后 catch 后面的代码会继续执行,而我们又不想执行 catch 后面的代码,也就是链式的绝对中止

    如下:

    .catch 后加上这一段:.then(res => console.log('我不想执行,但是却执行了')) ,可以看到最后依然会执行这一句,那么该怎么办呢?

    这时候就要使用第二种方法了。

  2. 让当前层级一直 pending 下去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    setDelay(2000)
    .then((result)=>{
    console.log(result)
    console.log('我进行到第一步的');
    return setDelaySecond(1)
    })
    .then((result)=>{
    console.log(result);
    console.log('我主动跳出循环了');
    // return Promise.reject('跳出循环的信息')
    // 重点在这
    return new Promise(()=>{console.log('后续的不会执行')}) // 这里返回的一个新的Promise,没有resolve和reject,那么会一直处于pending状态,因为没返回啊,那么这种状态就一直保持着,中断了这个Promise
    })
    .then((result)=>{
    console.log('我不执行');
    })
    .catch((mes)=>{
    console.dir(mes)
    console.log('我跳出了');
    })
    .then((res)=>{
    console.log('我也不会执行')
    })

    这样就解决了上述,错误跳出而导致无法完全中止 Promise 链的问题。

    但是这可能会导致潜在的内存泄露

Await、Async

async 的本质

async 声明的函数的返回本质上是一个Promise 对象

只要声明了这个函数是 async ,那么内部不管你怎么处理,它的返回值肯定是个 Promise 对象。

来看下面的例子:

1
2
3
4
5
(async function () {
return '我是Promise'
})()
// 返回的是Promise
// Promise {<resolved>: "我是Promise"}

会自动解析成 Promise.resolve('我是Promise');

等同于:

1
2
3
(async function () {
return Promise.resolve('我是Promise');
})()

await 的本质

await 等的是一个 Promise 的异步返回,如果等待的不是一个 Promise ,是起不到等待一会儿的作用的。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
const demo = async () => {
let res = await setTimeout(() => {
console.log('我延迟了一秒')
}, 1000)
console.log('我由于上面的程序还没执行完,先不执行,等待一会儿')
return res
}
demo ().then(res => console.log('输出', res))

// 我由于上面的程序还没执行完,先不执行“等待一会”
// 输出 1
// 我延迟了一秒

可以看到,并没有 await ,验证了上面所提到的注意事项。

实战演练

场景1

常规使用 asyncawait 时,我们通常将代码块使用 try...catch 来包裹,但是如果我们想拆分开来分别处理,不想因为一个的错误就整个process都crash掉了,那么难道我要写一堆 try...catch 么?

方法一

1
2
3
4
5
6
7
8
9
10
11
12
(async ()=>{
const result = await setDelay(1000).catch(err=>{
console.log(err)
});
console.log(result);
const result1 = await setDelaySecond(12).catch(err=>{
console.log(err)
})
console.log(result1);
console.log(await setDelay(1000));
console.log('完成了');
})()

可以看到,就算是有错误,也不会影响后续的操作。但是,这种方法的缺陷是非常不规整,await 后又跟着 catch

可以改进一下,封装一个提取错误的函数:

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function to (promise) {
return promise.then(data => {
return [null, data]
})
.catch(err => [err])
}

(async ()=>{
// es6的写法,返回一个数组(你可以改回es5的写法觉得不习惯的话),第一个是错误信息,第二个是then的异步返回数据,这里要注意一下重复变量声明可能导致问题(这里举例是全局,如果用let,const,请换变量名)。
[err, result] = await to(setDelay(1000))
// 如果err存在就是有错,不想继续执行就抛出错误
if (err) throw new Error('出现错误,同时我不想执行了');
console.log(result);
[err, result1] = await to(setDelaySecond(12))
// 还想执行就不要抛出错误
if (err) console.log('出现错误,同时我想继续执行', err);
console.log(result1);
console.log(await setDelay(1000));
console.log('完成了');
})()

场景2

依次分别延迟1秒输出值,一共5秒。

延迟函数如下:

1
2
3
4
5
6
7
8
const setDelay = (millisecond) => {
return new Promise((resolve, reject)=>{
if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
setTimeout(()=> {
resolve(`我延迟了${millisecond}毫秒后输出的`)
}, millisecond)
})
}

错误的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
arr = [setDelay(1000), setDelay(1000), setDelay(1000)]
arr[0]
.then(result=>{
console.log(result)
return arr[1]
})
.then(result=>{
console.log(result)
return arr[2]
})
.then(result=>{
console.log(result)
})

执行,我们发现这样输出是并行的。也就是说一秒钟一次性输出了3个值。

原因分析:

setDelay(1000) 这个直接添加到数组的时候,其实就已经执行了,注意你的执行语句 (1000)

所以我们可以将 Promise 预先存储在一个数组中,在调用的时候,再去执行。当然你也可以用闭包的方式存储起来,需要调用的时候再执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
arr = [setDelay, setDelay, setDelay]
arr[0](1000)
.then(result=>{
console.log(result)
return arr[1](1000)
})
.then(result=>{
console.log(result)
return arr[2](1000)
})
.then(result=>{
console.log(result)
})

优化后的Promise循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arr = [setDelay, setDelay, setDelay]
var temp
temp = arr[0](1000)
for (let i = 1; i <= arr.length; i++) {
if (i == arr.length) {
temp.then(result=>{
console.log('完成了');
})
break;
}
temp = temp.then((result)=>{
console.log(result);
return arr[i-1](1000)
});
}

直接使用async/await

1
2
3
4
5
6
7
(async ()=>{
arr = [timeout(2000), timeout(1000), timeout(1000)]
for (var i=0; i < arr.length; i++) {
result = await arr[i]();
console.log(result);
}
})()
文章作者: Yeoman Li
文章链接: https://yeomanli.github.io/2019/07/15/异步Promise以及AsyncAwait/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yeoman's Blog