为什么我的variables在函数内部修改后没有改变? – asynchronous代码引用
给出以下示例,为什么在所有情况下outerScopeVar
定义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
? 我不想要解决方法,我想知道为什么会发生这种情况。
注意:这是JavaScriptasynchronous性的一个规范问题。 随意改善这个问题,并添加更多的社区可以识别的简化的例子。
一个字的答案: asynchronous性 。
前言
这个主题已经迭代了至less几千次,在这里,堆栈溢出。 因此,首先我想指出一些非常有用的资源:
-
@Felix Kling的“如何从AJAX调用返回响应” 。 看到他很好的解释同步和asynchronousstream程,以及“重组代码”部分。
本杰明·格鲁恩鲍姆(Benjamin Gruenbaum)也在同一个主题中花费了很多精力解释asynchronous性。 -
@Matt Esch对“从fs.readFile获取数据”的回答也以一种简单的方式非常好地解释了asynchronous性。
手头的问题的答案
我们先来看看共同的行为。 在所有示例中, outerScopeVar
都是在函数内部修改的。 这个函数显然不是立即执行的,而是作为参数被分配或传递的。 这就是我们所说的callback 。
现在的问题是,这个callback函数何时被调用?
这取决于案件。 让我们试着再次追踪一些共同的行为:
-
img.onload
可能会在将来某个时候调用,当(如果)图像已经成功加载。 -
setTimeout
可能会在将来某个时间被调用,在延迟过期之后,超时还没有被clearTimeout
取消。 注意:即使使用0
作为延迟,所有浏览器都有一个最小超时延迟上限(在HTML5规范中指定为4ms)。 - jQuery
$.post
的callback可能会在将来某个时候调用,当(如果)Ajax请求成功完成的话。 - Node.js的
fs.readFile
可能会在将来某个时候被调用,当文件被成功读取或抛出错误。
在所有情况下,我们都有可能在未来某个时候运行的callback。 这个“将来的某个时间”就是我们所说的asynchronousstream程 。
asynchronous执行被推出同步stream程。 也就是说,在同步代码栈执行时,asynchronous代码永远不会执行。 这是JavaScript单线程的含义。
更具体地说,当JS引擎处于空闲状态 – 不执行(a)同步代码堆栈时,它将轮询可能触发asynchronouscallback的事件(例如,过期超时,接收到的networking响应)并且一个接一个地执行它们。 这被视为事件循环 。
也就是说,以手绘红色形状突出显示的asynchronous代码只有在其各自的代码块中的所有剩余同步代码已经执行之后才可以执行:
简而言之,callback函数是同步创build的,但是是asynchronous执行的。 直到你知道它已经执行,你不能依靠asynchronous函数的执行,以及如何做到这一点?
这很简单,真的。 依赖于asynchronous函数执行的逻辑应该从这个asynchronous函数内部被启动/调用。 例如,将alert
s和console.log
s移到callback函数内部会输出预期的结果,因为结果在那个时候是可用的。
实现你自己的callback逻辑
通常,您需要使用asynchronous函数的结果来做更多的事情,或者根据asynchronous函数的调用位置来执行不同的操作。 让我们来解决一个更复杂的例子:
var outerScopeVar; helloCatAsync(); alert(outerScopeVar); function helloCatAsync() { setTimeout(function() { outerScopeVar = 'Nya'; }, Math.random() * 2000); }
注意:我将setTimeout
作为一个通用的asynchronous函数,使用随机延迟,同样的例子适用于Ajax, readFile
, onload
和其他asynchronousstream程。
这个例子显然和其他例子有相同的问题,它没有等到asynchronous函数执行。
让我们来解决它实现我们自己的callback系统。 首先,我们摆脱了这种情况下完全无用的丑陋的outerScopeVar
。 然后我们添加一个接受函数参数的参数,我们的callback。 当asynchronous操作完成时,我们调用这个callback传递结果。 执行(请按顺序阅读评论):
// 1. Call helloCatAsync passing a callback function, // which will be called receiving the result from the async operation helloCatAsync(function(result) { // 5. Received the result from the async function, // now do whatever you want with it: alert(result); }); // 2. The "callback" parameter is a reference to the function which // was passed as argument from the helloCatAsync call function helloCatAsync(callback) { // 3. Start async operation: setTimeout(function() { // 4. Finished async operation, // call the callback passing the result as argument callback('Nya'); }, Math.random() * 2000); }
通常在实际使用情况下,DOM API和大多数库已经提供了callback函数(本示例中的helloCatAsync
实现)。 你只需要传递callback函数,并且明白它将在同步stream程之外执行,并重构你的代码以适应这一点。
您还会注意到,由于asynchronous性质,不可能将asynchronousstream的值返回到定义了callback的同步stream,因为asynchronouscallback在同步代码已经执行完之后执行很长时间。
而不是从asynchronouscallback中return
一个值,你将不得不使用callback模式,或者… Promise。
承诺
虽然有办法让香草JS保持callback地狱 ,但承诺正在日益普及,目前正在ES6中标准化(参见Promise – MDN )。
承诺(又名期货)提供了一个更线性的,因此愉快的阅读asynchronous代码,但解释他们的整个function超出了这个问题的范围。 相反,我会留下这些优秀的资源为感兴趣的:
- JavaScript承诺 – HTML5岩石
- 你失去了诺言 – domenic.me
关于JavaScriptasynchronous性的更多阅读资料
- 节点的艺术 – callback解释asynchronous代码和callback非常好与香草JS例子和Node.js代码。
注意:我已经将此答案标记为社区Wiki,因此具有至less100个声望的任何人都可以对其进行编辑和改进! 请随时改善这个答案,或提交一个全新的答案,如果你也想。
我想把这个问题变成一个规范的主题来回答与Ajax无关的asynchronous问题(这里有如何从AJAX调用返回响应 ),因此这个主题需要你的帮助尽可能地好,有帮助!
法布里西奥的答案是现货。 但是我想用一些技术性较差的东西来补充他的答案,这些东西的重点是一个类比来帮助解释asynchronous性的概念 。
一个比喻…
昨天,我所做的工作需要一个同事的一些信息。 我打电话给他。 以下是谈话的过程:
我 :嗨,鲍勃,我需要知道我们上个星期是怎么想的。 吉姆想要一个报告,而你是唯一知道周围细节的人。
鲍勃 :当然可以,但要花30分钟左右呢?
我 :鲍勃很好。 有了这些信息,给我一个回电!
此时,我挂断了电话。 由于我需要鲍勃的信息来完成我的报告,所以我离开报告,去喝咖啡,然后find了一些电子邮件。 40分钟后(鲍勃很慢),鲍勃打了个电话,给了我所需要的信息。 在这一点上,我重新开始了我的工作,因为我掌握了所有我需要的信息。
想象一下,如果谈话是这样的话,
我 :嗨,鲍勃,我需要知道我们上个星期是怎么想的。 吉姆想要一个报告,而你是唯一知道周围细节的人。
鲍勃 :当然可以,但要花30分钟左右呢?
我 :鲍勃很好。 我会等。
我坐在那里等着。 等着。 等着。 40分钟 什么也不做,只是在等待。 最后鲍勃给了我信息,挂了电话,我完成了报告。 但是我失去了40分钟的生产力。
这是asynchronous与同步的行为
这正是我们问题中所有例子中正在发生的事情。 加载一个图像,从磁盘加载一个文件,并通过AJAX请求一个页面都是缓慢的操作(在现代计算的上下文中)。
JavaScript可以让你注册一个callback函数,而这个callback函数在慢速操作完成的时候会被执行。 同时,JavaScript将继续执行其他代码。 JavaScript在等待慢速操作完成时执行其他代码的事实使得行为asynchronous 。 如果JavaScript在执行任何其他代码之前等待完成的操作,这将是同步行为。
var outerScopeVar; var img = document.createElement('img'); // Here we register the callback function. img.onload = function() { // Code within this function will be executed once the image has loaded. outerScopeVar = this.width; }; // But, while the image is loading, JavaScript continues executing, and // processes the following lines of JavaScript. img.src = 'lolcat.png'; alert(outerScopeVar);
在上面的代码中,我们要求JavaScript加载lolcat.png
,这是一个懒惰的操作。 一旦这个缓慢的操作完成,callback函数将被执行,但同时,JavaScript将继续处理下一行代码; 即alert(outerScopeVar)
。
这就是为什么我们看到警报显示undefined
。 因为alert()
是立即处理的,而不是在图像加载之后。
为了修复我们的代码,我们所要做的就是将alert(outerScopeVar)
代码移到callback函数中。 因此,我们不再需要声明为全局variables的outerScopeVar
variables。
var img = document.createElement('img'); img.onload = function() { var localScopeVar = this.width; alert(localScopeVar); }; img.src = 'lolcat.png';
你总是会看到一个被指定为一个函数的callback函数,因为这是JavaScript中定义一些代码的唯一方法,但是直到以后才能执行它。
因此,在我们所有的例子中, function() { /* Do something */ }
是callback; 为了修复所有的例子,我们所要做的就是将需要操作响应的代码移到那里!
*技术上你也可以使用eval()
,但eval()
对于这个目的是邪恶的
如何让我的呼叫者等待?
你现在可能有一些类似的代码;
function getWidthOfImage(src) { var outerScopeVar; var img = document.createElement('img'); img.onload = function() { outerScopeVar = this.width; }; img.src = src; return outerScopeVar; } var width = getWidthOfImage('lolcat.png'); alert(width);
但是,我们现在知道return outerScopeVar
立即发生; 在onload
callback函数更新variables之前。 这导致getWidthOfImage()
返回undefined
,并且undefined
被alert_d。
为了解决这个问题,我们需要允许调用getWidthOfImage()
函数来注册一个callback函数,然后将宽度的提示移动到callback函数中。
function getWidthOfImage(src, cb) { var img = document.createElement('img'); img.onload = function() { cb(this.width); }; img.src = src; } getWidthOfImage('lolcat.png', function (width) { alert(width); });
…和以前一样,请注意,我们已经能够删除全局variables(在这里是width
)。
对于那些正在寻找快速参考的人员,以及一些使用promises和async / await的例子,这里有一个更简洁的答案。
从一个调用asynchronous方法的函数(在这种情况下是setTimeout
)开始,使用朴素的方法(不起作用),并返回一条消息:
function getMessage() { var outerScopeVar; setTimeout(function() { outerScopeVar = 'Hello asynchronous world!'; }, 0); return outerScopeVar; } console.log(getMessage());
undefined
在这种情况下被logging,因为getMessage
在调用setTimeout
callback之前返回,并更新outerScopeVar
。
解决这个问题的两个主要方法是使用callback和承诺 :
callback
这里的改变是, getMessage
接受一个callback
参数,一旦可用,将会调用结果返callback用代码。
function getMessage(callback) { setTimeout(function() { callback('Hello asynchronous world!'); }, 0); } getMessage(function(message) { console.log(message); });
承诺
Promise提供了比callback更灵活的替代scheme,因为它们可以自然地组合起来协调多个asynchronous操作。 Promise / A +标准实现在node.js(0.12+)和许多当前的浏览器中本地提供,但也在像Bluebird和Q这样的库中实现。
function getMessage() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('Hello asynchronous world!'); }, 0); }); } getMessage().then(function(message) { console.log(message); });
jQuery的延期
jQuery提供的function类似于Deferreds的promise。
function getMessage() { var deferred = $.Deferred(); setTimeout(function() { deferred.resolve('Hello asynchronous world!'); }, 0); return deferred.promise(); } getMessage().done(function(message) { console.log(message); });
asynchronous/ AWAIT
如果您的JavaScript环境包含对async
和await
支持(如Node.js 7.6+),那么可以在async
函数中同步使用promise:
function getMessage () { return new Promise(function(resolve, reject) { setTimeout(function() { resolve('Hello asynchronous world!'); }, 0); }); } async function main() { let message = await getMessage(); console.log(message); } main();
为了说明这一点,杯子代表了outerScopeVar
。
asynchronous函数就像…
其他答案很好,我只是想提供一个简单的答案。 仅限于jQueryasynchronous调用
所有ajax调用(包括$.get
或$.post
或$.ajax
)是asynchronous的。
考虑你的例子
var outerScopeVar; //line 1 $.post('loldog', function(response) { //line 2 outerScopeVar = response; }); alert(outerScopeVar); //line 3
代码执行从第1行开始,在第2行声明variables,触发和asynchronous调用(即post请求),并继续执行第3行,而不等待post请求完成执行。
可以说,发布请求需要10秒才能完成, outerScopeVar
的值将只在这10秒之后设置。
尝试,
var outerScopeVar; //line 1 $.post('loldog', function(response) { //line 2, takes 10 seconds to complete outerScopeVar = response; }); alert("Lets wait for some time here! Waiting is fun"); //line 3 alert(outerScopeVar); //line 4
现在,当您执行此操作时,您将在第3行中收到警报。现在等待一段时间,直到您确定发布请求已返回某个值。 然后,当您单击确定,在警告框中,下一个警报将打印预期的值,因为您等待它。
在现实生活中,代码变成了,
var outerScopeVar; $.post('loldog', function(response) { outerScopeVar = response; alert(outerScopeVar); });
所有依赖asynchronous调用的代码都会移入asynchronous块中,或者等待asynchronous调用。
在所有这些场景中, outerScopeVar
被asynchronous修改或赋值,或者在稍后的时间(等待或侦听某个事件发生)中进行赋值,因此当前执行不会等待 。因此,所有这些情况下的当前执行stream程结果都是outerScopeVar = undefined
我们来讨论一下每个例子(我标记了asynchronous调用的部分或者延迟发生的部分):
1。
在这里,我们注册一个eventlist,它将在这个特定的事件上执行。在这里加载image.then当前的执行继续下一行img.src = 'lolcat.png';
和alert(outerScopeVar);
同时事件可能不会发生。 即functionimg.onload
等待加载的引用的图像,asynchronous。 这将会发生所有的下面的例子 – 事件可能会有所不同。
2。
这里的超时事件扮演着angular色,在指定的时间之后会调用处理程序。 这里是0
,但它仍然注册一个asynchronous事件,它将被添加到Event Queue
的最后一个位置执行,这就保证了延迟。
3。
这一次的Ajaxcallback。
4。
节点可以被认为是asynchronous编码的王者。这个被标记的函数被注册为一个callback处理程序,它将在读取指定的文件之后执行。
5。
明显的承诺(将来会做的事情)是asynchronous的。 看看延期,承诺和未来在JavaScript中有什么区别?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript