协慌网

登录 贡献 社区

如何从异步调用返回响应?

我有一个函数foo ,它发出 Ajax 请求。我如何从foo返回响应?

我尝试从success回调中返回值,并将响应分配给函数内部的局部变量并返回该变量,但这些方法都没有实际返回响应。

function foo() {
    var result;

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

    return result;
}

var result = foo(); // It always ends up being `undefined`.

答案

→有关不同示例的异步行为的更一般说明,请参阅 我在函数内部修改变量之后为什么变量不变? - 异步代码引用

→如果您已经了解问题,请跳至下面的可能解决方案。

问题

Ajax 中A代表异步 。这意味着发送请求(或者更确切地说是接收响应)将从正常执行流程中取出。在您的示例中, $.ajax立即返回并返回下一个语句, return result; ,在您传递的函数之前执行,因为甚至调用了success回调。

这是一个类比,希望使同步和异步流之间的区别更加清晰:

同步

想象一下,你打电话给朋友,让他为你寻找一些东西。虽然可能需要一段时间,但是你要等电话并凝视太空,直到你的朋友给你你需要的答案。

当您进行包含 “普通” 代码的函数调用时,会发生同样的情况:

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

var item = findItem();

// Do something with item
doSomethingElse();

尽管findItem可能需要很长时间才能执行,但var item = findItem();之后的任何代码都会出现var item = findItem();必须等到函数返回结果。

异步

你出于同样的原因再次打电话给你的朋友。但是这次你告诉他你很匆忙,他应该用手机给你回电话。你挂断了,离开了房子,做了你打算做的事情。一旦你的朋友给你回电话,你正在处理他给你的信息。

这正是您执行 Ajax 请求时发生的情况。

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

而不是等待响应,执行立即继续执行 Ajax 调用之后的语句。为了得到响应,最终,你提供了一次收到答复要调用的函数,一个回调 (注意些什么呢? 我打电话吗?)。在调用回调之前执行该调用之后的任何语句。


解决方案(S)

拥抱 JavaScript 的异步特性!虽然某些异步操作提供了同步对应(“Ajax” 也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。

你问为什么这么糟糕?

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

所有这些都是非常糟糕的用户体验。用户将无法判断一切是否正常。此外,连接速度慢的用户效果会更差。

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

  • 使用async/await承诺 (ES2017 +,如果您使用转换器或再生器,则可在旧版浏览器中使用)
  • 回调 (在节点中很流行)
  • 承诺与then() (ES2015 +,如果您使用众多承诺库之一,可在旧版浏览器中使用)

这三个都在当前浏览器和节点 7 + 中可用。


ES2017 +:与async/await承诺

2017 年发布的 ECMAScript 版本引入了异步函数的语法级支持 。在asyncawait的帮助下,您可以在 “同步样式” 中编写异步。代码仍然是异步的,但它更容易阅读 / 理解。

async/await建立在 promises 之上: async函数总是返回一个 promise。 await “解包” 一个承诺,并导致承诺被解决的值,或者如果承诺被拒绝则抛出错误。

重要提示:您只能在async函数中使用await 。目前,尚不支持顶级await ,因此您可能必须创建异步 IIFE( 立即调用函数表达式 )来启动async上下文。

您可以在 MDN 上阅读有关asyncawait更多信息。

这是一个建立在上面延迟之上的例子:

// 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);
})();

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


让函数接受回调

回调只是传递给另一个函数的函数。其他函数可以在函数准备就绪时调用函数。在异步进程的上下文中,只要异步进程完成,就会调用回调。通常,结果将传递给回调。

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

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

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

这里我们定义了函数 “inline”,但你可以传递任何函数引用:

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 +:承诺然后()

Promise API是 ECMAScript 6(ES2015)的新功能,但它已经具有良好的浏览器支持 。还有许多库实现了标准的 Promises API,并提供了其他方法来简化异步函数(例如bluebird )的使用和组合。

承诺是未来价值观的容器。当 promise 接收到值(已解决 )或取消( 拒绝 )时,它会通知所有想要访问此值的 “侦听器”。

普通回调的优势在于它们允许您解耦代码并且更容易编写。

这是一个使用 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).
  });

