为什么我的变量在函数内部修改后没有变化? – 异步代码引用

鉴于以下示例,为什么在所有情况下都未定义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 ? 我不想要解决方法,我想知道为什么会这样。


注意:这是JavaScript异步性的规范问题。 随意改进这个问题,并添加社区可以识别的更简化的示例。

一个字回答: 异步性

前言

在Stack Overflow中,此主题至少已经迭代了几千次。 因此,首先,我想指出一些非常有用的资源:

  • @Felix Kling的“如何从AJAX调用返回响应” 。 请参阅他解释同步和异步流程的优秀答案,以及“重组代码”部分。
    @Benjamin Gruenbaum也在同一个post中付出了很多努力来解释异步性。

  • @Matt Esch对“从fs.readFile获取数据”的回答也以一种简单的方式非常好地解释了异步性。


手头问题的答案

让我们首先追踪共同的行为。 在所有示例中,在outerScopeVar内修改outerScopeVar 。 该function显然不会立即执行,而是作为参数分配或传递。 这就是我们所说的回调

现在的问题是,该回调何时被称为?

这取决于具体情况。 让我们再次尝试追踪一些常见的行为:

  • img.onload可能会在将来 (以及如果)图像成功加载时的某个时间调用。
  • 在延迟过期并且clearTimeout没有取消超时之后,可能会在将来的某个时间调用setTimeout 。 注意:即使使用0作为延迟,所有浏览器都有最小超时延迟上限(在HTML5规范中指定为4ms)。
  • 当Ajax请求成功完成(以及如果)时,可以在将来的某个时间调用jQuery $.post的回调。
  • 当文件被成功读取或抛出错误时,Node.js的fs.readFile可能会在将来fs.readFile时间被调用。

在所有情况下,我们都有一个可能在将来某个时间运行的回调。 这个“未来的某个时刻”就是我们所说的异步流程

异步执行被推出同步流。 也就是说,异步代码在执行同步代码堆栈时永远不会执行。 这就是JavaScript是单线程的意思。

更具体地说,当JS引擎空闲时 – 不执行(a)同步代码的堆栈 – 它将轮询可能已触发异步回调的事件(例如,过期超时,接收到的网络响应)并一个接一个地执行它们。 这被视为事件循环 。

也就是说,手绘红色形状中突出显示的异步代码只有在执行了各自代码块中的所有剩余同步代码后才能执行:

异步代码突出显示

简而言之,回调函数是同步创建的,但是异步执行。 在知道它已执行之前,你不能依赖异步函数的执行,以及如何做到这一点?

这很简单,真的。 应该从这个异步函数内部启动/调用依赖于异步函数执行的逻辑。 例如,在回调函数内部移动alert s和console.log也会输出预期结果,因为结果在该点可用。

实现自己的回调逻辑

通常,您需要使用异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。 让我们来解决一个更复杂的例子:

 var outerScopeVar; helloCatAsync(); alert(outerScopeVar); function helloCatAsync() { setTimeout(function() { outerScopeVar = 'Nya'; }, Math.random() * 2000); } 

注意:我使用带有随机延迟的setTimeout作为通用异步函数,同样的示例适用于Ajax, readFileonload和任何其他异步流。

这个例子显然遇到与其他例子相同的问题,它不会等到异步函数执行。

让我们来解决它实现我们自己的回调系统。 首先,我们摆脱了丑陋的outerScopeVar ,在这种情况下完全没用。 然后我们添加一个接受函数参数的参数,即我们的回调。 当异步操作完成时,我们将此回调称为传递结果。 实施(请按顺序阅读评论):

 // 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和大多数库已经提供了回调function(在此演示示例中为helloCatAsync实现)。 您只需要传递回调函数并了解它将从同步流中执行,并重构您的代码以适应这一点。

您还会注意到,由于异步性质,不可能将异步流中的值返回到定义回调的同步流,因为异步回调在同步代码执行完毕后很久就会执行。

而不是从异步回调中return值,您将不得不使用回调模式,或者…… Promises。

承诺

虽然有很多方法可以保持回调地狱与香草JS,但承诺越来越受欢迎,目前正在ES6中进行标准化(请参阅Promise – MDN )。

Promises(aka Futures)提供了一个更线性,更令人愉快的异步代码读取,但解释它们的整个function超出了这个问题的范围。 相反,我将为感兴趣的人留下这些优秀的资源:

  • JavaScript承诺 – HTML5 Rocks
  • 你错过了承诺点 – domenic.me

关于JavaScript异步性的更多阅读材料

  • 节点的艺术 – 回调也很好地解释了异步代码和回调,以及vanilla JS示例和Node.js代码。

注意:我已将此答案标记为社区Wiki,因此任何拥有至少100个声誉的人都可以编辑和改进它! 如果您愿意,请随时改进这个答案,或提交一个全新的答案。

我想把这个问题变成一个规范的主题来回答与Ajax无关的异步问题( 如何从AJAX调用返回响应? )因此这个主题需要你的帮助尽可能好和有用!

法布里西奥的回答是现场的; 但是我想用不那么技术性的东西来补充他的答案,这些东西专注于一个类比来帮助解释异步性的概念


比喻……

昨天,我所做的工作需要一位同事的一些信息。 我打电话给他; 这是对话的方式:

:嗨鲍勃,我需要知道我们上周如何去酒吧 。 吉姆想要一份关于它的报告,而你是唯一知道它的细节的人。

鲍勃 :当然可以,但是我需要大约30分钟?

:那是伟大的鲍勃。 当你得到这些信息时,给我一个回铃!

此时,我挂了电话。 由于我需要鲍勃提供的信息来完成我的报告,我离开了报告并转而喝咖啡,然后我发现了一些电子邮件。 40分钟后(鲍勃很慢),鲍勃打电话给我,告诉我我需要的信息。 此时,我继续使用我的报告工作,因为我掌握了所需的所有信息。


