Promise、async/await 与 setInterval

TL;DR;

易混淆点:async 修饰的函数,不管函数内部 return 的是啥,最后都返回一个 Promise。而 return 返回的值,最后在 Promise.then() 里取到。

async 修饰一个函数,只是说明会隐式地返回一个 Promise,并不是说 这个函数内的操作都在一个 Promise

也就是说,async 修饰一个方法时,并不能保证方法里的操作同步执行。 (一般 async 修饰的方法,同步操作会使用关键字 await 。)

需求

定时循环执行一个数学计算,把计算出来的值返回并打印出来。

分析

不假思索写出一段代码:

1
2
3
4
5
6
7
8
9
let count = 0;
setInterval(function () {
count++;
if (count === 3) {
clearInterval(this);
}
}, 1000);

console.log(count);

执行的结果是:

1
0

这样,肯定是不行的……

JavaScript 是异步的,实际上这段代码里的 setInterval()console.log() 会同时执行。

要使它们的执行有一个先后顺序,这就涉及到 JavaScript 里如何「同步」的问题了。

同步的方式有许多种,这里谈谈 Promiseasync/await

Promise 和 setInterval

0x01 错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function testA() {
console.log("testA() 开始执行……");
return new Promise(function (resolve, reject) {
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
}
}, 2000);
resolve("testA() 返回内容,计算得到 count = " + count);
});
}

testA().then((data) => {
console.log(data);
console.log("testA.then() 开始执行");
});

执行的结果是:

1
2
3
4
5
6
testA() 开始执行……
testA() 返回内容,计算得到 count = 0
testA.then() 开始执行
testA() setTimeout 内容……
testA() setTimeout 内容……
testA() setTimeout 内容……

0x01 分析

Promise 的构造函数接受一个函数作为参数,这个函数又使用两个函数作为参数:resolvereject

简单理解的话,resolvereject 这两个函数都用于在 Promise 里返回值。后面调用 Promisethen() 时,就可以获取到返回的值。他们的不同之处在于 resolvePromise 的状态标志为「成功」,而 rejectPromise 的状态标志为「失败」(通常对应遇到异常)。

这段代码的问题和上面的一样,即 setIntervalresolve 会同时运行。所以执行 testA 后,早早地拿到了上面根本没有进入 setInterval 进行运算的 count 的值。

0x02 这样有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function testA() {
console.log("testA() 开始执行……");
return new Promise(function (resolve, reject) {
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
resolve("testA() 返回内容,计算得到 count = " + count);
}
}, 2000);
});
}

testA().then((data) => {
console.log(data);
console.log("testA.then() 开始执行");
});

执行的结果是:

1
2
3
4
5
6
testA() 开始执行……
testA() setTimeout 内容……
testA() setTimeout 内容……
testA() setTimeout 内容……
testA() 返回内容,计算得到 count = 3
testA.then() 开始执行

0x02 分析

我们把 resolve 也放进 setInterval 里,当计算完成后再返回,这样,就能保证拿到计算后的值啦。

async await 与 setInterval

我想整高级点,用 asyncawait 来重写,应该咋整?

0x03 错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function testA() {
console.log("testA() 开始执行……");
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
}
}, 1000);
return "testA() 返回内容,计算得到 count = " + count;
}

testA().then((data) => {
console.log(data);
console.log("testA.then() 开始执行了");
});

执行的结果是:

1
2
3
4
5
6
testA() 开始执行……
testA() 返回内容,计算得到 count = 0
testA.then() 开始执行了
testA() setTimeout 内容……
testA() setTimeout 内容……
testA() setTimeout 内容……

0x03 分析

同理,setIntervalreturn 会同时运行。所以执行 testA 后,早早地拿到了上面根本没有进入 setInterval 进行运算的值。

0x04 还是错误的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function testA() {
console.log("testA() 开始执行……");
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
return "testA() 返回内容,计算得到 count = " + count;
}
}, 1000);
}

