我有一个函数foo,它发出一个异步请求.如何从foo返回响应/结果?

我试图从回调中返回值,并将结果赋给函数中的一个局部变量,然后返回该局部变量,但这些方法实际上都不会返回响应(它们都返回undefined或变量result的初始值).

Example of an asynchronous function that accepts a callback(使用jQuery的ajax函数)

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
            // return response; // <- I tried that one as well
        }
    });

    return result; // It always returns `undefined`
}

Example using Node.js:

function foo() {
    var result;

    fs.readFile("path/to/file", function(err, data) {
        result = data;
        // return data; // <- I tried that one as well
    });

    return result; // It always returns `undefined`
}

Example using the 100 block of a promise:

function foo() {
    var result;

    fetch(url).then(function(response) {
        result = response;
        // return response; // <- I tried that one as well
    });

    return result; // It always returns `undefined`
}

推荐答案

→ For a more general explanation of asynchronous behaviour with different examples, see Why is my variable unaltered after I modify it inside of a function? - 异步的 code reference

→ If you already understand the problem, skip to the possible solutions below.

The problem

Ajax分之A代表asynchronous.这意味着发送请求(或者更确切地说,接收响应)将从正常的执行流中删除.在您的示例中,$.ajax立即返回,下一条语句return result;在您作为success callback传递的函数被调用之前执行.

下面是一个类比,希望能让同步流和异步流之间的区别变得更清晰:

同步的

想象一下,你打电话给一个朋友,让他帮你查一些东西.虽然这可能需要一段时间,但你会在电话上等待并凝视太空,直到你的朋友给出你需要的答案.

当您进行包含"正常"代码的函数调用时,也会发生同样的情况:

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();

尽管执行findItem可能需要很长时间,但在函数返回结果之前,var item = findItem();之后的任何代码都必须执行wait.

异步的

你再次打电话给你的朋友也是出于同样的原因.但这次你告诉他你很忙,他应该在你的手机上打call you back.你挂断电话,离开家,做任何你计划做的事.一旦你的朋友给你回电话,你就是在处理他给你的信息.

这正是在执行Ajax请求时发生的情况.

findItem(function(item) {
    // Do something with the item
});
doSomethingElse();

不是等待响应,而是立即继续执行,并执行Ajax调用之后的语句.为了最终获得响应,您提供了一个在接收到响应后调用的函数,值为callback(注意到什么了吗?call back?).该调用之后的任何语句都会在调用回调之前执行.


Solution(s)

Embrace the asynchronous nature of JavaScript!虽然某些异步操作提供同步对等项("Ajax"也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中.

你问为什么不好?

JavaScript在浏览器的UI线程中运行,任何长时间运行的进程都将锁定UI,使其无响应.此外,JavaScript的执行时间有上限,浏览器会询问用户是否继续执行.

所有这些都会导致非常糟糕的用户体验.用户无法判断是否一切正常.此外,对于连接速度较慢的用户,效果会更糟.

在下文中,我们将介绍三种不同的解决方案,它们都是相互叠加的:

  • Promises with 100(ES2017+,如果使用transpiler或regenerator,则在旧浏览器中可用)
  • Callbacks(在 node 中流行)
  • Promises with 100(ES2015+,如果您使用许多promise库中的一个,可以在较旧的浏览器中使用)

All three are available in current browsers, and node 7+.


ES2017+: Promises with async/await

2017年发布的ECMAScript版本引入了syntax-level support个异步函数.在asyncawait的帮助下,您可以用"同步风格"编写异步代码.代码仍然是异步的,但是更容易阅读/理解.

async/await建立在promise 之上:async函数总是返回promise .await"解除"promise ,要么导致promise 的解决价值,要么在promise 被拒绝时抛出错误.

Important:async函数中只能使用await.目前,顶级await还不受支持,所以您可能需要创建一个异步IIFE(Immediately Invoked Function Expression)来启动async上下文.

你可以在MDN上阅读更多关于asyncawait的内容.

下面是一个详细阐述上述delay函数findItem()的示例:

// Using 'superagent' which will return a promise.
var superagent = require('superagent')

// This is isn't declared as `async` because it already returns a promise
function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}

async function getAllBooks() {
  try {
    // GET a list of book IDs of the current user
    var bookIDs = await superagent.get('/user/books');
    // wait for 3 seconds (just for the sake of this example)
    await delay();
    // GET information about each book
    return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
  } catch(error) {
    // If any of the awaited promises was rejected, this catch block
    // would catch the rejection reason
    return null;
  }
}

// Start an IIFE to use `await` at the top level
(async function(){
  let books = await getAllBooks();
  console.log(books);
})();

当前的browsernode版本支持async/await.您还可以通过在regenerator(或使用再生器的工具,如Babel)的帮助下将代码转换为ES5来支持较旧的环境.


Let functions accept callbacks

回调是指将函数1传递给函数2.函数2可以随时调用函数1.在异步进程的上下文中,只要异步进程完成,就会调用回调.通常,结果会传递给回调函数.

在问题的示例中,您可以让foo接受回调,并将其用作success回调.所以这件事

var result = foo();
// Code that depends on 'result'

变成了

foo(function(result) {
    // Code that depends on 'result'
});

这里我们定义了函数"内联",但是您可以传递任何函数引用:

function myCallback(result) {
    // Code that depends on 'result'
}

foo(myCallback);

foo本身的定义如下:

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}