想象一下,如果谈话已经变成这样了;

:嗨鲍勃,我需要知道我们上周如何去酒吧 。 吉姆想要一份关于它的报告,而你是唯一知道它的细节的人。

鲍勃 :当然可以,但是我需要大约30分钟?

:那是伟大的鲍勃。 我会等。

我坐在那里等待。 等了。 等了。 40分钟。 什么都不做,等待。 最后,鲍勃给了我信息,我们挂了电话,然后我完成了我的报告。 但是我失去了40分钟的生产力。


这是异步行为与同步行为

这正是我们问题中所有例子中正在发生的事情。 加载图像,从磁盘加载文件,以及通过AJAX请求页面都是缓慢的操作(在现代计算的环境中)。

JavaScript不允许等待这些慢速操作完成,而是让您注册一个回调函数,该函数将在慢速操作完成时执行。 然而,与此同时,JavaScript将继续执行其他代码。 JavaScript在等待慢速操作完成时执行其他代码的事实使得行为异步 。 如果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 ,这是一个sloooow操作。 一旦这个慢速操作完成,就会执行回调函数,但与此同时,JavaScript将继续处理下一行代码; 即alert(outerScopeVar)

这就是为什么我们看到警报显示undefined ; 因为alert()是立即处理的,而不是在加载图像之后。

为了修复我们的代码,我们所要做的就是将alert(outerScopeVar)代码移动回调函数中。 因此,我们不再需要将outerScopeVar变量声明为全局变量。

 var img = document.createElement('img'); img.onload = function() { var localScopeVar = this.width; alert(localScopeVar); }; img.src = 'lolcat.png'; 

您将始终看到将回调指定为函数,因为这是JavaScript中定义某些代码的唯一方法,但直到稍后才执行。

因此,在我们的所有示例中, function() { /* Do something */ }是回调; 要修复所有示例,我们所要做的就是将需要操作响应的代码移动到那里!

*从技术上讲,你也可以使用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回调函数更新变量之前。 这导致getWidthOfImage()返回undefined ,并且undefined被警告。

要解决这个问题,我们需要允许函数调用getWidthOfImage()来注册回调,然后将宽度的警报移动到该回调中;

 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); }); 

……和以前一样,请注意我们已经能够删除全局变量(在这种情况下是width )。

对于正在寻找快速参考的人以及使用promises和async / await的一些示例,这里有一个更简洁的答案。

对于调用异步方法(在本例中为setTimeout )并返回消息的函数,从朴素的方法(不起作用)开始:

 function getMessage() { var outerScopeVar; setTimeout(function() { outerScopeVar = 'Hello asynchronous world!'; }, 0); return outerScopeVar; } console.log(getMessage()); 

在这种情况下会记录undefined ,因为getMessage在调用setTimeout回调之前返回并更新outerScopeVar

解决它的两种主要方法是使用回调承诺

回调

这里的更改是getMessage接受一个callback参数,该参数将被调用以在结果可用时将结果传回给调用代码。

 function getMessage(callback) { setTimeout(function() { callback('Hello asynchronous world!'); }, 0); } getMessage(function(message) { console.log(message); }); 

承诺

Promise提供了一种比回调更灵活的替代方案,因为它们可以自然地组合起来协调多个异步操作。 Promises / 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 Deferreds

jQuery提供的function类似于Deferreds的promises。

 function getMessage() { var deferred = $.Deferred(); setTimeout(function() { deferred.resolve('Hello asynchronous world!'); }, 0); return deferred.promise(); } getMessage().done(function(message) { console.log(message); }); 

异步/ AWAIT

如果您的JavaScript环境包含对asyncawait支持(如Node.js 7.6+),那么您可以在async函数中同步使用promises:

 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 ,杯子代表outerScopeVar

异步函数就像……

异步呼叫咖啡

其他答案非常好,我只想提供一个直截了当的答案。 仅限于jQuery异步调用

所有ajax调用(包括$.get$.post$.ajax )都是异步的。

考虑你的例子

 var outerScopeVar; //line 1 $.post('loldog', function(response) { //line 2 outerScopeVar = response; }); alert(outerScopeVar); //line 3 

代码执行从第1行开始,在第2行声明变量和触发器以及异步调用(即post请求),并从第3行继续执行,而不等待post请求完成其执行。

假设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); }); 

依赖于异步调用的所有代码都在异步块内移动,或者等待异步调用。

在所有这些场景中, outerScopeVar异步修改或分配一个值或稍后发生(等待或侦听某些事件发生),当前执行不会等待 。所有这些情况下当前执行流程导致outerScopeVar = undefined

让我们讨论每个例子(我标记了为某些事件发生的异步或延迟调用的部分):

1。

在此处输入图像描述

这里我们注册一个eventlistner,它将在特定事件上执行。加载图像。然后当前执行连续下一行img.src = 'lolcat.png';alert(outerScopeVar); 同时事件可能不会发生。 ie,funtion img.onload等待引用的图像加载,异步。 这将发生在以下所有示例中 – 事件可能不同。

2。

2

这里,timeout事件扮演角色,它将在指定时间后调用处理程序。 这里它是0 ,但它仍然注册一个异步事件,它将被添加到Event Queue的最后一个位置以便执行,这样可以保证延迟。

3。

在此处输入图像描述 这次是ajax回调。

4。

在此处输入图像描述

节点可以被视为异步编码之王。这里标记的函数被注册为一个回调处理程序,它将在读取指定文件后执行。

5。

在此处输入图像描述

明显的承诺(将来会做的事情)是异步的。 看看JavaScript中Deferred,Promise和Future之间有什么区别?

https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript