鉴于以下示例,为什么 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 中,这个话题已经被迭代了至少几千次。因此,首先我想指出一些非常有用的资源:
@Felix Kling 对“如何从异步调用返回响应?”的回答。请参阅他解释同步和异步流程的出色答案,以及“重构代码”部分。 @Benjamin Gruenbaum 在同一个线程中也付出了很多努力来解释异步性。
@Matt Esch 对“从 fs.readFile 获取数据”的回答也以简单的方式很好地解释了异步性。
手头问题的答案
让我们首先追踪常见的行为。在所有示例中,outerScopeVar
都在 函数 内部进行了修改。该函数显然不会立即执行,它被分配或作为参数传递。这就是我们所说的回调。
现在的问题是,何时调用该回调?
这取决于具体情况。让我们再次尝试追踪一些常见的行为:
img.onload 可能会在未来某个时间被调用,当(如果)图像已经成功加载。
setTimeout 可能会在将来的某个时间被调用,在延迟到期并且超时没有被 clearTimeout 取消之后。注意:即使使用 0 作为延迟,所有浏览器都有一个最小超时延迟上限(在 HTML5 规范中指定为 4 毫秒)。
jQuery $.post 的回调可能会在未来某个时间被调用,当(如果)Ajax 请求已经成功完成。
Node.js 的 fs.readFile 可能会在未来某个时间被调用,当文件被成功读取或抛出错误时。
在所有情况下,我们都有一个可能在未来某个时间运行的回调。这种“未来某个时间”就是我们所说的异步流。
异步执行被推出同步流。也就是说,异步代码永远不会在同步代码堆栈执行时执行。这就是 JavaScript 单线程的意义。
更具体地说,当 JS 引擎空闲时——不执行(a)同步代码堆栈——它将轮询可能触发异步回调的事件(例如过期超时、接收到网络响应)并一个接一个地执行它们。这被视为Event Loop。
也就是说,手绘红色形状中突出显示的异步代码只有在其各自代码块中的所有剩余同步代码都已执行后才能执行:
https://i.stack.imgur.com/40IwM.png
简而言之,回调函数是同步创建但异步执行的。在您知道异步函数已经执行之前,您不能依赖它的执行,以及如何做到这一点?
这很简单,真的。应该从这个异步函数内部启动/调用依赖于异步函数执行的逻辑。例如,将 alert
和 console.log
也移动到回调函数中会输出预期的结果,因为此时结果是可用的。
实现自己的回调逻辑
通常,您需要对异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们处理一个更复杂的例子:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
注意:我使用带有随机延迟的 setTimeout
作为通用异步函数,同样的示例适用于 Ajax、readFile
、onload
和任何其他异步流。
这个例子显然和其他例子有同样的问题,它没有等到异步函数执行。
让我们通过实现我们自己的回调系统来解决它。首先,我们去掉了在这种情况下完全没用的丑陋的 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. 调用 helloCatAsync 传递一个回调函数, // 会调用它来接收异步操作的结果 console.log("1. function called...") helloCatAsync(function(result) { // 5. Received async 函数的结果, // 现在用它做任何你想做的事:console.log("5.result is: ", result); }); // 2. "callback" 参数是对 // 作为参数从 helloCatAsync 调用 function helloCatAsync(callback) { console.log("2. callback 这里是作为上述参数传递的函数的引用。 .") // 3. 开始异步操作: setTimeout(function() { console.log("3. 开始异步操作...") console.log("4. 完成异步操作,调用回调,传递结果...") // 4. 完成异步操作, // 调用回调,将结果作为参数传递 callback('Nya'); }, Math.random() * 2000); }
大多数情况下,在实际用例中,DOM API 和大多数库已经提供了回调功能(本演示示例中的 helloCatAsync
实现)。您只需要传递回调函数并了解它将在同步流之外执行,并重组您的代码以适应它。
您还会注意到,由于异步特性,return
不可能将异步流中的值返回到定义回调的同步流,因为异步回调是在同步代码已经完成执行很久之后才执行的.
您必须使用回调模式或... Promises,而不是return
从异步回调中获取值。
承诺
尽管有一些方法可以通过 vanilla JS 来阻止 callback hell,但 promise 越来越受欢迎,并且目前正在 ES6 中标准化(参见 Promise - MDN)。
Promises(又名 Futures)提供了对异步代码的更线性,因此更愉快的阅读,但解释它们的整个功能超出了这个问题的范围。相反,我会将这些优秀的资源留给感兴趣的人:
JavaScript 承诺 - HTML5 摇滚
你错过了承诺的重点 - domenic.me
更多关于 JavaScript 异步性的阅读材料
The Art of Node - Callbacks 用 vanilla JS 示例和 Node.js 代码很好地解释了异步代码和回调。
注意:我已将此答案标记为社区 Wiki,因此任何拥有至少 100 名声望的人都可以编辑和改进它!请随时改进此答案,或者如果您愿意,也可以提交一个全新的答案。我想把这个问题变成一个规范的话题来回答与 Ajax 无关的异步问题(有 How to return the response from an AJAX call? for that),因此这个话题需要你的帮助才能尽可能好和有帮助!
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
,这是一个 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()
is evil 用于此目的
如何让我的来电者等待?
您当前可能有一些类似于此的代码;
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
)。
function img.onload(){..
时,我怎么知道它是异步的,这是不可能的,但它似乎是同步的。是在 JS 的感觉中,一个人应该在阅读了很多这样的狗屎之后得到吗?
对于正在寻找快速参考的人以及使用 Promise 和 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);
});
Promise 提供了一种比回调更灵活的替代方案,因为它们可以自然地组合以协调多个异步操作。 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 通过其 Deferreds 提供了类似于 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 环境包括对 async
和 await
的支持(如 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();
function getMessage(param1, param2, callback) {...}
。
显而易见,杯子代表outerScopeVar
。
异步函数就像...
https://i.stack.imgur.com/yCASc.jpg
其他答案非常好,我只想提供一个直截了当的答案。仅限于 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 请求完成执行。
假设发布请求需要 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);
});
所有依赖于异步调用的代码,都被移动到异步块内,或者等待异步调用。
在所有这些场景中,outerScopeVar
被修改或分配了一个值/strong>。所以所有这些情况当前的执行流程都会导致 outerScopeVar = undefined
让我们讨论每个示例(我标记了异步调用或延迟某些事件发生的部分):
1.
https://i.stack.imgur.com/Ck9q6.png
在这里我们注册一个事件列表器,它将在该特定事件上执行。这里加载图像。然后当前执行继续下一行img.src = 'lolcat.png';
和alert(outerScopeVar);
,同时事件可能不会发生。即,函数 img.onload
等待被引用的图像异步加载。这将在以下所有示例中发生 - 事件可能不同。
2.
https://i.stack.imgur.com/rOzdX.png
这里超时事件扮演了角色,它将在指定时间后调用处理程序。这里是 0
,但它仍然注册了一个异步事件,它将被添加到 Event Queue
的最后一个位置以执行,这保证了延迟。
3.
https://i.stack.imgur.com/2VpYE.png
4.
https://i.stack.imgur.com/p8ZrL.png
Node可以被认为是异步编码之王。这里标记的函数注册为回调处理程序,将在读取指定文件后执行。
5.
https://i.stack.imgur.com/vpmGy.png
明显的承诺(将来会做一些事情)是异步的。见What are the differences between Deferred, Promise and Future in JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript
简短的回答是:异步性。
为什么需要异步?
JavaScript 是单线程的,这意味着脚本的两个位不能同时运行;他们必须一个接一个地跑。在浏览器中,JavaScript 与大量其他内容共享一个线程,这些内容因浏览器而异。但通常 JavaScript 与绘画、更新样式和处理用户操作(例如突出显示文本和与表单控件交互)在同一个队列中。其中一件事情的活动会延迟其他事情。
您可能已经使用事件和回调来解决这个问题。以下是活动:
var img1 = document.querySelector('.img-1'); img1.addEventListener('load', function() { // 图像加载 console.log("Loaded"); }); img1.addEventListener('error', function() { // 捕获错误 console.log("Errorprinted"); });
这根本不是打喷嚏。我们得到图像,添加几个监听器,然后 JavaScript 可以停止执行,直到其中一个监听器被调用。
不幸的是,在上面的例子中,事件可能发生在我们开始监听它们之前,所以我们需要使用图像的“完整”属性来解决这个问题:
var img1 = document.querySelector('.img-1'); function loaded() { // 图像加载 console.log("Loaded"); } if (img1.complete) { 加载(); } else { img1.addEventListener('load', 已加载); } img1.addEventListener('error', function() { // 错误捕获 console.log("Errorprinted"); });
这不会在我们有机会收听之前捕获错误的图像;不幸的是,DOM 并没有给我们提供方法来做到这一点。此外,这是加载一张图像。如果我们想知道一组图像何时加载,事情会变得更加复杂。
活动并不总是最好的方式
事件非常适合在同一对象上多次发生的事情——keyup
、touchstart
等。使用这些事件,您并不真正关心在附加侦听器之前发生了什么。
正确执行此操作的两种主要方法:回调和承诺。
回调
回调是在其他函数的参数中传递的函数,这个过程在 JavaScript 中是有效的,因为函数是对象并且对象可以作为参数传递给函数。回调函数的基本结构如下所示:
函数getMessage(回调){回调(); } function showMessage() { console.log("Hello world!我是回调"); } getMessage(showMessage);
承诺
尽管有一些方法可以使用 vanilla JS 来阻止回调地狱,但 promise 越来越受欢迎,并且目前正在 ES6 (see Promise) 中标准化。
Promise 是一个占位符,表示异步操作的最终结果(值)
承诺占位符将替换为结果值(如果成功)或失败原因(如果不成功)
如果您不需要知道某事何时发生,而只需要知道它是否发生,那么您正在寻找的就是一个承诺。
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); });
不定期副业成功案例分享