小编典典

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

all

鉴于以下示例,为什么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 异步 性的规范问题。随意改进这个问题并添加更多社区可以识别的简化示例。


阅读 130

收藏
2022-03-01

共1个答案

小编典典

一句话回答: 异步

前言

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


手头问题的答案

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

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

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

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

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

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

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

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

突出显示的异步代码

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

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

上述示例的代码片段:

// 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您必须使用回调模式,或者...... Promises,而不是从异步回调中获取值。

承诺

尽管有一些方法可以使用 vanilla JS 来阻止回调地狱,但 Promise
越来越受欢迎,并且目前正在 ES6 中标准化(请参阅Promise - MDN)。

Promises(又名
Futures)提供了对异步代码的更线性,因此更愉快的阅读,但解释它们的整个功能超出了这个问题的范围。相反,我会将这些优秀的资源留给感兴趣的人:


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


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

我想把这个问题变成一个规范的话题来回答与 Ajax 无关的异步问题(有How to return the response from an AJAX
call?
for
that),因此这个话题需要你的帮助才能尽可能好和有帮助!

2022-03-01