应用于我们的 Ajax 调用,我们可以使用这样的 promises:

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("/echo/json")
  .then(function(result) {
    // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });

描述承诺提供的所有优点超出了本答案的范围,但如果您编写新代码,则应认真考虑它们。它们提供了很好的抽象和代码分离。

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

旁注:jQuery 的延迟对象

延迟对象是 jQuery 的 promises 自定义实现(在 Promise API 标准化之前)。它们的行为几乎与承诺相似,但暴露出略微不同的 API。

jQuery 的每个 Ajax 方法都已经返回一个 “延迟对象”(实际上是一个延迟对象的承诺),你可以从你的函数返回:

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

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

旁注:承诺陷阱

请记住,promises 和 deferred 对象只是未来值的容器 ,它们本身并不是值。例如,假设您有以下内容:

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 Deferred 对象,而不是来自服务器。这意味着if语句将始终获取此 Deferred 对象,将其视为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
});

不推荐:同步 “Ajax” 调用

正如我所提到的,一些(!)异步操作具有同步对应物。我不提倡使用它们,但为了完整起见,以下是执行同步调用的方法:

没有 jQuery

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

jQuery 的

如果使用jQuery ,则可以将async选项设置为false 。请注意,自 jQuery 1.8 以来不推荐使用此选项。然后,您可以仍然使用success回调或访问jqXHR 对象responseText属性:

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

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

当心!无法生成同步JSONP请求。 JSONP 本质上总是异步的(甚至不考虑这个选项的另一个原因)。

如果您没有在代码中使用 jQuery,那么这个答案就适合您

你的代码应该是这样的:

function foo() {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
    return httpRequest.responseText;
}

var result = foo(); // always ends up being 'undefined'

Felix Kling 为使用 jQuery for AJAX 的人写了一个很好的答案,我决定为那些没有使用 jQuery 的人提供替代方案。

注意,对于那些使用新fetch API 的人,Angular 或 promises 我在下面添加了另一个答案


你面对的是什么

这是另一个答案的 “问题解释” 的简短摘要,如果您在阅读本文后不确定,请阅读。

AJAX 中的A代表异步 。这意味着发送请求(或者更确切地说是接收响应)将从正常执行流程中取出。在您的示例中, .send立即返回并返回下一个语句, return result; ,在您传递的函数之前执行,因为甚至调用了success回调。

这意味着当您返回时,您定义的侦听器尚未执行,这意味着您返回的值尚未定义。

这是一个简单的比喻

function getFive(){ 
    var a;
    setTimeout(function(){
         a=5;
    },10);
    return a;
}

(小提琴)

的值a返回的undefined ,因为a=5部分还没有被执行。 AJAX 就像这样,你在服务器有机会告诉浏览器这个值是什么之前返回值。

一个可能的解决这个问题是代码重用积极 ,告诉你的程序在计算完成后做什么。

function onComplete(a){ // When the code completes, do this
    alert(a);
}

function getFive(whenDone){ 
    var a;
    setTimeout(function(){
         a=5;
         whenDone(a);
    },10);
}

这称为CPS 。基本上,我们将getFive传递getFive一个动作,当它完成时,我们告诉我们的代码在事件完成时如何反应(比如我们的 AJAX 调用,或者在这种情况下是超时)。

用法是:

getFive(onComplete);

哪个应警告 “5” 到屏幕。 (小提琴)

可能的解决方案

基本上有两种解决方法:

  1. 使 AJAX 调用同步(让我们称之为 SJAX)。
  2. 重构代码以使用回调正常工作。

1. 同步 AJAX - 不要这样做!!

至于同步 AJAX, 不要这样做!费利克斯的回答提出了一些令人信服的论据,说明为什么这是一个坏主意。总而言之,它会冻结用户的浏览器,直到服务器返回响应并创建非常糟糕的用户体验。以下是 MDN 对于其原因的另一个简短摘要:

XMLHttpRequest 支持同步和异步通信。但是,一般而言,出于性能原因,异步请求应优先于同步请求。

简而言之,同步请求会阻止代码的执行...... 这可能会导致严重的问题......

如果你必须这样做,你可以传递一个标志: 这是如何:

var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false);  // `false` makes the request synchronous
request.send(null);

if (request.status === 200) {// That's HTTP for 'ok'
  console.log(request.responseText);
}

2. 重组代码

让你的函数接受回调。在示例代码中,可以使foo接受回调。我们将告诉我们的代码如何在foo完成时做出反应

