答案只有一个字:异步性。
前言
这个主题在Stack Overflow中已经被迭代了至少几千次。因此,首先我想指出一些非常有用的资源:
@Felix Kling对“如何从异步调用返回响应?”的回答。请参阅他解释同步和异步流的精彩回答,以及“重构代码”一节。
@Benjamin Gruenbaum也花了很多精力来解释同一个线程中的异步性。
@Matt Esch对“从fs获取数据”的回答。readFile也以简单的方式很好地解释了异步性。
手头问题的答案
让我们先追溯一下常见的行为。在所有示例中,outerScopeVar都是在函数内部修改的。该函数显然没有立即执行;它被赋值或作为参数传递。这就是我们所说的回调。
问题是,这个回调什么时候被调用?
这要看情况而定。让我们再试着追踪一些常见的行为:
img.onload may be called sometime in the future when (and if) the image has successfully loaded.
setTimeout may be called sometime in the future after the delay has expired and the timeout hasn't been canceled by clearTimeout. Note: even when using 0 as delay, all browsers have a minimum timeout delay cap (specified to be 4ms in the HTML5 spec).
jQuery $.post's callback may be called sometime in the future when (and if) the Ajax request has been completed successfully.
Node.js's fs.readFile may be called sometime in the future when the file has been read successfully or thrown an error.
在所有情况下,我们都有一个可能在未来某个时候运行的回调。这个“将来的某个时候”就是我们所说的异步流。
异步执行被推出同步流。也就是说,在同步代码堆栈执行时,异步代码永远不会执行。这就是JavaScript是单线程的含义。
更具体地说,当JS引擎空闲时——不执行一堆(a)同步代码——它将轮询可能触发异步回调的事件(例如超时,收到的网络响应),并一个接一个地执行它们。这被视为事件循环。
也就是说,在手绘的红色图形中突出显示的异步代码只有在各自代码块中所有剩余的同步代码执行完之后才能执行:
简而言之,回调函数是同步创建的,但异步执行。在知道异步函数已经执行之前,您不能依赖它的执行,如何做到这一点呢?
这真的很简单。依赖于异步函数执行的逻辑应该从这个异步函数内部启动/调用。例如,移动警报和控制台。回调函数内的日志将输出预期的结果,因为结果在那时是可用的。
实现自己的回调逻辑
通常,您需要对异步函数的结果做更多的事情,或者根据异步函数被调用的位置对结果做不同的事情。让我们来看一个更复杂的例子:
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);
}
大多数情况下,在实际用例中,DOM API和大多数库已经提供了回调功能(演示示例中的helloCatAsync实现)。您只需要传递回调函数,并理解它将在同步流之外执行,并重新构造代码以适应这一点。
您还将注意到,由于异步特性,不可能将一个值从异步流返回到定义回调的同步流,因为异步回调是在同步代码已经完成执行很久之后才执行的。
您将不得不使用回调模式,而不是从异步回调返回值,或者……承诺。
承诺
虽然在普通JS中有一些方法可以避免回调的麻烦,但是承诺越来越受欢迎,目前正在ES6中标准化(参见承诺- MDN)。
promise(又名Futures)提供了更线性的异步代码阅读,因此更令人愉快,但是解释它们的全部功能超出了这个问题的范围。相反,我将把这些优秀的资源留给感兴趣的人:
JavaScript承诺- HTML5岩石
你错过了承诺的意义- domenic.me
更多关于JavaScript异步性的阅读材料
Node的艺术-回调用普通JS示例和Node. JS代码很好地解释了异步代码和回调。
注意:我已经把这个答案标记为社区维基。因此,任何拥有至少100个声誉的人都可以编辑和改进它!如果您愿意,请随时改进这个答案或提交一个全新的答案。
我想把这个问题变成一个规范的主题,以回答与Ajax无关的异步性问题(如何从Ajax调用返回响应?为此),因此这个话题需要你的帮助,尽可能地好和有用!