JavaScript何时同步?


202

我一直以为JavaScript总是异步的。但是,我了解到在某些情况下不是(即DOM操作)。关于何时何时将同步以及何时异步将有很好的参考吗?jQuery会完全影响吗?


14
总是有ajax例外。
defau1t 2014年

接受的答案是错误的,并且有误导性,请检查一下。
Suraj Jain

2
观看youtube.com/watch?v=8aGhZQkoFbQ有助于了解事件循环,以及栈,Web API和任务队列在同步和异步方面的工作方式也很有用
mtpultz

1
@ defau1t没错,JavaScript始终是同步的,当ajax调用完成时,回调最终在队列中结束,这是Java脚本同步特性的一个例外。
Suraj Jain

Answers:


281

JavaScript始终是同步和单线程的。如果您要在页面上执行JavaScript代码块,那么该页面上当前不会执行其他JavaScript。

JavaScript仅在可以进行Ajax调用的意义上是异步的。Ajax调用将停止执行,其他代码将能够执行,直到调用返回(成功或其他)为止,此时回调将同步运行。此时将不会再运行其他代码。它不会中断当前正在运行的任何其他代码。

JavaScript计时器与此相同类型的回调一起运行。

将JavaScript描述为异步可能会引起误解。准确地说JavaScript是同步的并且具有各种回调机制的单线程。

jQuery在Ajax调用上有一个选项可以使它们同步(带有该async: false选项)。初学者可能会不正确地使用它,因为它允许使用一种更传统的编程模型,而该模型可能会更习惯。有问题的原因是此选项将阻止页面上的所有 JavaScript,直到完成为止,包括所有事件处理程序和计时器。


31
抱歉,我不太理解此语句“代码将停止执​​行,直到调用返回(成功或错误)为止”。你能详细说明一下吗?当您还说“它不会中断正在运行的任何其他代码”时,该语句如何成立?您是否仅在第一条语句中谈论回调代码?请赐教。
克里希纳

2
NETTUTS有一个教程,在这里解释异步的基础是相当不错:net.tutsplus.com/tutorials/javascript-ajax/...
RobW

26
@cletus语句“代码将停止执​​行,直到调用返回”需要更正,因为执行不会停止。代码执行可以继续。否则,这意味着呼叫是同步的。
HS。

1
我也不明白那句话。
2013年

12
这个答案令人难以置信的误导和混乱。请改为查看CMS'或Faraz Ahmad的答案。
iono

214

JavaScript是单线程的,并具有同步执行模型。单线程意味着一次正在执行一个命令。同步意味着一次一次,即一次执行一行代码以使代码出现。因此,在JavaScript中,一次只能发生一件事。

执行上下文

JavaScript引擎与浏览器中的其他引擎进行交互。在JavaScript执行堆栈中,底部具有全局上下文,然后在调用函数时,JavaScript引擎会为各个函数创建新的执行上下文。当被调用函数退出时,其执行上下文将从堆栈中弹出,然后弹出下一个执行上下文,依此类推...

例如

function abc()
{
   console.log('abc');
}


function xyz()
{
   abc()
   console.log('xyz');
}
var one = 1;
xyz();

在上面的代码中,将创建一个全局执行上下文,var one并将其存储在该上下文中,其值将为1 ...当调用xyz()调用时,将创建一个新的执行上下文,并且如果我们定义了任何变量在xyz函数中,这些变量将存储在xyz()的执行上下文中。在xyz函数中,我们调用abc(),然后创建abc()执行上下文并将其放在执行堆栈上...现在,当abc()完成后,从堆栈中弹出其上下文,然后从以下位置弹出xyz()上下文:堆栈,然后弹出全局上下文...

现在介绍异步回调;异步意味着一次多个。

就像执行堆栈一样,还有事件队列。当我们希望收到有关JavaScript引擎中某个事件的通知时,我们可以侦听该事件,并将该事件放在队列中。例如,一个Ajax请求事件或HTTP请求事件。

