鉴于以下示例,为什么 outerScopeVar
在所有情况下都未定义?
var outerScopeVar; var img = document.createElement('img'); img.onload = function() { outerScopeVar = this.width; }; img.src = 'lolcat.png'; alert(outerScopeVar);
var outerScopeVar; setTimeout(function() { outerScopeVar = 'Hello Asynchronous World!'; }, 0); alert(outerScopeVar);
// Example using some jQuery var outerScopeVar; $.post('loldog', function(response) { outerScopeVar = response; }); alert(outerScopeVar);
// Node.js example var outerScopeVar; fs.readFile('./catdog.html', function(err, data) { outerScopeVar = data; }); console.log(outerScopeVar);
// with promises var outerScopeVar; myPromise.then(function (response) { outerScopeVar = response; }); console.log(outerScopeVar);
// with observables var outerScopeVar; myObservable.subscribe(function (value) { outerScopeVar = value; }); console.log(outerScopeVar);
// geolocation API var outerScopeVar; navigator.geolocation.getCurrentPosition(function (pos) { outerScopeVar = pos; }); console.log(outerScopeVar);
为什么在所有这些示例中都输出 undefined
?我不需要解决方法,我想知道为什么发生这种情况。
注意:这是一个关于 JavaScript 异步性的规范问题。请随意改进这个问题并添加社区可以认同的更多简化示例。
Fabrício 的回答非常正确;但我想用一些不太技术性的内容来补充他的答案,重点是通过类比来帮助解释异步性的概念。
类比...
昨天,我正在做的工作需要从同事那里得到一些信息。我给他打了电话;谈话是这样进行的:
说到这里,我挂断了电话。由于我需要鲍勃提供的信息来完成我的报告,所以我留下了报告,去喝了杯咖啡,然后我看了一些电子邮件。 40 分钟后(鲍勃很慢),鲍勃回电并给了我我需要的信息。此时,我继续处理我的报告,因为我已经获得了所需的所有信息。
想象一下如果对话是这样进行的;
我坐在那里等待。并等待着。并等待着。 40分钟。除了等待什么也不做。最终,鲍勃给了我信息,我们挂断了电话,我完成了我的报告。但我损失了 40 分钟的工作效率。
这是异步与同步行为
这正是我们问题中所有示例中发生的情况。加载图像、从磁盘加载文件以及通过 AJAX 请求页面都是缓慢的操作(在现代计算的背景下)。
JavaScript 允许您注册一个回调函数,该函数将在慢速操作完成时执行,而不是等待这些慢速操作完成。但与此同时,JavaScript 将继续执行其他代码。事实上,JavaScript 在等待缓慢操作完成的同时执行其他代码,这使得该行为异步。如果 JavaScript 在执行任何其他代码之前等待操作完成,这将是同步行为。
在上面的代码中,我们要求 JavaScript 加载
lolcat.png
,这是一个sloooow 操作。一旦这个缓慢的操作完成,回调函数就会被执行,但与此同时,JavaScript 将继续处理下一行代码;即alert(outerScopeVar)
。这就是为什么我们看到警报显示
未定义
;因为alert()
是立即处理的,而不是在图像加载之后。为了修复我们的代码,我们所要做的就是将
alert(outerScopeVar)
代码移到回调函数中。因此,我们不再需要将outerScopeVar
变量声明为全局变量。您将总是看到回调被指定为函数,因为这是 JavaScript 中定义某些代码但稍后才执行它的唯一*方法。
因此,在我们所有的示例中,
function() { /* Do Something */ }
是回调;要修复所有示例,我们所要做的就是将需要操作响应的代码移到那里!* 从技术上讲,您也可以使用
eval()
,但是eval()
为此目的是邪恶的如何让来电者等待?
您当前可能有一些与此类似的代码;
但是,我们现在知道
返回outerScopeVar
会立即发生;在onload
回调函数更新变量之前。这会导致getWidthOfImage()
返回undefined
,并发出警报undefined
。要解决此问题,我们需要允许调用
getWidthOfImage()
的函数注册回调,然后将宽度警报移至该回调内;...和以前一样,请注意,我们已经能够删除全局变量(在本例中为
width
)。一个词回答:异步性。
前言
这个主题在 Stack Overflow 中已经被重复了至少几千次。因此,首先我想指出一些非常有用的资源:
@Felix Kling 对“如何从异步调用返回响应?”的回答。请参阅他解释同步和异步流程的出色答案,以及“重组代码”部分。
@Benjamin Gruenbaum 也投入了大量精力来解释同一线程中的异步性。
@Matt Esch 对“从 fs.readFile 获取数据”的回答也很好地解释了异步性简单的方式。
当前问题的答案
让我们首先追踪常见的行为。在所有示例中,
outerScopeVar
均在函数内部进行修改。该函数显然不会立即执行;它被分配或作为参数传递。这就是我们所说的回调。现在的问题是,什么时候调用该回调?
这要看具体情况。让我们尝试再次追踪一些常见行为:
img.onload
可能会在将来的某个时候调用(如果)图像已成功加载。setTimeout
可能会在延迟到期且超时尚未被clearTimeout
取消后在将来的某个时间被调用。注意:即使使用0
作为延迟,所有浏览器都有最小超时延迟上限(HTML5 规范中指定为 4 毫秒)。$.post
的回调。fs.readFile
可能会在将来的某个时候被调用。在所有情况下,我们都有一个可能在将来某个时候运行的回调。这个“将来的某个时候”就是我们所说的异步流。
异步执行被推出同步流程。也就是说,当同步代码堆栈正在执行时,异步代码将永远执行。这就是JavaScript单线程的意义。
更具体地说,当 JS 引擎空闲时——不执行一堆(a)同步代码——它将轮询可能触发异步回调的事件(例如超时、收到网络响应)并执行它们又一个。这被视为事件循环。
也就是说,手绘红色形状中突出显示的异步代码只能在其各自代码块中的所有剩余同步代码执行完毕后才执行:
简而言之,回调函数是同步创建但异步执行的。在知道异步函数已执行之前,您不能依赖它的执行,如何做到这一点?
这真的很简单。依赖于异步函数执行的逻辑应该从该异步函数内部启动/调用。例如,将
alert
和console.log
移动到回调函数内将输出预期结果,因为结果在此时可用。实现您自己的回调逻辑
通常,您需要对异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们来处理一个更复杂的例子:
注意:我使用具有随机延迟的
setTimeout
作为通用异步函数;相同的示例适用于 Ajax、readFile、onload 和任何其他异步流。这个示例显然与其他示例存在相同的问题;它不会等到异步函数执行。
让我们通过实现我们自己的回调系统来解决这个问题。首先,我们摆脱了那个丑陋的
outerScopeVar
,它在这种情况下完全没用。然后我们添加一个接受函数参数的参数,即我们的回调。当异步操作完成时,我们调用此回调并传递结果。实现(请按顺序阅读注释):上述示例的代码片段: