协慌网

登录 贡献 社区

在函数内部修改变量后,为什么变量未更改? - 异步代码参考

给定以下示例,为什么在所有情况下都未定义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 已至少重复了数千次此主题。因此,首先,我想指出一些非常有用的资源:


眼前问题的答案

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

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

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

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

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

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

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

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

异步代码突出显示

简而言之,回调函数是同步创建的,但异步执行。您只是不能依赖异步函数的执行,除非您知道它已执行,并且该怎么做?

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

上面示例的代码片段:

// 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从异步流回到这里被定义回调同步流量的值,作为后同步码已执行完毕,异步回调长期执行。

而不是return值,您将不得不使用回调模式,或者。。。

承诺

尽管可以通过香草 JS 来阻止回调地狱,但 Promise 越来越流行,并且目前已在 ES6 中进行了标准化(请参阅Promise-MDN )。

承诺(又称期货)提供了一种更加线性,因此令人愉悦的异步代码读取方法,但是解释其整个功能不在此问题范围之内。相反,我会将这些出色的资源留给有兴趣的人:


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


注意:我已将此答案标记为 Community Wiki,因此具有至少 100 个信誉的任何人都可以对其进行编辑和改进!请随时改善此答案,如果您愿意,也可以提交一个全新的答案。

我想将这个问题变成一个规范的主题,以回答与 Ajax 无关的异步性问题(为此,有一种如何从 AJAX 调用返回响应的方法),因此,这个主题需要您的帮助,以便使其尽可能的好和有用。 !

Fabrício 的答案就在现场。但是我想用一些不太技术性的东西来补充他的答案,该技术着重于类比,以帮助解释异步性的概念


比喻...

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

:鲍勃,您好,我需要知道上周我们如何去做酒吧。吉姆想要一份报告,而您是唯一知道此事细节的人。

鲍勃:好的,但是大约要花 30 分钟吗?

:太好了,鲍勃。知道了,给我回个电话!

此时,我挂断了电话。由于我需要 Bob 的信息来完成我的报告,因此我离开了报告,去喝咖啡,然后接了一封电子邮件。 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 )。

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

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

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

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

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

回呼

此处的更改是getMessage接受一个callback参数,该参数将被调用以将结果传递回调用代码(一旦可用)。

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

承诺

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

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 提供的功能类似于其 Deferred 的 Promise。

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

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

异步 / 等待

如果您的 JavaScript 环境包括对asyncawait支持(例如 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();