给出以下示例,为什么在所有情况下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.
这里有一个更简洁的答案,供那些正在寻找快速参考的人使用,还有一些使用Promissions和async/await的示例
从调用异步方法(在本例中为
setTimeout
)并返回消息的函数的朴素方法(不起作用)开始:在这种情况下
undefined
会被记录,因为getMessage
在调用setTimeout
回调并更新outerScopeVar
之前返回解决此问题的两种主要方法是使用回调和承诺:
回调
这里的变化是
getMessage
接受callback
参数,一旦可用,将调用该参数将结果传递回调用代码Promises
承诺提供了一种比回调更灵活的替代方法,因为它们可以自然地结合起来协调多个异步操作。node.js(0.12+)和许多当前浏览器中本机提供了Promises/A+标准实现,但也在Bluebird和Q等库中实现
jQueryDeferreds
jQuery提供了类似于承诺的功能
异步/等待
如果您的JavaScript环境包括对^{} 和^{} (如Node.js 7.6+)的支持,那么您可以在
async
函数中同步使用承诺:一个词的答案是:异步性
前言
在堆栈溢出中,这个主题已经被迭代了至少几千次。因此,首先我想指出一些非常有用的资源:
@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引擎空闲时(不执行一堆同步代码),它将轮询可能触发异步回调的事件(例如过期超时、收到的网络响应),并逐个执行。这被认为是Event Loop
也就是说,手绘红色形状中突出显示的异步代码只能在其各自代码块中的所有剩余同步代码执行后执行:
简而言之,回调函数是同步创建的,但异步执行。在知道异步函数已经执行之前,您不能依赖于它的执行,如何做到这一点
其实很简单。依赖于异步函数执行的逻辑应该从此异步函数内部启动/调用。例如,在回调函数中移动
alert
和console.log
也会输出预期的结果,因为此时结果可用实现自己的回调逻辑
通常,您需要对异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们来处理一个更复杂的示例:
注意:我使用带有随机延迟的
setTimeout
作为通用异步函数,相同的示例适用于Ajax、readFile
、onload
和任何其他异步流此示例显然与其他示例存在相同的问题,它不会等待异步函数执行
让我们通过实现我们自己的回调系统来解决这个问题。首先,我们去掉了这个丑陋的
outerScopeVar
,在这种情况下它是完全无用的。然后我们添加一个接受函数参数的参数,我们的回调函数。异步操作完成后,我们调用此回调传递结果。实施情况(请按顺序阅读评论):上述示例的代码段:
在实际用例中,domapi和大多数库都已经提供了回调功能(本示例中的
helloCatAsync
实现)。您只需要传递回调函数并了解它将在同步流之外执行,然后重新构造代码以适应这一点您还将注意到,由于异步的性质,不可能将值从异步流返回到定义回调的同步流,因为异步回调是在同步代码已经完成执行很久之后执行的
您将不得不使用回调模式,或者。。。承诺
承诺
尽管有一些方法可以让vanilla JS远离callback hell,但承诺越来越受欢迎,目前正在ES6中标准化(见Promise - MDN)
Promises(又称Futures)提供了一种更线性的异步代码阅读方式,因此也更令人愉快,但解释它们的全部功能超出了本问题的范围。相反,我将把这些优秀的资源留给感兴趣的人:
更多关于JavaScript异步性的阅读资料
法布里西奥的回答恰到好处;但我想用一些不那么技术性的东西来补充他的回答,重点放在一个类比上,以帮助解释异步性的概念
一个类比…
昨天,我正在做的工作需要一位同事提供一些信息。我给他打电话;下面是对话的过程:
这时,我挂断了电话。因为我需要鲍勃提供信息来完成我的报告,所以我离开了报告,去喝了杯咖啡,然后我又收到了一些电子邮件。40分钟后(鲍勃慢了),鲍勃回电话给我需要的信息。在这一点上,我继续我的工作与我的报告,因为我有我需要的所有信息
想象一下,如果谈话是这样进行的
我坐在那里等待。然后等待。然后等待。40分钟。除了等待什么也不做。最后,鲍勃给了我信息,我们挂断了电话,我完成了报告。但我失去了40分钟的工作效率
这是异步与同步行为的对比
这正是我们问题中所有例子所发生的情况。加载图像、从磁盘加载文件以及通过AJAX请求页面都是缓慢的操作(在现代计算环境中)
JavaScript让您注册一个回调函数,该函数将在慢操作完成时执行,而不是等待这些慢操作完成。然而,与此同时,JavaScript将继续执行其他代码。JavaScript在等待缓慢的操作完成时执行其他代码,这使得该行为变得异步。如果JavaScript在执行任何其他代码之前等待操作完成,这将是同步行为
在上面的代码中,我们要求JavaScript加载
lolcat.png
,这是一个slooow操作。回调函数将在这个缓慢的操作完成后执行,但与此同时,JavaScript将继续处理下一行代码;i、 e.alert(outerScopeVar)
这就是为什么我们看到警报显示
undefined
;因为alert()
是立即处理的,而不是在图像加载之后为了修复代码,我们所要做的就是将
alert(outerScopeVar)
代码移动到回调函数中。因此,我们不再需要将outerScopeVar
变量声明为全局变量您将始终看到回调被指定为函数,因为这是JavaScript中定义某些代码的唯一*方法,但在以后才执行它
因此,在我们所有的例子中,
function() { /* Do something */ }
是回调;要修复所有示例,我们只需将需要操作响应的代码移到其中*从技术上讲,您也可以使用} is evil 用于此目的
eval()
,但是^{如何让来电者等待?
您当前可能有一些类似的代码
然而,我们现在知道
return outerScopeVar
立即发生;在onload
回调函数更新变量之前。这会导致getWidthOfImage()
返回undefined
,并且undefined
被警告要解决这个问题,我们需要允许调用
getWidthOfImage()
的函数注册回调,然后将宽度的警报移动到该回调中。。。与前面一样,请注意,我们已经能够删除全局变量(在本例中为
width
)