所以:

var result = foo();
// code that depends on `result` goes here

变为:

foo(function(result) {
    // code that depends on `result`
});

这里我们传递了一个匿名函数,但我们可以轻松地将引用传递给现有函数,使其看起来像:

function myHandler(result) {
    // code that depends on `result`
}
foo(myHandler);

有关如何完成此类回调设计的更多详细信息,请查看 Felix 的答案。

现在,让我们定义 foo 自己采取相应的行动

function foo(callback) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.onload = function(){ // when the request is loaded
       callback(httpRequest.responseText);// we're calling our method
    };
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
}

(小提琴)

我们现在已经让我们的 foo 函数接受了一个在 AJAX 成功完成时运行的动作,我们可以通过检查响应状态是否为 200 并进行相应的操作来进一步扩展它(创建一个失败处理程序等)。有效解决我们的问题。

如果您仍然很难理解这一点,请阅读 MDN 的 AJAX 入门指南

XMLHttpRequest 2 (首先阅读Benjamin GruenbaumFelix Kling的答案)

如果你不使用 jQuery 并想要一个很好的简短的 XMLHttpRequest 2,它适用于现代浏览器,也适用于移动浏览器,我建议用这种方式:

function ajax(a, b, c){ // URL, callback, just a placeholder
  c = new XMLHttpRequest;
  c.open('GET', a);
  c.onload = b;
  c.send()
}

如你看到的:

  1. 它比列出的所有其他功能都短。
  2. 回调是直接设置的(因此没有额外的不必要的闭包)。
  3. 它使用新的 onload(因此您不必检查 readystate && status)
  4. 还有其他一些我不记得的情况会让 XMLHttpRequest 1 变得烦人。

有两种方法可以获得此 Ajax 调用的响应(三种使用 XMLHttpRequest var 名称):

最简单的:

this.response

或者,如果由于某种原因你bind()回调bind()到一个类:

e.target.response

例:

function callback(e){
  console.log(this.response);
}
ajax('URL', callback);

或者(上面的一个是更好的匿名函数总是一个问题):

ajax('URL', function(e){console.log(this.response)});

没什么比这更容易

现在有些人可能会说最好使用 onreadystatechange 或甚至 XMLHttpRequest 变量名。那是错的。

查看XMLHttpRequest 高级功能

它支持所有 * 现代浏览器。我可以确认,因为我使用这种方法,因为 XMLHttpRequest 2 存在。在我使用的所有浏览器上,我从未遇到任何类型的问题。

onreadystatechange 仅在您希望获取状态 2 的标头时才有用。

使用XMLHttpRequest变量名是另一个重大错误,因为您需要在 onload / oreadystatechange 闭包内执行回调,否则您将丢失它。


现在,如果您想使用 post 和 FormData 更复杂的东西,您可以轻松扩展此功能:

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.send(d||null)
}

再一次...... 这是一个非常短的功能,但它确实得到和发布。

用法示例:

x(url, callback); // By default it's get so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set post data

或者传递一个完整的表单元素( document.getElementsByTagName('form')[0] ):

var fd = new FormData(form);
x(url, callback, 'post', fd);

或者设置一些自定义值:

var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);

如你所见,我没有实现同步... 这是一件坏事。

话虽如此...... 为什么不这么简单呢?


正如评论中所提到的,使用 error && synchronous 确实完全打破了答案的要点。哪个是以正确方式使用 Ajax 的简短方法?

错误处理程序

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.onerror = error;
  c.send(d||null)
}

function error(e){
  console.log('--Error--', this.type);
  console.log('this: ', this);
  console.log('Event: ', e)
}
function displayAjax(e){
  console.log(e, this);
}
x('WRONGURL', displayAjax);

在上面的脚本中,您有一个静态定义的错误处理程序,因此它不会危及该功能。错误处理程序也可用于其他功能。

但要真正解决错误, 唯一的方法是写一个错误的 URL,在这种情况下每个浏览器都会抛出一个错误。

如果您设置自定义标头,将 responseType 设置为 blob 数组缓冲区或其他任何内容,则错误处理程序可能很有用...

即使你传递'POSTAPAPAP' 作为方法它也不会抛出错误。

即使你将'fdggdgilfdghfldj' 作为 formdata 传递它也不会抛出错误。