callback指的是我们调用时传递给foo的函数,然后我们将其传递给success.也就是说,一旦Ajax请求成功,$.ajax将调用callback并将响应传递给回调(可以用result引用,因为这是我们定义回调的方式).

您也可以在将响应传递给回调之前对其进行处理:

function foo(callback) {
    $.ajax({
        // ...
        success: function(response) {
            // For example, filter the response
            callback(filtered_response);
        }
    });
}

使用回调编写代码比看起来更容易.毕竟,浏览器中的JavaScript在很大程度上是事件驱动的(DOM事件).接收Ajax响应只是一个事件. 当您必须使用第三方代码时,可能会出现困难,但大多数问题都可以通过仔细考虑应用程序流来解决.


ES2015+: Promises with then()

Promise API是ECMAScript 6(ES2015)的一个新特性,但它已经有了很好的browser support.还有许多实现标准promise API并提供附加方法以简化异步函数的使用和组合的库(例如,bluebird).

promise 是future个价值的容器.当promise 收到该值(resolved)或被取消(rejected)时,它会通知所有想要访问该值的"侦听器".

与普通回调相比的优点是,它们允许您将代码解耦,并且更容易组合.

以下是使用promise 的示例:

function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}

delay()
  .then(function(v) { // `delay` returns a promise
    console.log(v); // Log the value once it is resolved
  })
  .catch(function(v) {
    // Or do something else if it is rejected
    // (it would not happen in this example, since `reject` is not called).
  });
.as-console-wrapper { max-height: 100% !important; top: 0; }

应用到我们的Ajax调用中,我们可以使用这样的promise :

function ajax(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open('GET', url);
    xhr.send();
  });
}

ajax("https://jsonplaceholder.typicode.com/todos/1")
  .then(function(result) {
    console.log(result); // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });
.as-console-wrapper { max-height: 100% !important; top: 0; }

描述Promise提供的所有优势超出了本答案的范围,但是如果您编写新代码,您应该认真考虑它们.它们为您的代码提供了很好的抽象和分离.

关于promise 的更多信息:HTML5 rocks - JavaScript Promises.

旁注:jQuery的延迟对象

Deferred objects是jQuery对Promise的自定义实现(在Promise API标准化之前).它们的行为几乎像promise ,但公开的API略有不同.

jQuery的每个Ajax方法都已经返回了一个"延迟对象"(实际上是延迟对象的promise ),您只需从函数中返回:

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

附注:promise 已实现

请记住,promise 和延迟对象对于future 的价值只有containers,而不是价值本身.例如,假设您有以下情况:

function checkPassword() {
    return $.ajax({
        url: '/password',
        data: {
            username: $('#username').val(),
            password: $('#password').val()
        },
        type: 'POST',
        dataType: 'json'
    });
}

if (checkPassword()) {
    // Tell the user they're logged in
}

这段代码误解了上述异步问题.具体来说,$.ajax()在判断服务器上的"/password"页面时不会冻结代码——它会向服务器发送一个请求,在等待时,它会立即返回一个jQuery Ajax延迟对象,而不是服务器的响应.这意味着if语句将始终获取这个延迟对象,将其视为true,并像用户登录一样继续.不好的.

但是解决办法很简单:

checkPassword()
.done(function(r) {
    if (r) {
        // Tell the user they're logged in
    } else {
        // Tell the user their password was bad
    }
})
.fail(function(x) {
    // Tell the user something bad happened
});

Not recommended: 同步的 "Ajax" calls

正如我提到的,有些(!)异步操作具有同步对应项.我不主张使用它们,但为了完整起见,下面是执行同步调用的方式:

没有jQuery

如果直接使用XMLHttpRequest对象,请将false作为第三个参数传递给.open.

jQuery

如果使用jQuery,可以将async选项设置为false.请注意,自jQuery 1.8以来,这个选项是deprecated.

function foo() {
    var jqXHR = $.ajax({
        //...
        async: false
    });
    return jqXHR.responseText;
}

如果使用任何其他jQueryAjax方法,例如$.get$.getJSON等,则必须将其更改为$.ajax(因为只能将配置参数传递给$.ajax).

Heads up!不可能发出同步JSONP请求.JSONP本质上是异步的(一个更不用考虑这个选项的原因).

Javascript相关问答推荐

除了在Angular 16中使用快照之外,什么是可行且更灵活的替代方案?

使用useParams路由失败

togglePopover()不打开但不关闭原生HTML popover'

使搜索栏更改语言

如何将Openjphjs与next.js一起使用?

为什么useState触发具有相同值的呈现

如果Arrow函数返回函数,而不是为useEffect返回NULL,则会出现错误

rxjs插入延迟数据

单个HTML中的多个HTML文件

如何在使用rhandsontable生成表时扩展数字输入验证?

NG/Express API路由处理程序停止工作

如何在JAVASCRIPT中合并两组对象并返回一些键

我想使用GAS和HTML将从Electron 表格中获得的信息插入到文本字段的初始值中

在验证和提交表单后使用useNavigate()进行react 重定向,使用带有加载器和操作的路由

判断函数参数的类型

是否可以在不更改组件标识的情况下换出Reaction组件定义(以维护状态/引用等)?如果是这样的话,是如何做到的呢?

在单击按钮时生成多个表单时的处理状态

如何处理不带参数的redux thunk payloadCreator回调函数?

无法在Adyen自定义卡安全字段创建中使用自定义占位符

Rails 7:在不使用导入映射的情况下导入Java脚本