给定以下示例,为什么outerScopeVar在所有情况下都未定义?
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异步性的典型问题。随时改进此问题,并添加更多简化的示例,社区可以识别。
一句话回答:异步性。
让我们首先跟踪常见行为。在所有示例中,outerScopeVar都在函数内部修改了。该函数显然不会立即执行,而是被分配或作为参数传递。这就是我们所说的回调。
现在的问题是,何时调用该回调?
这要视情况而定。让我们尝试再次跟踪一些常见行为:
img.onload
setTimeout
$.post
fs.readFile
异步执行从同步流中推出。也就是说,异步代码将永远不会在同步代码堆栈正在执行时执行。这就是JavaScript是单线程的意思。
更具体地说,当JS引擎处于空闲状态时-不执行(a)同步代码的堆栈-它将轮询可能触发异步回调的事件(例如,过期的超时,收到的网络响应),然后一个接一个地执行它们。这被视为事件循环。
也就是说,以手绘红色形状突出显示的异步代码只有在执行了它们各自代码块中的所有其余同步代码之后,才可以执行:
异步代码突出显示
简而言之,回调函数是同步创建的,但异步执行。在知道异步函数已执行之前,你就不能依赖它的执行,以及如何执行?
真的很简单。应从该异步函数内部启动/调用依赖于异步函数执行的逻辑。例如,将alerts和console.logs 移到回调函数中也将输出预期的结果,因为此时该结果可用。
通常,你需要根据异步函数的结果执行更多操作,或者根据调用异步函数的位置对结果执行不同的操作。让我们处理一个更复杂的示例:
var outerScopeVar; helloCatAsync(); alert(outerScopeVar); function helloCatAsync() { setTimeout(function() { outerScopeVar = 'Nya'; }, Math.random() * 2000); }
注意:我使用setTimeout随机延迟作为通用异步函数,同一示例适用于Ajax readFile,onload和任何其他异步流。
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. 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此示例中的实现)。你只需要传递回调函数,并了解它将在同步流之外执行,并重新组织代码以适应该情况。
helloCatAsync
你还将注意到,由于异步特性,不可能将return值从异步流返回到定义了回调的同步流,因为异步回调在同步代码已经完成执行很长时间之后才执行。
而不是return从异步回调中获取值,你将不得不使用回调模式,或者。。。
return