给出以下示例,为什么在所有情况下都未定义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);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
为什么在所有这些示例中都输出undefined
?我不想要解决办法,我想知道为什么会发生这种事。
Note: This is a canonical question for JavaScript asynchronicity. Feel free to improve this question and add more simplified examples which the community can identify with.
这里有一个更简洁的答案,供人们快速参考,以及一些使用promises和async/await的例子。
从调用异步方法(在本例中为
setTimeout
)并返回消息的函数的朴素方法(这不起作用)开始:在本例中,
undefined
被记录,因为getMessage
在调用setTimeout
回调并更新outerScopeVar
之前返回。解决这一问题的两种主要方法是使用“回调”和“承诺”:
回调
这里的变化是
getMessage
接受一个callback
参数,该参数将被调用以在调用代码可用时将结果返回给调用代码。Promises
Promises提供了一种比回调更灵活的替代方法,因为它们可以自然地组合起来协调多个异步操作。node.js(0.12+)和许多当前浏览器本机提供了Promises/A+标准实现,但也在Bluebird和Q等库中实现。
jQueryDeferreds
jQuery提供的功能类似于带有延迟的承诺。
异步/等待
如果JavaScript环境支持^{} 和^{} (如Node.js 7.6+),那么可以在
async
函数中同步使用promises:法布里西奥的答案是正确的;但我想用一些不太专业的东西来补充他的答案,其中着重于一个类比,以帮助解释异步性的概念。
一个类比……
昨天,我正在做的工作需要一位同事提供一些信息。我给他打了电话;谈话是这样进行的:
这时,我挂了电话。因为我需要鲍勃的信息来完成我的报告,所以我离开了报告,去喝了杯咖啡,然后我收到了一些电子邮件。40分钟后(鲍勃很慢),鲍勃回电话给我所需要的信息。在这一点上,我继续我的工作与我的报告,因为我有所有我需要的信息。
想象一下,如果谈话是这样进行的
我坐在那里等着。等待着。等待着。40分钟。除了等待什么也不做。最后,鲍勃给了我信息,我们挂了电话,我完成了报告。但我失去了40分钟的工作效率。
这是异步与同步行为
这正是我们问题中所有例子所发生的事情。加载图像、从磁盘加载文件以及通过AJAX请求页面都是缓慢的操作(在现代计算环境中)。
JavaScript让您注册一个回调函数,当慢速操作完成时,将执行回调函数,而不是等待这些慢速操作完成。但与此同时,JavaScript将继续执行其他代码。JavaScript在等待慢速操作完成时执行其他代码,这使得行为变得异步。如果JavaScript在执行任何其他代码之前等待操作完成,这将是同步行为。
在上面的代码中,我们要求JavaScript加载
lolcat.png
,这是一个slooow操作。一旦这个缓慢的操作完成,回调函数将被执行,但与此同时,JavaScript将继续处理下一行代码,即alert(outerScopeVar)
。这就是为什么我们看到显示
undefined
;的警报,因为alert()
是立即处理的,而不是在加载图像之后。为了修复代码,我们所要做的就是将
alert(outerScopeVar)
代码移到回调函数中。因此,我们不再需要声明为全局变量的outerScopeVar
变量。您将始终看到回调被指定为函数,因为这是JavaScript中定义某些代码的唯一*方法,但在以后才执行它。
因此,在我们的所有示例中,
function() { /* Do something */ }
是回调;要修复所有示例,我们所要做的就是将需要操作响应的代码移到其中!*从技术上讲,您也可以使用} is evil 用于此目的
eval()
,但是^{我怎么让我的来电者等着?
你现在可能有一些类似的代码
但是,我们现在知道
return outerScopeVar
是在onload
回调函数更新变量之前立即发生的。这将导致getWidthOfImage()
返回undefined
,并提醒undefined
。要解决这个问题,我们需要允许调用
getWidthOfImage()
的函数注册回调,然后将宽度的警报移动到该回调内。。。如前所述,注意我们已经能够删除全局变量(在本例中是
width
)。一个字的答案:异步性。
前言
这个主题已经被迭代了至少几千次,这里是堆栈溢出。因此,首先我想指出一些非常有用的资源:
@Felix Kling's answer to "How do I return the response from an asynchronous call?"。请参阅他解释同步和异步流的出色答案,以及“重构代码”部分。
@Benjamin Gruenbaum也在同一个线程中花了很多精力来解释异步性。
@Matt Esch's answer to "Get data from fs.readFile"还以一种简单的方式非常好地解释了异步性。
对手头问题的回答
让我们先追踪一下常见的行为。在所有示例中,
outerScopeVar
都是在函数内部修改的。该函数显然不是立即执行的,而是作为参数分配或传递的。这就是我们所说的回调。现在的问题是,何时调用回调?
这要看情况而定。让我们再次尝试跟踪一些常见的行为:
img.onload
可以在将来的某个时候称为,当(并且如果)成功加载图像时。setTimeout
在延迟过期且超时未被clearTimeout
取消后,可以在将来的某个时间称为。注意:即使使用0
作为延迟,所有浏览器都有最小的超时延迟上限(在HTML5规范中指定为4ms)。$.post
的回调可以在将来的某个时候调用,当(并且如果)Ajax请求成功完成时。fs.readFile
可能在将来的某个时候被称为,当文件被成功读取或抛出错误时。在所有情况下,我们都有一个回调,它可能在将来的某个时候运行。“将来某个时候”就是我们所说的异步流。
异步执行被推出同步流。也就是说,当同步代码堆栈正在执行时,异步代码将不会执行。这就是JavaScript单线程的含义。
更具体地说,当JS引擎空闲时(不执行(a)个同步代码堆栈),它将轮询可能触发异步回调的事件(例如,过期超时、接收到的网络响应),并逐个执行这些事件。这被视为Event Loop。
也就是说,手绘红色图形中突出显示的异步代码只能在其各自代码块中的所有剩余同步代码执行后执行:
简而言之,回调函数是同步创建但异步执行的。在知道异步函数已经执行之前,不能依赖它的执行,如何做到这一点?
很简单,真的。依赖于异步函数执行的逻辑应该从这个异步函数内部启动/调用。例如,在回调函数中移动
alert
s和console.log
s也会输出预期的结果,因为此时结果是可用的。实现自己的回调逻辑
通常,您需要对异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们来处理一个更复杂的例子:
注意:我使用带有随机延迟的
setTimeout
作为通用异步函数,相同的示例适用于Ajax、readFile
、onload
和任何其他异步流。这个例子显然与其他例子有着相同的问题,它不是等待异步函数执行。
我们来实现自己的回调系统。首先,我们去掉了在这种情况下完全无用的丑陋的
outerScopeVar
。然后我们添加一个接受函数参数的参数,我们的回调函数。当异步操作完成时,我们调用这个回调来传递结果。实施(请按顺序阅读意见):上述示例的代码段:
大多数情况下,在实际的用例中,DOM API和大多数库已经提供了回调功能(在这个演示示例中是
helloCatAsync
实现)。您只需要传递回调函数并理解它将在同步流之外执行,然后重新构造代码以适应这一点。您还将注意到,由于异步特性,不可能将异步流中的值
return
返回到定义回调的同步流,因为异步回调在同步代码完成执行后很长一段时间才执行。您将不得不使用回调模式,或者。。。承诺。
承诺
尽管有很多方法可以让vanilla JS的callback hell不受影响,但promises正在变得越来越流行,目前正在ES6中进行标准化(参见Promise - MDN)。
Promises(也称为Futures)提供了对异步代码更线性、更令人愉快的阅读,但是解释它们的全部功能超出了这个问题的范围。相反,我将把这些优秀的资源留给感兴趣的人:
更多关于JavaScript异步性的阅读资料
相关问题 更多 >
编程相关推荐