varouterScopeVar; var img = document.createElement('img'); img.onload = 函数() { 外部作用域变量 = this.width; }; img.src = 'lolcat.png'; 警报(outerScopeVar);</pre>varouterScopeVar; 设置超时(函数(){ externalScopeVar = '你好异步世界!'; }, 0); 警报(outerScopeVar);</pre>// 使用一些 jQuery 的示例 var 外部作用域变量; $.post('loldog', 函数(响应) { 外部范围变量 = 响应; }); 警报(outerScopeVar);</pre>// Node.js 示例 var 外部作用域变量; fs.readFile('./catdog.html', 函数(错误, 数据) { 外部作用域变量 = 数据; }); console.log(outerScopeVar);</pre>// 带有 Promise var 外部作用域变量; myPromise.then(函数(响应){ 外部范围变量 = 响应; }); console.log(outerScopeVar);</pre>// 带有可观察值 var 外部作用域变量; myObservable.subscribe(函数(值){ 外部作用域变量 = 值; }); console.log(outerScopeVar);</pre>// 地理定位API var 外部作用域变量; navigator.geolocation.getCurrentPosition(函数 (pos) { 外部作用域变量 = pos; }); console.log(outerScopeVar);</pre> <p>为什么在所有这些示例中都输出 <code>undefined</code>我不需要解决方法,我想知道<strong>为什么</strong>会发生这种情况。</p> <小时/>> <块引用> <p><strong>注意:</strong>这是一个关于 JavaScript 异步性的规范问题。请自行改进这个问题并社区添加特色的更简化示例。</p> </blockquote><p><br /></p>
P粉0644484492023-08-29 11:02:10
Fabrício 的回答非常正确;但我想用一些不太技术性的内容来补充他的答案,重点是通过类比来帮助解释异步性的概念。
昨天,我正在做的工作需要从同事那里得到一些信息。我给他打了电话;谈话是这样进行的:
说到这里,我挂断了电话。由于我需要鲍勃提供的信息来完成我的报告,所以我留下了报告,去喝了杯咖啡,然后我看了一些电子邮件。 40 分钟后(鲍勃很慢),鲍勃回电并给了我我需要的信息。此时,我继续处理我的报告,因为我已经获得了所需的所有信息。
想象一下如果对话是这样进行的;
我坐在那里等待。并等待着。并等待着。 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)
。
这就是为什么我们看到警报显示未定义
;因为 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);
但是,我们现在知道返回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
)。
P粉1916105802023-08-29 10:42:58
一个词回答:异步性。
这个主题在 Stack Overflow 中已经被重复了至少几千次。因此,首先我想指出一些非常有用的资源:
@Felix Kling 对“如何从异步调用返回响应?”的回答。请参阅他解释同步和异步流程的出色答案,以及“重组代码”部分。
@Benjamin Gruenbaum 也投入了大量精力来解释同一线程中的异步性。
@Matt Esch 对“从 fs.readFile 获取数据”的回答也很好地解释了异步性简单的方式。
让我们首先追踪常见的行为。在所有示例中,outerScopeVar
均在函数内部进行修改。该函数显然不会立即执行;它被分配或作为参数传递。这就是我们所说的回调。
现在的问题是,什么时候调用该回调?
这要看具体情况。让我们尝试再次追踪一些常见行为:
img.onload
可能会在将来的某个时候调用(如果)图像已成功加载。setTimeout
可能会在延迟到期且超时尚未被 clearTimeout
取消后在将来的某个时间被调用。注意:即使使用 0
作为延迟,所有浏览器都有最小超时延迟上限(HTML5 规范中指定为 4 毫秒)。$.post
的回调。fs.readFile
可能会在将来的某个时候被调用。在所有情况下,我们都有一个可能在将来某个时候运行的回调。这个“将来的某个时候”就是我们所说的异步流。
异步执行被推出同步流程。也就是说,当同步代码堆栈正在执行时,异步代码将永远执行。这就是JavaScript单线程的意义。
更具体地说,当 JS 引擎空闲时——不执行一堆(a)同步代码——它将轮询可能触发异步回调的事件(例如超时、收到网络响应)并执行它们又一个。这被视为事件循环。
也就是说,手绘红色形状中突出显示的异步代码只能在其各自代码块中的所有剩余同步代码执行完毕后才执行:
简而言之,回调函数是同步创建但异步执行的。在知道异步函数已执行之前,您不能依赖它的执行,如何做到这一点?
这真的很简单。依赖于异步函数执行的逻辑应该从该异步函数内部启动/调用。例如,将 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 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);
}