为什么我的变量在函数内部修改后保持不变?异步代码引用

2024-06-14 18:18:23 发布

您现在位置: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条回答

这里有一个更简洁的答案,供那些正在寻找快速参考的人使用,还有一些使用Promissions和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

承诺提供了一种比回调更灵活的替代方法,因为它们可以自然地结合起来协调多个异步操作。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函数中同步使用承诺:

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函数内修改。该函数显然不是立即执行的,而是作为参数赋值或传递的。这就是我们所说的回调

现在的问题是,什么时候调用回调

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

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

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

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

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

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

async code highlighted

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

其实很简单。依赖于异步函数执行的逻辑应该从此异步函数内部启动/调用。例如,在回调函数中移动alertconsole.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);
}

上述示例的代码段:

&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);
}
和#13;
和#13;

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

您还将注意到,由于异步的性质,不可能将值从异步流返回到定义回调的同步流,因为异步回调是在同步代码已经完成执行很久之后执行的

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

承诺

尽管有一些方法可以让vanilla JS远离callback hell,但承诺越来越受欢迎,目前正在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!

法布里西奥的回答恰到好处;但我想用一些不那么技术性的东西来补充他的回答,重点放在一个类比上,以帮助解释异步性的概念


一个类比…

昨天,我正在做的工作需要一位同事提供一些信息。我给他打电话;下面是对话的过程:

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将继续处理下一行代码;i、 e.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

相关问题 更多 >