每当执行堆栈为空时,如上面的代码示例所示,JavaScript引擎就会定期查看事件队列,并查看是否有任何事件要通知。例如,在队列中有两个事件,一个ajax请求和一个HTTP请求。它还会查看是否有需要在该事件触发器上运行的函数...因此,JavaScript引擎将收到有关该事件的通知,并知道在该事件上要执行的相应函数...因此,JavaScript引擎将调用处理程序函数,在示例情况下,例如AjaxHandler()将被调用,并且像通常在调用函数时一样,将其执行上下文放在执行上下文上,现在函数执行完成,并且事件ajax请求也从事件队列中删除... 当AjaxHandler()完成时,执行堆栈为空,因此引擎再次查看事件队列并运行队列中的下一个HTTP请求的事件处理函数。重要的是要记住,仅当执行堆栈为空时才处理事件队列。

例如,请参见下面的代码,以解释Javascript引擎的执行堆栈和事件队列处理。

function waitfunction() {
    var a = 5000 + new Date().getTime();
    while (new Date() < a){}
    console.log('waitfunction() context will be popped after this line');
}

function clickHandler() {
    console.log('click event handler...');   
}

document.addEventListener('click', clickHandler);


waitfunction(); //a new context for this function is created and placed on the execution stack
console.log('global context will be popped after this line');

<html>
    <head>

    </head>
    <body>

        <script src="program.js"></script>
    </body>
</html>

现在运行该网页并单击该页面,然后在控制台上查看输出。输出将是

waitfunction() context will be popped after this line
global context will be emptied after this line
click event handler...

如执行上下文部分所述,JavaScript引擎正在同步运行代码,浏览器正在异步将事件放入事件队列。因此,需要很长时间才能完成的功能会中断事件处理。在浏览器中发生的事件(例如事件)可以通过JavaScript以这种方式处理,如果应该运行一个侦听器,则当执行堆栈为空时,引擎将运行它。而且事件是按照事件发生的顺序进行处理的,所以异步部分是关于引擎外部发生的事情,即,当这些外部事件发生时引擎应该怎么做。

因此,JavaScript始终是同步的。


16
这个答案很明确,应该得到更多的支持。
ranu

7
当然,我已经阅读了有关Javascript异步行为的最佳解释。
查尔斯·贾梅特

1
关于执行上下文和队列的很好的解释。
Divyanshu Maithani

1
当然,这需要您阅读一些有关执行上下文堆栈的信息,只有添加它为空和事件que才使我最终感觉像我对确定性的Java脚本执行有所了解。更糟糕的是,我觉得它只需要阅读一页,却几乎找不到任何地方。那么,为什么没人说呢?他们不知道还是什么?但是我觉得如果一个js教程有这个功能,可以节省很多时间。>:|
元帅工艺

2
完美的解释!
Julsy

100

JavaScript是单线程的,并且您一直在执行常规的同步代码流执行。

JavaScript可以具有的异步行为的好例子是事件(用户交互,Ajax请求结果等)和计时器,基本上是随时可能发生的动作。

我建议您看一下以下文章:

该文章将帮助您了解JavaScript的单线程性质,计时器在内部的工作方式以及异步JavaScript执行的工作方式。

异步的


接受的答案会误导我们在这种情况下可以采取某些措施?/
Suraj Jain

8

对于真正了解JS的工作原理的人来说,这个问题似乎有些错,但是大多数使用JS的人没有这么深的洞察力(并不一定需要它),对于他们来说,这是一个相当混乱的观点,我会尝试从这个角度回答。

JS在执行代码方面是同步的。每行仅在完成之前在该行之后运行,如果该行在完成之后调用一个函数,则...

造成混淆的主要原因是您的浏览器能够告诉JS随时执行更多代码(与您如何从控制台执行页面上执行更多JS代码类似)。举例来说,JS具有回调函数,其目的是允许JS异步进行行为,以便在等待已执行的JS函数(即GET调用)返回一个答案的同时运行JS的其他部分,JS会继续运行直到此时浏览器有一个答案,事件循环(浏览器)将执行调用回调函数的JS代码。

由于事件循环(浏览器)可以输入更多要在任何时候执行的JS,因此JS是异步的(导致浏览器输入JS代码的主要因素是超时,回调和事件)

我希望这很清楚,可以对某人有所帮助。


4

定义

