JavaScript 何时同步?

Posted

技术标签:

【中文标题】JavaScript 何时同步?【英文标题】:When is JavaScript synchronous? 【发布时间】:2011-01-03 09:16:37 【问题描述】:

我一直认为 javascript 总是异步的。但是,我了解到有些情况并非如此(即 DOM 操作)。是否有关于何时同步和何时异步的良好参考? jQuery 对此有影响吗?

【问题讨论】:

总是除了 ajax。 接受的答案有误,有误导,请查收。 观看youtube.com/watch?v=8aGhZQkoFbQ 以了解事件循环以及堆栈、Web API 和任务队列在同步和异步方面的工作方式也很有用 @defau1t 是不是错了,JavaScript一直是同步的,当ajax调用完成后回调就进入队列了,怎么会是java脚本同步的异常呢。 【参考方案1】:

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

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

我建议你看看下面的文章:

How JavaScript Timers Work

那篇文章将帮助您了解 JavaScript 的单线程特性以及计时器如何在内部工作以及异步 JavaScript 执行的工作原理。

【讨论】:

接受的答案会误导我们在这种情况下可以做些什么吗?/【参考方案2】:

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

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

JavaScript 计时器使用相同类型的回调进行操作。

将 JavaScript 描述为异步可能会产生误导。更准确的说法是 JavaScript 是同步单线程的,有多种回调机制。

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

【讨论】:

对不起,我不太明白这句话“代码将停止执​​行,直到调用返回(成功或错误)”。你能详细说明一下吗?当您还说“它不会中断任何其他正在运行的代码”时,该陈述怎么可能是真的?您是否仅在第一条语句中谈论回调代码?请赐教。 Nettuts 有一个教程很好地解释了异步的基础知识:net.tutsplus.com/tutorials/javascript-ajax/… @cletus 语句“代码将停止执​​行,直到调用返回”需要更正,因为执行不会停止。代码执行可以继续。否则,这将意味着调用是同步的。 这个答案令人难以置信的误导和混淆。请参阅 CMS 或 Faraz Ahmad 的回答。 很遗憾,这个错误且具有误导性的答案获得了近 200 个赞。我尝试进行编辑,但它被拒绝了,因为它“偏离了帖子的初衷”。 “代码将停止执​​行,直到调用返回”。这是错误的。这个答案应该被编辑或删除。【参考方案3】:

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 总是同步的。

【讨论】:

这个答案很清楚,应该得到更多的支持。 当然是我读过的对 Javascript 异步行为的最佳解释。 很好地解释了执行上下文和队列。 当然,这需要您阅读一些有关执行上下文堆栈的信息,只有添加它是空的和事件队列才让我最终觉得我确定性地理解了 java 脚本执行。更糟糕的是我觉得它只需要阅读一页,但我几乎找不到任何地方。那为什么没人说呢?要么他们不知道,要么什么?但我觉得如果一个 js 教程有这个,它可以为我节省很多时间。 >:| 完美解释!【参考方案4】:

对于真正了解 JS 工作原理的人来说,这个问题可能看起来不对,但是大多数使用 JS 的人没有这么深的洞察力(也不一定需要它),对他们来说这是一个相当令人困惑的点,我会试着从这个角度来回答。

JS 的代码执行方式是同步的。每行仅在完成之前的行之后运行,并且如果该行在完成之后调用函数等...

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

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

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

【讨论】:

【参考方案5】:

定义

“异步”一词的含义可能略有不同,导致此处的答案看似矛盾,但实际上并非如此。 Wikipedia on Asynchrony 有这个定义:

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

非 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 中没有preemption。无论事件队列中可能有什么,这些事件的处理都必须等到这段代码运行完成。 EcmaScript 规范在section 8.4 Jobs and Jobs Queues 中说:

只有在没有运行的执行上下文且执行上下文堆栈为空的情况下才能启动Job的执行。

异步示例

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

setTimeout():代理(例如浏览器)将在超时到期时将事件放入事件队列中。时间监控和事件在队列中的放置是由非 JavaScript 代码进行的,因此您可以想象这与一些 JavaScript 代码的潜在执行并行发生。但是提供给setTimeout 的回调只有在当前执行的 JavaScript 代码运行完成并且正在读取相应的事件队列时才能执行。

fetch():代理将使用操作系统函数来执行 HTTP 请求并监视任何传入响应。同样,这个非 JavaScript 任务可能与一些仍在执行的 JavaScript 代码并行运行。但是,将解析 fetch() 返回的 Promise 的 Promise 解析过程只能在当前执行的 JavaScript 运行完成时执行。

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

queueMicrotask():立即将事件放入微任务队列。当调用堆栈为空并且该事件被消费时,回调将被执行。

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

语言结构

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

DOM 事件处理

当 JavaScript 代码访问 DOM API 时,这在某些情况下可能会使 DOM API 触发一个或多个同步通知。如果你的代码有一个事件处理程序监听它,它就会被调用。

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

在其他情况下,DOM API 只会在适当的事件队列中分派一个事件,一旦调用堆栈被清空,JavaScript 就会拾取它。

见synchronous and asynchronous events

【讨论】:

【参考方案6】:

“我一直认为 JavaScript 总是 异步”

您可以以同步方式或异步方式使用 JavaScript。事实上,JavaScript 有非常好的异步支持。例如,我可能有需要数据库请求的代码。然后,我可以在等待该请求完成的同时运行其他代码,而不是 dependent 该请求。 Promise、async/await 等都支持这种异步编码。但如果您不需要一种很好的方式来处理长时间等待,那么只需同步使用 JS。

“异步”是什么意思。那么它并不意味着多线程,而是描述了一种非依赖关系。从这个受欢迎的answer 中查看这张图片:

         A-Start ------------------------------------------ A-End   
           | B-Start -----------------------------------------|--- B-End   
           |    |      C-Start ------------------- C-End      |      |   
           |    |       |                           |         |      |
           V    V       V                           V         V      V      
1 thread->|<-A-|<--B---|<-C-|-A-|-C-|--A--|-B-|--C-->|---A---->|--B-->| 

我们看到单线程应用程序可以具有异步行为。函数 A 中的工作不依赖于函数 B 的完成,因此当函数 A 在函数 B 之前开始时,函数 A 能够在稍后的时间在同一个线程上完成。

因此,仅仅因为 JavaScript 在单个线程上一次执行一个命令,并不意味着 JavaScript 只能用作同步语言。

“是否有关于何时同步和何时异步的良好参考”

我想知道这是否是您问题的核心。我认为您的意思是您如何知道您正在调用的某些代码是异步的还是同步的。也就是说,您的其余代码会在您等待某个结果时运行并执行某些操作吗?您的第一个检查应该是您使用的任何库的文档。例如,节点方法具有明确的名称,例如 readFileSync。如果文档不好,那么这里有很多关于 SO 的帮助。例如:

How to know if a function is async?

【讨论】:

以上是关于JavaScript 何时同步?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScrip笔记心得(持续更新)

JavaScrip笔记心得(持续更新)

JavaScrip book

JavaScrip入门

JavaScrip和Java一样吗?

明白JavaScript原型链和JavaScrip继承