testA().then((data) => {
console.log(data);
console.log("testA.then() 开始执行了");
});

执行的结果是:

1
2
3
4
5
6
testA() 开始执行……
undefined
testA.then() 开始执行了
testA() setTimeout 内容……
testA() setTimeout 内容……
testA() setTimeout 内容……

0x04 分析

想当然地用类似上面 Promise 的方法,把 return 放在 setInterval 里面,还是不行。

获取到的 dataundefined。这说明有个隐式的 returnsetInterval 一起执行啦……

这可咋整啊……

setInterval 是异步的,咋整,用最上面的方法,套个 Promise 呗……可这不就又回到最初的起点了吗……

0x05 这样可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function testA() {
console.log("testA() 开始执行……");
let result = await new Promise((resolve, reject) => {
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
resolve("testA() 返回内容,计算得到 count = " + count);
}
}, 2000)
});
console.log("testA 延迟后的内容");
return result;
}

testA().then(result => {
console.log(result);
console.log("testA.then() 开始执行了");
})

结果:

1
2
3
4
5
6
7
testA() 开始执行……
testA() setTimeout 内容……
testA() setTimeout 内容……
testA() setTimeout 内容……
testA 延迟后的内容
testA() 返回内容,计算得到 count = 3
testA.then() 开始执行了

0x05 分析

setInterval 包装到一个 Promise 里并返回,然后使用 await 等待这个 Promise(包装着 setInterval)执行完并取得返回值。

再把这个返回值返回。

为啥呢

async 修饰的函数,不管 return 的是啥,最后都返回一个 Promise。而 return 返回的值,最后在 Promise.then() 里取到。

例如:

1
2
3
4
5
6
7
8
9
async function getCount() {
return 10;
}

getCount().then((count) => {
console.log(count);
});

// 10

为啥 0x03 和 0x04 是错误的呢?

因为 async 修饰一个函数,只是说明会隐式地返回一个 Promise,并不是说 这个函数内的操作都在一个 Promise

也就是说,async 修饰一个方法,并不能保证方法里的操作同步执行。

之前我把 async 这类关键字与 Java 里面的一些关键字对比理解……

按我的理解,「0x03 错误示例」就相当于:

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
34
35
36
function testA() {
console.log("testA() 开始执行……");
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
}
}, 1000);
return Promise.resolve("testA() 返回内容,计算得到 count = " + count);
}

testA().then((data) => {
console.log(data);
console.log("testA.then() 开始执行了");
});

// 0x03 错误示例
async function testA() {
console.log("testA() 开始执行……");
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
}
}, 1000);
return "testA() 返回内容,计算得到 count = " + count;
}

testA().then((data) => {
console.log(data);
console.log("testA.then() 开始执行了");
});

其它

虽然标题是:「Promise、async/await 与 setInterval」,实际上 setTimeout 也是一个道理。

箭头函数

我在这篇文章里用 setInterval 时,用的是:

1
2
3
4
5
6
7
8
let count = 0;
setInterval(function () {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
}
}, 1000);

setInterval 传入一个函数作为参数,使用:

1
function(){}

而不能用:

1
()=>{}

这是因为 function(){} 函数里的 this 指的是 function(){} 本身,而 ()=>{} 里的 this 则指的是 ()=>{} 外部的环境。

按定义来讲则是:箭头函数没有 this。如果访问 this,则从外部获取。

如果我们在这用 ()=>{} 的话,如下:

1
2
3
4
5
6
7
8
let count = 0;
setInterval(()=> {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(this);
}
}, 1000);

这里的 setInterval 则会一直循环执行,不会停止。

因为 clearInterval(this) 根本没得效果。

如果要用箭头函数的话,需要这样:

1
2
3
4
5
6
7
8
let count = 0;
let interval = setInterval(() => {
count++;
console.log("testA() setTimeout 内容……");
if (count === 3) {
clearInterval(interval);
}
}, 1000);