JavaScript 中的 “await” 关键字将微任务推迟到下一个时钟周期。

huangapple go评论72阅读模式
英文:

javascript await keyword deffer microtask to the next tick

问题

根据MDN关于await的文档:

当在代码中遇到await时(无论是在异步函数中还是在模块中),等待的表达式会被执行,而依赖于该表达式值的所有代码都会被暂停并推送到微任务队列中。

  console.log(name, "start");
  console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// First end
// Second start
// Second middle
// Second end

在这种情况下,这两个异步函数在效果上是同步的,因为它们不包含任何await表达式。这三个语句在同一刻发生。

这很清楚,但是这个:

但是,一旦有一个await,函数就会变成异步的,后续语句的执行会被推迟到下一个tick。

  console.log(name, "start");
  await console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// Second start
// Second middle
// First end
// Second end

我的问题是为什么是下一个tick?难道不是事件循环的同一tick吗?微任务(依赖于表达式值的所有代码)将从微任务队列中获取,但不是在下一个tick吗?

英文:

According to the MDN documentation on await:

> When an await is encountered in code (either in an async function or in a module), the awaited expression is executed, while all code that depends on the expression's value is paused and pushed into the microtask queue.

async function foo(name) {
  console.log(name, "start");
  console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// First end
// Second start
// Second middle
// Second end

> In this case, the two async functions are synchronous in effect, because they don't contain any await expression. The three statements happen in the same tick.

This is clear BUT this:

> However, as soon as there's one await, the function becomes asynchronous, and execution of following statements is deferred to the NEXT tick.

async function foo(name) {
  console.log(name, "start");
  await console.log(name, "middle");
  console.log(name, "end");
}

foo("First");
foo("Second");

// First start
// First middle
// Second start
// Second middle
// First end
// Second end

My question is why NEXT tick? Isn't it the same tick of the event loop? Microtask (all code that depends on the expression's value) will be taken from the microtask queue but NOT on the next tick?

答案1

得分: 2

> 我的问题是为什么是“下一个 tick”?

Mozilla 贡献者对“tick”的定义与 NodeJs 文档的作者所称的“tick”略有不同,这可能是混淆的原因。

> 它不是事件循环的相同 tick 吗?

它确实在事件循环的相同 迭代 中执行。

> 微任务(依赖于表达式值的所有代码)将从微任务队列中取出,但不会在下一个 tick 中执行吗?

then 回调函数(或它们的 await 函数恢复对应函数)确实来自微任务队列。是否将其视为相同的或不同的 tick 取决于“tick”的定义。

不同定义的参考资料

NodeJs 文档

这是 NodeJs 文档将 事件循环 的一个阶段定义为以下内容:

