为什么变量在函数内部修改后没有改变?【Js异步代码】
鉴于以下示例,为什么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在所有这些示例中输出?我不想要解决方法,我想知道为什么会这样。
答案
让我们首先追踪常见的行为。在所有示例中,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 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本演示示例中的实现)。您只需要传递回调函数并了解它将在同步流之外执行,并重构您的代码以适应它。
您还会注意到,由于异步性质,不可能将return异步流中的值返回到定义回调的同步流,因为异步回调在同步代码已经完成执行后很长时间才执行。
不是return从异步回调中获取值,您将不得不使用回调模式,或者使用promise。
使用 promise 和 async/await
对于正在寻找快速参考以及一些使用 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
Promise 提供了一种比回调更灵活的替代方案,因为它们可以自然地组合以协调多个异步操作。
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery延迟
jQuery 提供的功能类似于带有 Deferred 的 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();本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!