一尘不染

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

javascript

鉴于以下示例,为什么outerScopeVar在所有情况下都是undefined?

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


阅读 218

收藏
2021-10-16

共1个答案

一尘不染

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

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

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

  • img.onload可能会在将来的某个时间调用,当(以及如果)图像已成功加载时。
  • setTimeout可能会在将来的某个时间调用,在延迟到期并且超时没有被 取消之后clearTimeout。注意:即使使用0作为延迟,所有浏览器都有最小超时延迟上限(在 HTML5 规范中指定为 4ms)。
  • 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从异步回调中获取值,您将不得不使用回调模式,或者…承诺。可能好和有帮助!

2021-10-16