开发者问题收集

为什么我在函数内部修改变量后它保持不变? - 异步代码参考

2014-05-14
304530

鉴于以下示例,为什么 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);
// with observables
var outerScopeVar;
myObservable.subscribe(function (value) {
    outerScopeVar = value;
});
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.

3个回答

一个词的答案: 异步性

前言

这个主题在 Stack Overflow 中至少被重复了几千次。因此,首先我想指出一些非常有用的资源:


手头问题的答案

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

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

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

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

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

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

更具体地说,当 JS 引擎处于空闲状态(不执行(非)同步代码堆栈)时,它会轮询可能触发异步回调的事件(例如超时、收到网络响应)并逐个执行它们。这被视为 事件循环

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

突出显示异步代码

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

这真的很简单。依赖于异步函数执行的逻辑应该从此异步函数内部启动/调用。例如,将 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 an 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 an argument
        callback('Nya');
    }, Math.random() * 2000);
}

上述示例的代码片段:

// 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 an 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 实现)。您只需传递回调函数并了解它将在同步流程之外执行,然后重构代码以适应这一点即可。

您还会注意到,由于异步特性,无法将值从异步流程 返回 到定义回调的同步流程,因为异步回调是在同步代码执行完毕很久之后执行的。

您不必从异步回调 返回 值,而是必须使用回调模式,或者......承诺。

承诺

尽管有多种方法可以通过 vanilla JS 避免 回调地狱 ,但承诺越来越受欢迎,目前正在 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 that 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!

2014-05-14

Fabrício 的回答非常准确;但我想用一些不太技术性的东西来补充他的回答,重点放在类比上,以帮助解释异步性的概念


类比...

昨天,我所做的工作需要同事提供一些信息。我给他打了电话;对话如下:

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!

此时,我挂断了电话。由于我需要 Bob 提供的信息来完成我的报告,所以我离开了报告,去喝了杯咖啡,然后我查看了一些电子邮件。40 分钟后(Bob 很慢),Bob 回电话并给了我所需的信息。此时,我恢复了报告工作,因为我已掌握了所需的所有信息。


想象一下,如果对话是这样的;

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 分钟。什么也没做,只是等待。最后,Bob 给了我信息,我们挂断了电话,我完成了报告。但我损失了 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 ,这是一个 的操作。回调函数将在此慢速操作完成后执行,但与此同时,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 )。

2014-05-29

对于那些正在寻找快速参考以及使用承诺和 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 提供了一种比回调更灵活的替代方案,因为它们可以自然地组合起来以协调多个异步操作。 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 getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

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

async/await

如果您的 JavaScript 环境包含对 async await 的支持(例如 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();
JohnnyHK
2015-01-20