术语“异步”可以以略有不同的含义使用,从而导致看似矛盾的答案,而实际上却并非如此。异步维基百科具有以下定义:

在计算机程序设计中,异步是指事件的发生与主程序流程以及处理此类事件的方式无关。这些可能是“外部”事件,例如信号的到来,或与程序执行同时发生的由程序引发的动作,而程序不会阻塞等待结果。

非JavaScript代码可以将此类“外部”事件排队到一些JavaScript的事件队列中。但是,就目前而言。

无抢占

运行JavaScript代码不会外部中断,以便在脚本中执行其他一些JavaScript代码。一段JavaScript接一个地执行,其顺序由每个事件队列中的事件顺序以及这些队列的优先级确定。

例如,您可以绝对确定在执行以下代码时,不会再执行其他JavaScript(在同一脚本中):

let a = [1, 4, 15, 7, 2];
let sum = 0;
for (let i = 0; i < a.length; i++) {
    sum += a[i];
}

换句话说,JavaScript中没有抢占。无论事件队列中有什么内容,这些事件的处理都必须等到这段代码运行完毕为止。EcmaScript规范在第8.4节Jobs和Jobs Queues中说

仅当没有正在运行的执行上下文并且执行上下文堆栈为空时,才能启动作业的执行。

异步的例子

正如其他人已经写过的那样,异步在JavaScript中有多种情况起作用,并且总是涉及一个事件队列,只有在没有其他JavaScript代码正在执行时,这才导致JavaScript执行:

  • setTimeout():当超时时间到期时,代理(例如浏览器)会将事件放入事件队列。时间的监视和事件在队列中的放置是通过非JavaScript代码进行的,因此您可以想象这与某些JavaScript代码的潜在执行同时发生。但是,setTimeout仅当当前正在执行的JavaScript代码运行完毕并且正在读取适当的事件队列时,才能执行提供给该回调的回调。

  • fetch():代理将使用OS功能执行HTTP请求并监视任何传入的响应。同样,此非JavaScript任务可以与仍在执行的某些JavaScript代码并行运行。但是,将解决所返回的诺言的诺言解析程序fetch()只能在当前执行的JavaScript运行完毕后才能执行。

  • requestAnimationFrame():当浏览器的渲染引擎(非JavaScript)准备好执行绘制操作时,会在JavaScript队列中放置一个事件。处理JavaScript事件后,将执行回调函数。

  • queueMicrotask():立即将事件放置在微任务队列中。当调用堆栈为空并且使用了该事件时,将执行回调。

还有更多示例,但是所有这些功能都是由主机环境提供的,而不是由核心EcmaScript提供的。使用核心EcmaScript,您可以使用同步将事件放入Promise Job Queue中Promise.resolve()

语言构造

EcmaScript的提供了几种语言结构,支持异步模式,比如yieldasyncawait。但是请不要误会:外部事件不会中断 JavaScript代码。的“中断”的是yieldawait似乎提供仅仅是一个控制,从函数调用返回后来就恢复其执行上下文的预定义的方式,或者通过JS代码(在的情况下yield),或在事件队列(在的情况下await)。

DOM事件处理

当JavaScript代码访问DOM API时,在某些情况下,这可能会使DOM API触发一个或多个同步通知。并且,如果您的代码中有一个事件处理程序正在侦听该事件处理程序,那么它将被调用。

这可能是抢先式并发,但事实并非如此:一旦事件处理程序返回,DOM API最终也会返回,并且原始JavaScript代码将继续。

在其他情况下,DOM API只会在适当的事件队列中调度一个事件,并且在清空调用栈后,JavaScript会对其进行提取。

查看同步和异步事件


0

在所有情况下都是同步的。

使用Promises以下命令阻塞线程的示例:

  const test = () => new Promise((result, reject) => {
    const time = new Date().getTime() + (3 * 1000);

    console.info('Test start...');

    while (new Date().getTime() < time) {
      // Waiting...
    }

    console.info('Test finish...');
  });

  test()
    .then(() => console.info('Then'))
    .finally(() => console.info('Finally'));

  console.info('Finish!');

输出将是:

Test start...
Test finish...
Finish!
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.