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

2024-11-02 21:00:00
admin
原创
36
摘要:问题描述:给出以下例子,为什么outerScopeVar在所有情况下都是未定义的?var outerScopeVar; var img = document.createElement('img'); img.onload = function() { outer...

问题描述:

给出以下例子,为什么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?我不想要解决方法,我想知道为什么会发生这种情况。


注意:这是关于JavaScript 异步性的典型问题。欢迎改进此问题并添加更多社区可以识别的简化示例。


解决方案 1:

一个词的答案:异步性

前言

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

  • @Felix Kling 对“如何返回异步调用的响应?”的回答。请参阅他解释同步和异步流程的出色回答,以及“重构代码”部分。

@Benjamin Gruenbaum 也在同一线程中花了很多精力来解释异步性。

  • @Matt Esch 对“从 fs.readFile 获取数据”的回答也以简单的方式非常好地解释了异步性。


当前问题的答案

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

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

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

  • img.onload当图像成功加载时(如果成功加载),可能会在将来的某个时间被调用。

  • setTimeout可能会在延迟到期后某个时间被调用,并且超时尚未被取消clearTimeout。注意:即使使用0延迟,所有浏览器都有最小超时延迟上限(在 HTML5 规范中指定为 4 毫秒)。

  • 当 Ajax 请求成功完成时(并且如果成功完成),jQuery$.post的回调可能会在将来的某个时间被调用。

  • 当文件读取成功或者抛出错误时,Node.js可能会在将来的某个时间fs.readFile被调用。

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

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

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

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

异步代码突出显示

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

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

运行代码片段Hide results展开片段

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

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

您不必从return异步回调中获取值,而必须使用回调模式,或者......承诺。

承诺

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

Promises(又名 Futures)提供了更线性、更令人愉悦的异步代码阅读体验,但解释其全部功能超出了本文的范围。相反,我将为感兴趣的人留下这些优秀的资源:

  • JavaScript 承诺 - HTML5 Rocks

  • 你错过了承诺的意义 - domenic.me


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

  • Node 的艺术 - 回调通过原始 JS 示例和 Node.js 代码很好地解释了异步代码和回调。


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

我想将这个问题变成一个规范主题来回答与 Ajax 无关的异步问题(有关如何从 AJAX 调用返回响应?),因此这个主题需要您的帮助才能尽可能好和有用!

解决方案 2:

Fabrício 的回答是正确的;但我想用一些不太技术性的东西来补充他的答案,重点是通过类比来帮助解释异步的概念


打个比方……

昨天,我工作上需要一位同事提供一些信息。我给他打了电话;以下是通话内容:

:嗨,鲍勃,我想知道我们上周在酒吧做的怎么样。吉姆想要一份报告,而你是唯一一个知道细节的人。

鲍勃:当然可以,但是要花大约 30 分钟的时间吧?

:太好了,鲍勃。等你了解情况后再给我打电话吧!

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


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

:嗨,鲍勃,我想知道我们上周在酒吧做的怎么样。吉姆想要一份报告,而你是唯一一个知道细节的人。

鲍勃:当然可以,但是要花大约 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,这是一个很慢的操作。回调函数将在此缓慢的操作完成后执行,但与此同时,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)。

解决方案 3:

对于那些寻求快速参考以及使用承诺和异步/等待的一些示例的人来说,这是一个更简洁的答案。

