Why Async/await Have Different Output When Await Expression Is A Argument Of Concat()?
Solution 1:
Looking at the statement
output1 = output1.concat([await sleep(sec)]);
On the left hand side output1
is a variable identifier, used to provide the location in which to store the result of evaluating the right hand side. The variable's binding doesn't change and it always supplies the location of the variable's value.
On the right hand side output1
is a value - the value retrieved from the location supplied by the variable name.
Now if the JavaScript engine retrieves the value of output1
before continuing evaluation, all three map function calls
- retrieve a reference to the empty array stored in
output
, - wait for the timer promise and set
output1
to a new value being the array returned from theconcat
method.
Hence each map operation concatenates an array containing a timer value to an empty array and stores the result in output1
overwriting the result of previous awaited operations.
This explains why you only see the last array stored in output1
when Promise.all
becomes settled. I will also retract the "if the JavaScript engine..." wording above. The JavaScript Engine does get the value of output1
before the await
:
function sleep(ms) {
return new Promise(resolve =>
setTimeout(() => {
console.log( output1.length);
resolve(ms);
}, ms)
);
}
const seconds = [1000, 3000, 2000];
let output1 = [];
let output2 = [];
(async function run1() {
await Promise.all(
seconds.map(async sec => {
output1 = output1.concat([await sleep(sec)]);
//output1.push(await sleep(sec));
console.log(output1[0]);
})
);
console.log({ output1 });
})();
let count = 6;
let timer = setInterval( ()=> {
console.log(output1[0])
if(--count <=0 ) {
clearInterval( timer);
}
}, 500);
To clear up why the second method (run2
) works (which is not related to the existence of closures):
The .map
method calls the map function synchronously and the calls synchronously return a promise without waiting for timer promise fulfillment.
In the second version,
seconds.map(async sec => {
const res = await sleep(sec);
output2 = output2.concat([res]);
}
the const res = await sleep( sec)
line saves the execution context and waits for the sleep
promise to fulfill. When the promise is fulfilled, await
restores the saved context and stores the promise value in res
. The next line
ouput2 = output2.concat([res]);
executes after timer expiry and on the right hand side will load the value of output2
current when the line is executed, as updated by a previous timer expiry if one has occurred.
Contrast this with run1
where the JavaScript engine essentially cached* the value of ouput1
when starting to evaluate the expression on the right hand side of the assignment operator and used the same empty array value for all iterations as demonstrated in the snippet.
*The accepted answer of the duplicate indicates that the left hand operand of an addition operation is retrieved from storage before the right hand operand is returned by await
. In the case of run1
we are seeing that the object on which a method will be called (the value of output1
) is retrieved before the value of the argument used to call the method has been determined. As described in comments to the linked answer, this is quite a "hidden pitfall".
Solution 2:
I add a line in between first example. Guess this will help you see the reason.
"use strict";
function sleep(ms) {
return new Promise(resolve =>
setTimeout(() => {
resolve(ms);
}, ms)
);
}
const seconds = [1000, 3000, 2000];
let output1 = [];
(async function run1() {
await Promise.all(
seconds.map(async sec => {
const dummy = output1;
console.log(`dummy_${sec}`, dummy);
output1 = dummy.concat([await sleep(sec)]);
})
);
console.log({ output1 });
})();
Update:
Below is your original code transpiled to ES5 code, with generator polyfill. Pay attention to how transpiler manages to pause between the output1.concat([ /* pause here */ ])
call.
The trick is to bind .concat
in-place to output1
, before yield and wait for continuation.
At the moment this binding happens, output1 == []
, this is what I wanna emphasis by re-assigning to dummy
variable and print it in previous code.
var seconds = [1000, 3000, 2000];
var output1 = [];
(function run1() {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, Promise.all(seconds.map(function (sec) { return __awaiter(_this, void 0, void 0, function () {
var _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
_b = (_a = output1).concat;
return [4 /*yield*/, sleep(sec)];
case 1:
output1 = _b.apply(_a, [[_c.sent()]]);
return [2 /*return*/];
}
});
}); }))];
case 1:
_a.sent();
console.log({ output1: output1 });
return [2 /*return*/];
}
});
});
})();
Post a Comment for "Why Async/await Have Different Output When Await Expression Is A Argument Of Concat()?"