Why Is Awaiting Inline On A Variable In The Outer Scope Different From Simple Await Assignment?
Solution 1:
For the first case (output1), the current value of output1
is "cached" before calling and evaluating the async function and awaiting it.
It would be more or less equivalent to the following (order of execution follows the sleep time of each async function):
let old= output1; //0
output1 =old+1000;
output1 =old+2000;
output1 =old+3000; //0+3000is the finalresult
For the second case (output2), the async function is evaluated before the current value of output2
is cached. You end up with a sum of all values.
It is more or less equivalent to:
let old= output2; //0
output2 =old+1000;
old= output2; //1000
output2 =old+2000;
old= output2; //3000
output2 =old+3000; //3000+3000is the finalresult
Note: if you use the values [1000, 4000, 2000], you will see that the result is 4000, so only the middle value is added to the output (not 1000+2000 which would also be 3000 with the values from the question)
To be honest, I would have expected the result in the first case to be either 2000 (the last value) or non-deterministic (race-conditions). Apparently, there is some little detail in how JavaScript schedules each async task.
Thanks to A_A with an answer to why the final result is 3000 (or 4000, with updated values): this is the longest delay and it will be executed/finished last.
Additional explanation: there seems to be confusion caused by my bad word choice of "cached". Let me try to explain:
JavaScript programs are generally evaluated top-to-bottom, left-to-right (ignoring async calls for a moment).
let y = 6;
let x = 7;
y = y * x;
Will be "seen" by the JavaScript runtime as:
- Store 6 in y
- Store 7 in x
- Remember current y value (6)
- Remember current x value (7)
- Multiply 6 and 7 (= 42)
- Store 42 in y
Let's map this to your first example:
for (let i of [100,300,200])
output1 = output1 + (await sleep(i));
- Remember current output1 value (0)
- Start first async "sleep" task and await its completion
- Remember current output1 value (0)
- Start second async sleep task and await its completion
- Remember current output1 value (0)
- Start third async sleep task and await its completion
- First sleep task completes (100)
- Add current output1 value and task result (0 + 100)
- Store result in output1 (100)
- Third sleep task completes (200)
- Add current output1 value and task result (0 + 200)
- Store result in output1 (200)
- Second sleep task completes (300)
- Add current output1 value and task result (0 + 300)
- Store result in output1 (300)
- The final value of
output1
is 300
Now compare to the second example. The evaluation order becomes:
for (let i of [100,300,200]) {
const res = await sleep(sec);
output2 = output2 + res;
}
- Start first async "sleep" task and await its completion
- Start second async sleep task and await its completion
- Start third async sleep task and await its completion
- First sleep task completes (100)
- Remember current output2 value (0)
- Add current output2 value and task result (0 + 100)
- Store result in output2 (100)
- Third sleep task completes (200)
- Remember current output2 value (100)
- Add current output2 value and task result (100 + 200)
- Store result in output2 (300)
- Second sleep task completes (300)
- Remember current output2 value (300)
- Add current output2 value and task result (300 + 300)
- Store result in output2 (600)
- The final value of
output2
is 600
Solution 2:
On top of @ef-dknittl-frank's answer. I would like to share my point of view. I have difficulties understanding this question because I want to understand how exactly JS working behind the scene.
TL;DR, if you rewrite the run1 and run2 in .then
, it would be more clear.
run1
can be rewritten as below with .then()
:
(asyncfunctionrun1() {
awaitPromise.all(
seconds.map(sec => {
returnPromise.resolve(output1).then(temp => {
returnsleep(sec).then(res => {
output1 = temp + res;
});
});
})
);
console.log({ output1 });
})();
run2
can be rewritten as below with .then()
:
(asyncfunctionrun2() {
awaitPromise.all(
seconds.map(sec => {
returnsleep(sec).then(res => {
output2 = output2 + res;
});
})
);
console.log({ output2 });
})();
I post this answer just in case you have difficulties like me to understand this question.
The key is what execution context (local scope, scope chain and variables in closures from outer scopes) is when the codes are executing.
For the run1
,
We rewrite the below codes:
output1 = output1 + (awaitsleep(sec));
to
a = b + c
The sequence of execution is:
1. store the value tob2. store the value to c
3. sum b and c
4. assign the sum (b+c) toa
we analyze the sequence one by one.
1. store the value tob
When we execute line 1
, the value of output1
is assigned to b. At this point, output1
is 0
from the scope chain(outer scope) and only sync codes are running. Therefore, all the value of b is 0 inside the map()
loop
2. store the value to c
When we execute line 2
, the value of await sleep(sec)
is assigned to c. For instance, let say 1000
(From await sleep) is assigned to c. At this point, you can already see the log start
and fin
because all sync code(outer scope) is finished before executing line2.
3.sum b and c
When we execute line 3
, b is 0 and c is 1000(from the previous example), so the sum is 1000.
4. assign the sum (b+c) toa
When we execute line 4
, the a
is output1
and output1
is the variable identifier, used to provide the location in which to store a value and in this case, it stores the sum
. To be more clear, the line4 is executing after all sync code(outer scope) is finished, so we access the output1
by Closures
from outer scopes.
Why output1
always show 3000? It is because the line4 above is executed asynchronously and the last code executed is 3000ms. a
(output1) is re-assigned by a new value from the asynchronous codes.
run1
can be rewritten as below with .then()
:
(asyncfunctionrun1() {
awaitPromise.all(
seconds.map(sec => {
returnPromise.resolve(output1).then(temp => {
returnsleep(sec).then(res => {
output1 = temp + res;
});
});
})
);
console.log({ output1 });
})();
For the run2
,
The codes below
output2 = output2 + res;
Both output2
are referenced to the same thing and they are accessed also by Closures
from outer scopes. To be more clear, this line is executing after all sync code(outer scope) is finished, so we access the output2
by Closures
from outer scopes.
run2
can be rewritten as below:
(asyncfunctionrun2() {
awaitPromise.all(
seconds.map(sec => {
returnsleep(sec).then(res => {
output2 = output2 + res;
});
})
);
console.log({ output2 });
})();
Extra point:
if we change the codes of run1
below
from:
output1 = output1 + (awaitsleep(sec));
to:
output1 = (awaitsleep(sec)) + output1;
It will output the same result as run2
.
Let's rewrite it to a = b + c
like the previous example.
The sequence of execution is:
1. await sleep() and store the value to b (it is running asynchronously)
2. store the value to c (c is equal to `output1` which is accessed by Closure)
3. sum b and c
4. assign the sum (b+c) to a (a is equal to `output1` which is accessed by Closure)
If we rewrite the above changes of run1
to .then():
(asyncfunctionrun1() {
awaitPromise.all(
seconds.map(sec => {
returnsleep(sec).then(res => {
output1 = output1 + res;
});
})
);
console.log({ output1 });
})();
Did you notice? it is the same as run2()
.
Post a Comment for "Why Is Awaiting Inline On A Variable In The Outer Scope Different From Simple Await Assignment?"