从简单的方法(不起作用)开始,该方法调用异步方法(在本例中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+标准实现,但也在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延迟

jQuery 通过其 Deferreds 提供了与承诺类似的功能。

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

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

异步/等待

如果你的 JavaScript 环境包含对async和的支持(例如 Node.js 7.6+),那么你可以在函数await内同步使用承诺: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();

解决方案 4:

显而易见,杯子代表着outerScopeVar

异步函数就像......

异步调用咖啡

解决方案 5:

其他答案都很棒,我只想提供一个直接的答案。仅限于 jQuery 异步调用

所有 ajax 调用(包括$.get$.post$.ajax)都是异步的。

考虑到你的例子

var outerScopeVar;  //line 1
$.post('loldog', function(response) {  //line 2
    outerScopeVar = response;
});
alert(outerScopeVar);  //line 3

代码从第 1 行开始执行,在第 2 行声明变量并触发异步调用(即 post 请求),并从第 3 行继续执行,而无需等待 post 请求执行完成。

假设 post 请求需要 10 秒才能完成,那么 的值outerScopeVar只会在 10 秒后设置。

尝试一下,

var outerScopeVar; //line 1
$.post('loldog', function(response) {  //line 2, takes 10 seconds to complete
    outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun");  //line 3
alert(outerScopeVar);  //line 4

现在,当您执行此操作时,您会在第 3 行收到警报。现在等待一段时间,直到您确定发布请求已返回某个值。然后,当您在警报框上单击“确定”时,下一个警报将打印预期值,因为您等待了它。

在现实生活中,代码变成,

var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
    alert(outerScopeVar);
});

所有依赖于异步调用的代码都被移到异步块内,或者等待异步调用。

解决方案 6:

在所有这些场景中,都会异步outerScopeVar修改或分配一个值,或者在稍后的时间发生(等待或监听某个事件的发生),当前执行不会等待。所以在所有这些情况下,当前执行流都会导致outerScopeVar = undefined

让我们讨论每个例子(我标记了异步调用或延迟发生某些事件的部分):

1.

在此处输入图片描述

这里我们注册了一个事件列表,该列表将在特定事件发生时执行。这里加载图像。然后当前执行继续下一行img.src = 'lolcat.png';alert(outerScopeVar);同时事件可能不会发生。即,函数img.onload等待引用的图像异步加载。这将发生在以下所有示例中 - 事件可能有所不同。

2.

2

此处超时事件起着作用,它将在指定时间后调用处理程序。它在这里0,但它仍然注册了一个异步事件,它将被添加到Event Queue执行的最后位置,从而保证延迟。

3.

在此处输入图片描述
这次是ajax回调。

4.

在此处输入图片描述

Node可以看作是异步编码之王。这里标记的函数被注册为回调处理程序,它将在读取指定的文件后执行。

5.

在此处输入图片描述

明显的承诺(将来会完成某事)是异步的。请参阅JavaScript 中的 Deferred、Promise 和 Future 之间有什么区别?

https://www.quora.com/Whats-the-difference- Between-a-promise-and-a-callback-in-Javascript

解决方案 7:

简短的回答是:异步。

为什么需要异步?

JavaScript 是单线程的,这意味着脚本的两个部分不能同时运行;它们必须一个接一个地运行。在浏览器中,JavaScript 与大量其他内容共享一个线程,这些内容因浏览器而异。但通常 JavaScript 与绘制、更新样式和处理用户操作(例如突出显示文本和与表单控件交互)位于同一队列中。其中一项活动会延迟其他活动。

您可能已经使用事件和回调来解决这个问题。以下是事件:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // image loaded
  console.log("Loaded");
});

img1.addEventListener('error', function() {
  // error caught
  console.log("Error printed");
});
<img class="img-1" src="#" alt="img">

运行代码片段Hide results展开片段

这根本不是打喷嚏。我们获取图像,添加几个侦听器,然后 JavaScript 可以停止执行,直到其中一个侦听器被调用。

不幸的是,在上面的例子中,事件可能在我们开始监听它们之前就发生了,所以我们需要使用图像的“complete”属性来解决这个问题:

var img1 = document.querySelector('.img-1');

function loaded() {
  // image loaded
  console.log("Loaded");
}

if (img1.complete) {
  loaded();
} else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // error caught
  console.log("Error printed");
});
<img class="img-1" src="#" alt="img">

运行代码片段Hide results展开片段

这不会在我们有机会监听之前捕获出错的图像;不幸的是,DOM 没有提供给我们这样做的方法。此外,这只是加载一张图片。如果我们想知道一组图像何时加载,事情会变得更加复杂。

活动并不总是最好的方式

事件非常适合在同一个对象上发生多次的事情keyup-touchstart等等。对于这些事件,您并不真正关心在附加侦听器之前发生了什么。

正确执行此操作的两种主要方法是回调和承诺。

回调

回调函数是传递到其他函数的参数中的函数,此过程在 JavaScript 中有效,因为函数是对象,而对象可以作为参数传递给函数。回调函数的基本结构如下所示:

function getMessage(callback) {
  callback();
}

function showMessage() {
  console.log("Hello world! I am a callback");
}
getMessage(showMessage);

运行代码片段Hide results展开片段

承诺

尽管有一些方法可以通过 vanilla JS 避免回调地狱,但 Promise 越来越受欢迎,并且目前正在 ES6 中进行标准化(参见 Promise)。

Promise是一个占位符代表异步操作的最终结果(值)

  • 承诺占位符将被结果值(如果成功)或失败原因(如果不成功)替换

如果您不需要知道某件事何时发生,而只需要知道它是否发生,那么承诺就是您所寻找的。

承诺有点像事件监听器,不同之处在于:

  • Promise 只能成功或失败一次

  • 承诺不能从失败转变为成功,反之亦然

  • 一旦你有了结果,承诺就是不可改变的

  • 如果 Promise 成功或失败,并且你稍后添加了成功/失败回调,则会调用正确的回调

  • 事件是否发生在你添加回调之前并不重要

注意:始终从 Promise 内的函数返回结果,否则后续函数就无法执行。

Promise 术语

承诺可以是:

  • 已实现:与承诺相关的操作已成功完成

    • 异步操作已完成

    • 承诺有价值

    • 承诺不会再改变

  • 拒绝:与承诺相关的操作失败

    • 异步操作失败

    • 承诺永远不会实现

    • 承诺有一个原因,表明操作失败的原因

    • 承诺不会再改变

  • 待处理:尚未完成或拒绝

    • 异步操作尚未完成

    • 可以转换为已完成或已拒绝

  • 已解决:已完成或已拒绝,因此不可改变

如何创建承诺

function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello world! I am a promise');
    }, 0);
  });
}

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

运行代码片段Hide results展开片段

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用