在第一种情况下,错误在this.statusText下的displayAjax()内,因为Method not Allowed

在第二种情况下,它只是起作用。如果您传递了正确的帖子数据,则必须在服务器端进行检查。

跨域不允许自动抛出错误。

在错误响应中,没有错误代码。

只有this.type设置为 error。

如果您完全无法控制错误,为什么要添加错误处理程序?大多数错误都在回调函数displayAjax()

因此:如果您能够正确复制和粘贴 URL,则无需进行错误检查。 ;)

PS:作为我写的第一个测试 x('x',displayAjax)......,它完全得到了回应...... ??? 所以我检查了 HTML 所在的文件夹,并且有一个名为'x.xml' 的文件。因此,即使您忘记了文件的扩展名,XMLHttpRequest 2 也会找到它 。我好意思


同步读取文件

不要那样做。

如果你想阻止浏览器一段时间加载一个漂亮的大.txt文件同步。

function omg(a, c){ // URL
  c = new XMLHttpRequest;
  c.open('GET', a, true);
  c.send();
  return c; // Or c.response
}

现在你可以做到

var res = omg('thisIsGonnaBlockThePage.txt');

没有其他方法可以以非异步方式执行此操作。 (是的,使用 setTimeout 循环...... 但是认真吗?)

另一点是...... 如果你使用 API 或只是你自己的列表文件或者你总是为每个请求使用不同的函数...

只有当你有一个页面,你总是加载相同的 XML / JSON 或任何你只需要一个函数。在这种情况下,修改一点 Ajax 函数并用您的特殊函数替换 b。


以上功能仅供基本使用。

如果你想扩展功能......

是的你可以。

我使用了很多 API,我在每个 HTML 页面中集成的第一个函数之一是这个答案中的第一个 Ajax 函数,只有 GET ...

但是你可以用 XMLHttpRequest 2 做很多事情:

我创建了一个下载管理器(使用范围,包括简历,文件读取器,文件系统),使用画布的各种图像调整器转换器,使用 base64images 填充 Web SQL 数据库等等...... 但在这些情况下,您应该只创建一个函数目的... 有时你需要一个 blob,数组缓冲区,你可以设置标题,覆盖 mimetype,还有更多......

但这里的问题是如何返回 Ajax 响应...(我添加了一个简单的方法。)

→有关不同示例的异步行为的更一般说明,请参阅 我在函数内部修改变量之后为什么变量不变? - 异步代码引用

→如果您已经了解问题,请跳至下面的可能解决方案。

问题

Ajax 中A代表异步 。这意味着发送请求(或者更确切地说是接收响应)将从正常执行流程中取出。在您的示例中, $.ajax立即返回并返回下一个语句, return result; ,在您传递的函数之前执行,因为甚至调用了success回调。

这是一个类比,希望使同步和异步流之间的区别更加清晰:

同步

想象一下,你打电话给朋友,让他为你寻找一些东西。虽然可能需要一段时间,但是你要等电话并凝视太空,直到你的朋友给你你需要的答案。

当您进行包含 “普通” 代码的函数调用时,会发生同样的情况:

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

var item = findItem();

// Do something with item
doSomethingElse();

尽管findItem可能需要很长时间才能执行,但var item = findItem();之后的任何代码都会出现var item = findItem();必须等到函数返回结果。

异步

你出于同样的原因再次打电话给你的朋友。但是这次你告诉他你很匆忙,他应该用手机给你回电话。你挂断了,离开了房子,做了你打算做的事情。一旦你的朋友给你回电话,你正在处理他给你的信息。

这正是您执行 Ajax 请求时发生的情况。

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

而不是等待响应,执行立即继续执行 Ajax 调用之后的语句。为了得到响应,最终,你提供了一次收到答复要调用的函数,一个回调 (注意些什么呢? 我打电话吗?)。在调用回调之前执行该调用之后的任何语句。


解决方案(S)

