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

2024-04-18 03:32:21 发布

您现在位置:Python中文网/ 问答频道 /正文

给出以下示例,为什么在所有情况下都未定义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.


Tags: poslog示例imgdataresponsevarwith
3条回答

这里有一个更简洁的答案,供人们快速参考,以及一些使用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);
});

Promises

Promises提供了一种比回调更灵活的替代方法,因为它们可以自然地组合起来协调多个异步操作。node.js(0.12+)和许多当前浏览器本机提供了Promises/A+标准实现,但也在BluebirdQ等库中实现。

function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello asynchronous world!');
    }, 0);
  });
}

getMessage().then(function(message) {
  console.log(message);  
});

jQueryDeferreds

jQuery提供的功能类似于带有延迟的承诺。

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

getMessage().done(function(message) {
  console.log(message);  
});

异步/等待

如果JavaScript环境支持^{}^{}(如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();

法布里西奥的答案是正确的;但我想用一些不太专业的东西来补充他的答案,其中着重于一个类比,以帮助解释异步性的概念。


一个类比……

昨天,我正在做的工作需要一位同事提供一些信息。我给他打了电话;谈话是这样进行的:

Me: Hi Bob, I need to know how we foo'd the bar'd last week. Jim wants a report on it, and you're the only one who knows the details about it.

Bob: Sure thing, but it'll take me around 30 minutes?

Me: That's great Bob. Give me a ring back when you've got the information!

这时,我挂了电话。因为我需要鲍勃的信息来完成我的报告,所以我离开了报告,去喝了杯咖啡,然后我收到了一些电子邮件。40分钟后(鲍勃很慢),鲍勃回电话给我所需要的信息。在这一点上,我继续我的工作与我的报告,因为我有所有我需要的信息。


想象一下,如果谈话是这样进行的

Me: Hi Bob, I need to know how we foo'd the bar'd last week. Jim want's a report on it, and you're the only one who knows the details about it.

Bob: Sure thing, but it'll take me around 30 minutes?

Me: That's great Bob. I'll wait.

我坐在那里等着。等待着。等待着。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,这是一个slooow操作。一旦这个缓慢的操作完成,回调函数将被执行,但与此同时,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(),但是^{} is evil用于此目的


我怎么让我的来电者等着?

你现在可能有一些类似的代码

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)。

一个字的答案:异步性

前言

这个主题已经被迭代了至少几千次,这里是堆栈溢出。因此,首先我想指出一些非常有用的资源:


对手头问题的回答

让我们先追踪一下常见的行为。在所有示例中,outerScopeVar都是在函数内部修改的。该函数显然不是立即执行的,而是作为参数分配或传递的。这就是我们所说的回调。

现在的问题是,何时调用回调?

这要看情况而定。让我们再次尝试跟踪一些常见的行为:

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

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

异步执行被推出同步流。也就是说,当同步代码堆栈正在执行时,异步代码将不会执行。这就是JavaScript单线程的含义。

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

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

async code highlighted

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

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

实现自己的回调逻辑

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

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

上述示例的代码段:

&13;
&13;
// 1. Call helloCatAsync passing a callback function,
//    which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
    // 5. Received the result from the async function,
    //    now do whatever you want with it:
    console.log("5. result is: ", result);
});

// 2. The "callback" parameter is a reference to the function which
//    was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
    console.log("2. callback here is the function passed as argument above...")
    // 3. Start async operation:
    setTimeout(function() {
    console.log("3. start async operation...")
    console.log("4. finished async operation, calling the callback, passing the result...")
        // 4. Finished async operation,
        //    call the callback passing the result as argument
        callback('Nya');
    }, Math.random() * 2000);
}

大多数情况下,在实际的用例中,DOM API和大多数库已经提供了回调功能(在这个演示示例中是helloCatAsync实现)。您只需要传递回调函数并理解它将在同步流之外执行,然后重新构造代码以适应这一点。

您还将注意到,由于异步特性,不可能将异步流中的值return返回到定义回调的同步流,因为异步回调在同步代码完成执行后很长一段时间才执行。

您将不得不使用回调模式,或者。。。承诺。

承诺

尽管有很多方法可以让vanilla JS的callback hell不受影响,但promises正在变得越来越流行,目前正在ES6中进行标准化(参见Promise - MDN)。

Promises(也称为Futures)提供了对异步代码更线性、更令人愉快的阅读,但是解释它们的全部功能超出了这个问题的范围。相反,我将把这些优秀的资源留给感兴趣的人:


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


Note: I've marked this answer as Community Wiki, hence anyone with at least 100 reputations can edit and improve it! Please feel free to improve this answer, or submit a completely new answer if you'd like as well.

I want to turn this question into a canonical topic to answer asynchronicity issues which are unrelated to Ajax (there is How to return the response from an AJAX call? for that), hence this topic needs your help to be as good and helpful as possible!

相关问题 更多 >