> ```none
> ┌───────────────────────────┐
> ┌─>│ timers │
> │ └─────────────┬─────────────┘
> │ ┌─────────────┴─────────────┐
> │ │ pending callbacks │
> │ └─────────────┬─────────────┘
> │ ┌─────────────┴─────────────┐
> │ │ idle, prepare │
> │ └─────────────┬─────────────┘ ┌───────────────┐
> │ ┌─────────────┴─────────────┐ │ incoming: │
> │ │ poll │<─────┤ connections, │
> │ └─────────────┬─────────────┘ │ data, etc. │
> │ ┌─────────────┴─────────────┐ └───────────────┘
> │ │ check │
> │ └─────────────┬─────────────┘
> │ ┌─────────────┴─────────────┐
> └──┤ close callbacks │
> └───────────────────────────┘
>
> 每个方框都被称为事件循环的一个“阶段”。

在同一文档中,该循环的一个单次迭代被标识为一个“tick”:

> setImmediate() 在事件循环的以下迭代或“tick”上触发

这意味着在一个“tick”(迭代)中可以执行许多已计划的回调函数,包括微任务队列中的回调函数。

Mozilla 贡献者

Mozilla 贡献者以一种更通用和简单的方式定义了“事件循环”,但在讨论微任务时 他们说了与 NodeJs 文档类似的事情:

> 每个代理由一个事件循环驱动,它收集任何用户和其他事件,将任务排队处理每个回调。然后运行任何待处理的 JavaScript 任务,然后运行任何待处理的微任务,然后在循环以检查待处理任务之前再次运行所需的渲染和绘制。

但这并不完全正确,因为HTML 标准规定事件循环的一个迭代将选择并执行一个(!)任务,然后处理任何待处理的微任务。如果有更多任务,则将在事件循环的下一个迭代中处理它们,一次处理一个任务。

主要的区别在于他们所称的“tick”,正如您引用的内容所示,还有这里

> &gt; myPromise &gt; .then((value) =&gt; `${value} and bar`) &gt; .then((value) =&gt; `${value} and bar again`) &gt; .then((value) =&gt; `${value} and again`) &gt; .then((value) =&gt; `${value} and again`) &gt; .then((value) =&gt; { &gt; console.log(value); &gt; }) &gt; .catch((err) =&gt; { &gt; console.error(err); &gt; }); &gt;
> 注意:为了更快的执行,所有同步操作最好在一个处理程序内完成,否则需要多个“ticks”来依次执行所有处理程序。

这里所谓的“tick”是指引擎从微任务队列中启动一个单一的(then-)回调函数,通常是从引擎监视的任何队列中调用回调函数。对于 NodeJs 文档的作者来说,这些回调函数都在同一个“tick”中执行。

如果这还不够令人困惑,NodeJs 包括一个名为 process.nextTick() 的函数来安排回调函数的执行,相应的回调函数队列实际上在同一事件循环迭代中处理!那么名字中包含什么...

> process.nextTick() 队列始终在 Node.js 事件循环的每一轮内在微任务队列之前处理。

结论

您对过程的理解是正确的,但不同作者对“tick”一词的不同定义给这个主题带来了混淆。

我建议完全避免使用这个术语。

英文:

> My question is why NEXT tick?

The definition that Mozilla Contributors use for "tick" is slightly different from what the authors of the NodeJs documentation call "tick", and this might be the cause of confusion.

> Isn't it the same tick of the event loop?

It is indeed executed in the same iteration of the event loop.

> Microtask (all code that depends on the expression's value) will be taken from the microtask queue but NOT on the next tick?

then-callbacks (or their await function-resuming counterparts) are indeed taken from the microtask queue. Whether or not this is regarded as the same or different tick, depends on the definition of "tick".

References for the differing definitions

NodeJs Docs

This is what the NodeJs documentation defines as one phase of the event loop:

> ```none
> ┌───────────────────────────┐
> ┌─>│ timers │
> │ └─────────────┬─────────────┘
> │ ┌─────────────┴─────────────┐
> │ │ pending callbacks │
> │ └─────────────┬─────────────┘
> │ ┌─────────────┴─────────────┐
> │ │ idle, prepare │
> │ └─────────────┬─────────────┘ ┌───────────────┐
> │ ┌─────────────┴─────────────┐ │ incoming: │
> │ │ poll │<─────┤ connections, │
> │ └─────────────┬─────────────┘ │ data, etc. │
> │ ┌─────────────┴─────────────┐ └───────────────┘
> │ │ check │
> │ └─────────────┬─────────────┘
> │ ┌─────────────┴─────────────┐
> └──┤ close callbacks │
> └───────────────────────────┘
>
> Each box will be referred to as a "phase" of the event loop.

In the same document, a single iteration of this loop is identified as a "tick":

> setImmediate() fires on the following iteration or 'tick' of the event loop

This means in one tick (iteration), there can be many scheduled callbacks that are executed, including callbacks in the microtask queue.

Mozilla Contributors

Mozilla Contributors define "event loop" in a more generic and simplistic way here, but when discussing microtasks they say something similar to the NodeJs documentation:

> Each agent is driven by an event loop, which collects any user and other events, enqueuing tasks to handle each callback. It then runs any pending JavaScript tasks, then any pending microtasks, then performs any needed rendering and painting before looping again to check for pending tasks.

But this is not entirely correct, as the HTML Standard stipulates that one iteration of the event loop will pick and execute one(!) task, and will then process any pending microtasks. If there are more tasks, then they will be treated in the next iterations of the event loop, one task at a time.

The major difference comes with what they call a "tick", as seen in what you quoted, and also here:

>
&gt; myPromise
&gt; .then((value) =&gt; `${value} and bar`)
&gt; .then((value) =&gt; `${value} and bar again`)
&gt; .then((value) =&gt; `${value} and again`)
&gt; .then((value) =&gt; `${value} and again`)
&gt; .then((value) =&gt; {
&gt; console.log(value);
&gt; })
&gt; .catch((err) =&gt; {
&gt; console.error(err);
&gt; });
&gt;

> Note: For faster execution, all synchronous actions should preferably be done within one handler, otherwise it would take several ticks to execute all handlers in sequence.

What is called a tick here, refers to the engine initiating a single (then-) callback from the microtask queue, and in general, calling a callback from any queue that is monitored by the engine. For the authors of the NodeJs documentation those callbacks are all made in the same "tick".

As if this is not confusing enough, NodeJs includes a function called process.nextTick() to schedule the execution of a callback, and the corresponding queue of callbacks is actually processed within the same event loop iteration! So what's in a name...:

> The process.nextTick() queue is always processed before the microtask queue within each turn of the Node.js event loop.

Conclusion

Your understanding of the process is correct, but the different definitions of the word "tick" by different authors is bringing confusion to the subject.

I would avoid the term all together.

答案2

得分: 0

当JS遇到await时,它立即执行语句,如果该语句返回一个已完成的Promise或一个值(被视为已解决的Promise),则创建一个微任务,将其推送到当前任务的末尾以执行下一个语句。

我们可以重构代码,用已完成的Promise等效物替换console.log,然后将其记录到控制台:

async function foo(name) {
    console.log(name, "start");
    const promise = Promise.resolve(console.log(name, "middle"));
    console.log(promise);
    await promise;
    console.log(name, "end"); // 在2个单独的微任务中执行
}

foo("First");
foo("Second");

有趣的是,我们可以利用微任务,例如从用户收集同步输入并在微任务中做出响应。在这里,我使用它来收集任意顺序的鼠标指针数据,并稍后在微任务中处理它。

这是一个非常酷的功能,因为它使UI看起来同步,因为没有创建新的任务。另一方面,使用setTimeout可能会导致UI不太愉快的结果,因为会创建新任务,允许事件循环从UI/IO事件中触发任何中间任务,这可能会改变我们当前UI工作中使用的状态:

// 使用已解决的Promise将移动延迟为微任务,因此状态变异的顺序并不重要
ops[prop] && Promise.resolve().then(ops[prop]);

JavaScript 中的 “await” 关键字将微任务推迟到下一个时钟周期。

https://stackoverflow.com/questions/76420174/is-there-a-design-pattern-for-logic-performed-on-multiple-event-listeners/76420340#76420340

英文:

When JS encounters await it executes the statement immediately and if the statement returns a fullfilled promise or a value (which is treated as a resolved promise) creates a microtask pushing it to the end of the current task for executing the next statements.

We could refactor the code to replace console.log with a fullfilled promise equivalent and console.log it:

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

async function foo(name) {
    console.log(name, &quot;start&quot;);
    const promise = Promise.resolve(console.log(name, &quot;middle&quot;));
    console.log(promise);
    await promise;
    console.log(name, &quot;end&quot;); // executed in 2 separate microtasks
}

foo(&quot;First&quot;);
foo(&quot;Second&quot;);

<!-- end snippet -->

JavaScript 中的 “await” 关键字将微任务推迟到下一个时钟周期。

Interestingly enough but we can use microtasks to our advantage for example collecting sync input from the user and react to it in a microtask. Here I use that to collect mouse pointer data in any order and handle it later in a microtask.

That's a really cool feature, it makes the UI looking sync, because there's no new tasks created. On the other hand using setTimeout could give a less pleasant result with possible jittering of the UI since new tasks are created allowing the event loop fire any intermediate tasks from the UI/IO events that could even mutate our state used in our current UI work:

    // use a resolved Promise to postpone the move as a microtask so
    // the order of state mutation isn&#39;t important
    ops[prop] &amp;&amp; Promise.resolve().then(ops[prop]);

https://stackoverflow.com/questions/76420174/is-there-a-design-pattern-for-logic-performed-on-multiple-event-listeners/76420340#76420340

huangapple
  • 本文由 发表于 2023年6月22日 19:02:16
  • 转载请务必保留本文链接:https://go.coder-hub.com/76531222.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定