拥抱 JavaScript 的异步特性!虽然某些异步操作提供了同步对应(“Ajax” 也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。

你问为什么这么糟糕?

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

所有这些都是非常糟糕的用户体验。用户将无法判断一切是否正常。此外,连接速度慢的用户效果会更差。

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

  • 使用async/await承诺 (ES2017 +,如果您使用转换器或再生器,则可在旧版浏览器中使用)
  • 回调 (在节点中很流行)
  • 承诺与then() (ES2015 +,如果您使用众多承诺库之一,可在旧版浏览器中使用)

这三个都在当前浏览器和节点 7 + 中可用。


ES2017 +:与async/await承诺

2017 年发布的 ECMAScript 版本引入了异步函数的语法级支持 。在asyncawait的帮助下,您可以在 “同步样式” 中编写异步。代码仍然是异步的,但它更容易阅读 / 理解。

async/await建立在 promises 之上: async函数总是返回一个 promise。 await “解包” 一个承诺,并导致承诺被解决的值,或者如果承诺被拒绝则抛出错误。

重要提示:您只能在async函数中使用await 。目前,尚不支持顶级await ,因此您可能必须创建异步 IIFE( 立即调用函数表达式 )来启动async上下文。

您可以在 MDN 上阅读有关asyncawait更多信息。

这是一个建立在上面延迟之上的例子:

// 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);
})();

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


让函数接受回调

回调只是传递给另一个函数的函数。其他函数可以在函数准备就绪时调用函数。在异步进程的上下文中,只要异步进程完成,就会调用回调。通常,结果将传递给回调。

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

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

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

这里我们定义了函数 “inline”,但你可以传递任何函数引用:

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 +:承诺然后()

Promise API是 ECMAScript 6(ES2015)的新功能,但它已经具有良好的浏览器支持 。还有许多库实现了标准的 Promises API,并提供了其他方法来简化异步函数(例如bluebird )的使用和组合。

承诺是未来价值观的容器。当 promise 接收到值(已解决 )或取消( 拒绝 )时,它会通知所有想要访问此值的 “侦听器”。

普通回调的优势在于它们允许您解耦代码并且更容易编写。

这是一个使用 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).
  });

应用于我们的 Ajax 调用,我们可以使用这样的 promises:

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("/echo/json")
  .then(function(result) {
    // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });

描述承诺提供的所有优点超出了本答案的范围,但如果您编写新代码,则应认真考虑它们。它们提供了很好的抽象和代码分离。

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

旁注:jQuery 的延迟对象

延迟对象是 jQuery 的 promises 自定义实现(在 Promise API 标准化之前)。它们的行为几乎与承诺相似,但暴露出略微不同的 API。

jQuery 的每个 Ajax 方法都已经返回一个 “延迟对象”(实际上是一个延迟对象的承诺),你可以从你的函数返回:

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

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

旁注:承诺陷阱

请记住,promises 和 deferred 对象只是未来值的容器 ,它们本身并不是值。例如,假设您有以下内容:

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 Deferred 对象,而不是来自服务器。这意味着if语句将始终获取此 Deferred 对象,将其视为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
});

不推荐:同步 “Ajax” 调用

正如我所提到的,一些(!)异步操作具有同步对应物。我不提倡使用它们,但为了完整起见,以下是执行同步调用的方法:

没有 jQuery

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

jQuery 的

如果使用jQuery ,则可以将async选项设置为false 。请注意,自 jQuery 1.8 以来不推荐使用此选项。然后,您可以仍然使用success回调或访问jqXHR 对象responseText属性:

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

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

当心!无法生成同步JSONP请求。 JSONP 本质上总是异步的(甚至不考虑这个选项的另一个原因)。

如果您没有在代码中使用 jQuery,那么这个答案就适合您

你的代码应该是这样的:

function foo() {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
    return httpRequest.responseText;
}

var result = foo(); // always ends up being 'undefined'

Felix Kling 为使用 jQuery for AJAX 的人写了一个很好的答案,我决定为那些没有使用 jQuery 的人提供替代方案。

注意,对于那些使用新fetch API 的人,Angular 或 promises 我在下面添加了另一个答案


你面对的是什么

这是另一个答案的 “问题解释” 的简短摘要,如果您在阅读本文后不确定,请阅读。

AJAX 中的A代表异步 。这意味着发送请求(或者更确切地说是接收响应)将从正常执行流程中取出。在您的示例中, .send立即返回并返回下一个语句, return result; ,在您传递的函数之前执行,因为甚至调用了success回调。

这意味着当您返回时,您定义的侦听器尚未执行,这意味着您返回的值尚未定义。

这是一个简单的比喻

