javascript - 将临时分配给变量时异步函数的不同行为
问题描述
为什么在以下情况下会出现不同的结果?第一个示例工作正常,返回一个包含三个元素的数组["qwe", "rty", "asd"]
。第二个示例仅返回最后一个元素["asd"]
。请解释一下它是如何工作的?为什么会发生这种行为?
在通过中间变量工作的第一个示例awaitResult
中。
class XXX {
constructor() {
this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
}
async getValue(key) {
return this.storage[key];
}
async logValues() {
let keys = [1, 2, 3]
let values = []
// ----- First version -----
await Promise.all(
keys.map(
async key => {
let awaitResult = await this.getValue(key)
values = values.concat(awaitResult)
}
)
);
console.log(values)
}
}
let xxx = new XXX()
xxx.logValues()
在第二个示例中,没有awaitResult
.
class XXX {
constructor() {
this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
}
async getValue(key) {
return this.storage[key];
}
async logValues() {
let keys = [1, 2, 3]
let values = []
// ----- Second version -----
await Promise.all(
keys.map(
async key => values = values.concat(await this.getValue(key)),
)
);
console.log(values)
}
}
let xxx = new XXX()
xxx.logValues()
解决方案
Jonas Wilms的回答是绝对正确的。我只是想通过一些澄清来扩展它,因为有两个关键的事情需要理解:
异步函数实际上是部分同步的
我认为,这是最重要的。这是事情-异步函数101的知识:
- 他们将稍后执行。
- 他们返回一个 Promise。
但第一点实际上是错误的。异步函数会同步运行,直到遇到一个await
关键字后跟一个 Promise,然后暂停,等到 Promise 解决后再继续:
function getValue() {
return 42;
}
async function notReallyAsync() {
console.log("-- function start --");
const result = getValue();
console.log("-- function end --");
return result;
}
console.log("- script start -");
notReallyAsync()
.then(res => console.log(res));
console.log("- script end -");
因此,notReallyAsync
调用时将运行完成,因为其中没有await
。它仍然返回一个 Promise,它只会被放入事件队列并在事件循环的下一次迭代中解决。
但是,如果确实有await
,则该函数会在该点暂停,并且之后的任何代码await
都将仅在 Promise 解决后运行:
function getAsyncValue() {
return new Promise(resolve => resolve(42));
}
async function moreAsync() {
console.log("-- function start --");
const result = await getAsyncValue();
console.log("-- function end --");
return result;
}
console.log("- script start -");
moreAsync()
.then(res => console.log(res));
console.log("- script end -");
因此,这绝对是了解正在发生的事情的关键。第二部分实际上只是第一部分的结果
承诺总是在当前代码运行后得到解决
是的,我之前提到过,但仍然 - 承诺解决作为事件循环执行的一部分发生。网上可能有更好的资源,但我写了一个简单的(我希望)它是如何工作的大纲,作为我在这里的答案的一部分。如果您在那里了解事件循环的基本概念 - 很好,这就是您所需要的基础知识。
本质上,现在运行的任何代码都在事件循环的当前执行中。任何承诺都将最早在下一次迭代中得到解决。如果有多个 Promise,那么您可能需要等待几次迭代。不管怎样,以后都会发生。
那么,这一切如何适用于这里
为了更清楚,这里是解释:之前的代码将与其引用的任何内容的 当前值await
同步完成,而之后的代码将发生下一个事件循环: await
let awaitResult = await this.getValue(key)
values = values.concat(awaitResult)
表示将首先等待该值,然后在解析values
时获取该值并将awaitResult
其连接到该值。如果我们表示按顺序发生的事情,您会得到如下信息:
let values = [];
//function 1:
let key1 = 1;
let awaitResult1;
awaitResult1 = await this.getValue(key1); //pause function 1 wait until it's resolved
//function 2:
key2 = 2;
let awaitResult2;
awaitResult2 = await this.getValue(key2); //pause function 2 and wait until it's resolved
//function 3:
key3 = 3;
let awaitResult3;
awaitResult3 = await this.getValue(key3); //pause function 3 and wait until it's resolved
//...event loop completes...
//...next event loop starts
//the Promise in function 1 is resolved, so the function is unpaused
awaitResult1 = ['qwe'];
values = values.concat(awaitResult1);
//...event loop completes...
//...next event loop starts
//the Promise in function 2 is resolved, so the function is unpaused
awaitResult2 = ['rty'];
values = values.concat(awaitResult2);
//...event loop completes...
//...next event loop starts
//the Promise in function 3 is resolved, so the function is unpaused
awaitResult3 = ['asd'];
values = values.concat(awaitResult3);
因此,您将在一个数组中正确添加所有值。
但是,以下内容:
values = values.concat(await this.getValue(key))
意味着首先 values
将被提取,然后函数暂停以等待this.getValue(key)
. 由于values
将始终在对其进行任何修改之前获取,因此该值始终是一个空数组(起始值),因此等效于以下代码:
let values = [];
//function 1:
values = [].concat(await this.getValue(1)); //pause function 1 and wait until it's resolved
// ^^ what `values` is always equal during this loop
//function 2:
values = [].concat(await this.getValue(2)); //pause function 2 and wait until it's resolved
// ^^ what `values` is always equal to at this point in time
//function 3:
values = [].concat(await this.getValue(3)); //pause function 3 and wait until it's resolved
// ^^ what `values` is always equal to at this point in time
//...event loop completes...
//...next event loop starts
//the Promise in function 1 is resolved, so the function is unpaused
values = [].concat(['qwe']);
//...event loop completes...
//...next event loop starts
//the Promise in function 2 is resolved, so the function is unpaused
values = [].concat(['rty']);
//...event loop completes...
//...next event loop starts
//the Promise in function 3 is resolved, so the function is unpaused
values = [].concat(['asd']);
底线 - 的位置await
确实会影响代码的运行方式,从而影响其语义。
更好的写法
这是一个相当冗长的解释,但问题的实际根源是这段代码没有正确编写:
- 运行
.map
一个简单的循环操作是不好的做法。它应该用于执行映射操作 - 将数组的每个元素 1:1 转换为另一个数组。这里,.map
只是一个循环。 await Promise.all
应该在有多个Promise 等待时使用。values
是异步操作之间的共享变量,它可能会遇到访问公共资源的所有异步代码的常见问题 - “脏”读取或写入可以将资源从与实际状态不同的状态更改。这是第二个中发生的情况每次写入使用初始values
而不是当前保存的代码版本。
适当地使用这些我们得到:
- 用于
.map
创建一个 Promise 数组。 - 用于
await Promise.all
等到上述所有问题都得到解决。 - 当 Promise 被解决时,将结果
values
同步合并。
class XXX {
constructor() {
this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
}
async getValue(key) {
console.log()
return this.storage[key];
}
async logValues() {
console.log("start")
let keys = [1, 2, 3]
let results = await Promise.all( //2. await all promises
keys.map(key => this.getValue(key)) //1. convert to promises
);
let values = results.reduce((acc, result) => acc.concat(result), []); //3. reduce and concat the results
console.log(values);
}
}
let xxx = new XXX()
xxx.logValues()
这也可以在运行时折叠到 Promise API 中Promise.all().then
:
class XXX {
constructor() {
this.storage = {1: ['qwe'], 2: ['rty'], 3: ['asd']}
}
async getValue(key) {
console.log()
return this.storage[key];
}
async logValues() {
console.log("start")
let keys = [1, 2, 3]
let values = await Promise.all( //2. await all promises
keys.map(key => this.getValue(key)) //1. convert to promises
)
.then(results => results.reduce((acc, result) => acc.concat(result), []));//3. reduce and concat the results
console.log(values);
}
}
let xxx = new XXX()
xxx.logValues()
推荐阅读
- google-chrome - 如何解决 Chrome 73 上 SVG 旋转 CSS 中的错误?
- git - Where is the database installed for git bonobo?
- java - 测试 double 的值是否小于 int 的最大值
- php - 如何正确配置 PHPMailer - 当前发送电子邮件时出错
- wordpress - How can I modify post titles before Timber::render?
- reactjs - 需要有关如何将 Jquery .change() 转换为 React.js 以显示/隐藏 div 的帮助
- mysql - Tableau中事实表的测量值错误
- mysql - Symfony 学说:映射:导入不导入反射关联表
- php - 无法选择 ip=inet_pton($ip) 的位置
- php - php Segmentation fault: 11 写入或读取/加载文件时