function getFive(){ 
    var a;
    setTimeout(function(){
         a=5;
    },10);
    return a;
}

(小提琴)

的值a返回的undefined ,因为a=5部分还没有被执行。 AJAX 就像这样,你在服务器有机会告诉浏览器这个值是什么之前返回值。

一个可能的解决这个问题是代码重用积极 ,告诉你的程序在计算完成后做什么。

function onComplete(a){ // When the code completes, do this
    alert(a);
}

function getFive(whenDone){ 
    var a;
    setTimeout(function(){
         a=5;
         whenDone(a);
    },10);
}

这称为CPS 。基本上,我们将getFive传递getFive一个动作,当它完成时,我们告诉我们的代码在事件完成时如何反应(比如我们的 AJAX 调用,或者在这种情况下是超时)。

用法是:

getFive(onComplete);

哪个应警告 “5” 到屏幕。 (小提琴)

可能的解决方案

基本上有两种解决方法:

  1. 使 AJAX 调用同步(让我们称之为 SJAX)。
  2. 重构代码以使用回调正常工作。

1. 同步 AJAX - 不要这样做!!

至于同步 AJAX, 不要这样做!费利克斯的回答提出了一些令人信服的论据,说明为什么这是一个坏主意。总而言之,它会冻结用户的浏览器,直到服务器返回响应并创建非常糟糕的用户体验。以下是 MDN 对于其原因的另一个简短摘要:

XMLHttpRequest 支持同步和异步通信。但是,一般而言,出于性能原因,异步请求应优先于同步请求。

简而言之,同步请求会阻止代码的执行...... 这可能会导致严重的问题......

如果你必须这样做,你可以传递一个标志: 这是如何:

var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false);  // `false` makes the request synchronous
request.send(null);

if (request.status === 200) {// That's HTTP for 'ok'
  console.log(request.responseText);
}

2. 重组代码

让你的函数接受回调。在示例代码中,可以使foo接受回调。我们将告诉我们的代码如何在foo完成时做出反应

所以:

var result = foo();
// code that depends on `result` goes here

变为:

foo(function(result) {
    // code that depends on `result`
});

这里我们传递了一个匿名函数,但我们可以轻松地将引用传递给现有函数,使其看起来像:

function myHandler(result) {
    // code that depends on `result`
}
foo(myHandler);

有关如何完成此类回调设计的更多详细信息,请查看 Felix 的答案。

现在,让我们定义 foo 自己采取相应的行动

function foo(callback) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.onload = function(){ // when the request is loaded
       callback(httpRequest.responseText);// we're calling our method
    };
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
}

(小提琴)

我们现在已经让我们的 foo 函数接受了一个在 AJAX 成功完成时运行的动作,我们可以通过检查响应状态是否为 200 并进行相应的操作来进一步扩展它(创建一个失败处理程序等)。有效解决我们的问题。

如果您仍然很难理解这一点,请阅读 MDN 的 AJAX 入门指南

XMLHttpRequest 2 (首先阅读Benjamin GruenbaumFelix Kling的答案)

如果你不使用 jQuery 并想要一个很好的简短的 XMLHttpRequest 2,它适用于现代浏览器,也适用于移动浏览器,我建议用这种方式:

function ajax(a, b, c){ // URL, callback, just a placeholder
  c = new XMLHttpRequest;
  c.open('GET', a);
  c.onload = b;
  c.send()
}

如你看到的:

  1. 它比列出的所有其他功能都短。
  2. 回调是直接设置的(因此没有额外的不必要的闭包)。
  3. 它使用新的 onload(因此您不必检查 readystate && status)
  4. 还有其他一些我不记得的情况会让 XMLHttpRequest 1 变得烦人。

有两种方法可以获得此 Ajax 调用的响应(三种使用 XMLHttpRequest var 名称):

最简单的:

this.response

或者,如果由于某种原因你bind()回调bind()到一个类:

e.target.response

例:

function callback(e){
  console.log(this.response);
}
ajax('URL', callback);

或者(上面的一个是更好的匿名函数总是一个问题):

ajax('URL', function(e){console.log(this.response)});

没什么比这更容易

现在有些人可能会说最好使用 onreadystatechange 或甚至 XMLHttpRequest 变量名。那是错的。

查看XMLHttpRequest 高级功能

它支持所有 * 现代浏览器。我可以确认,因为我使用这种方法,因为 XMLHttpRequest 2 存在。在我使用的所有浏览器上,我从未遇到任何类型的问题。

onreadystatechange 仅在您希望获取状态 2 的标头时才有用。

使用XMLHttpRequest变量名是另一个重大错误,因为您需要在 onload / oreadystatechange 闭包内执行回调,否则您将丢失它。


现在,如果您想使用 post 和 FormData 更复杂的东西,您可以轻松扩展此功能:

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.send(d||null)
}

再一次...... 这是一个非常短的功能,但它确实得到和发布。

用法示例:

x(url, callback); // By default it's get so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set post data

或者传递一个完整的表单元素( document.getElementsByTagName('form')[0] ):

var fd = new FormData(form);
x(url, callback, 'post', fd);

或者设置一些自定义值:

var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);

如你所见,我没有实现同步... 这是一件坏事。

话虽如此...... 为什么不这么简单呢?


正如评论中所提到的,使用 error && synchronous 确实完全打破了答案的要点。哪个是以正确方式使用 Ajax 的简短方法?

错误处理程序

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.onerror = error;
  c.send(d||null)
}

function error(e){
  console.log('--Error--', this.type);
  console.log('this: ', this);
  console.log('Event: ', e)
}
function displayAjax(e){
  console.log(e, this);
}
x('WRONGURL', displayAjax);

在上面的脚本中,您有一个静态定义的错误处理程序,因此它不会危及该功能。错误处理程序也可用于其他功能。

但要真正解决错误, 唯一的方法是写一个错误的 URL,在这种情况下每个浏览器都会抛出一个错误。

如果您设置自定义标头,将 responseType 设置为 blob 数组缓冲区或其他任何内容,则错误处理程序可能很有用...

即使你传递'POSTAPAPAP' 作为方法它也不会抛出错误。

即使你将'fdggdgilfdghfldj' 作为 formdata 传递它也不会抛出错误。

在第一种情况下,错误在this.statusText下的displayAjax()内,因为Method not Allowed

在第二种情况下,它只是起作用。如果您传递了正确的帖子数据,则必须在服务器端进行检查。

跨域不允许自动抛出错误。

在错误响应中,没有错误代码。

只有this.type设置为 error。

如果您完全无法控制错误,为什么要添加错误处理程序?大多数错误都在回调函数displayAjax()

因此:如果您能够正确复制和粘贴 URL,则无需进行错误检查。 ;)

PS:作为我写的第一个测试 x('x',displayAjax)......,它完全得到了回应...... ??? 所以我检查了 HTML 所在的文件夹,并且有一个名为'x.xml' 的文件。因此,即使您忘记了文件的扩展名,XMLHttpRequest 2 也会找到它 。我好意思


同步读取文件

不要那样做。

如果你想阻止浏览器一段时间加载一个漂亮的大.txt文件同步。

function omg(a, c){ // URL
  c = new XMLHttpRequest;
  c.open('GET', a, true);
  c.send();
  return c; // Or c.response
}

现在你可以做到

var res = omg('thisIsGonnaBlockThePage.txt');

没有其他方法可以以非异步方式执行此操作。 (是的,使用 setTimeout 循环...... 但是认真吗?)

另一点是...... 如果你使用 API 或只是你自己的列表文件或者你总是为每个请求使用不同的函数...

只有当你有一个页面,你总是加载相同的 XML / JSON 或任何你只需要一个函数。在这种情况下,修改一点 Ajax 函数并用您的特殊函数替换 b。


以上功能仅供基本使用。

如果你想扩展功能......

是的你可以。

我使用了很多 API,我在每个 HTML 页面中集成的第一个函数之一是这个答案中的第一个 Ajax 函数,只有 GET ...

但是你可以用 XMLHttpRequest 2 做很多事情:

我创建了一个下载管理器(使用范围,包括简历,文件读取器,文件系统),使用画布的各种图像调整器转换器,使用 base64images 填充 Web SQL 数据库等等...... 但在这些情况下,您应该只创建一个函数目的... 有时你需要一个 blob,数组缓冲区,你可以设置标题,覆盖 mimetype,还有更多......

但这里的问题是如何返回 Ajax 响应...(我添加了一个简单的方法。)