javascript - 如何从异步调用返回响应

标签 javascript jquery ajax asynchronous

我有一个功能 foo它发出异步请求。如何从 foo 返回响应/结果?
我试图从回调中返回值,并将结果分配给函数内的局部变量并返回该变量,但这些方法都没有真正返回响应(它们都返回 undefined 或任何初始值变量 result 是)。
使用 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`
}
使用 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`
}
使用 then 的示例 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? - Asynchronous code reference

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


问题
Ajax代表 asynchronous .这意味着发送请求(或者更确切地说是接收响应)已经脱离了正常的执行流程。在您的示例中,$.ajax立即返回和下一个语句,return result; , 在您作为 success 传递的函数之前执行甚至调用了回调。
这是一个类比,希望可以更清楚地区分同步流和异步流之间的区别:
同步
想象一下,你给 friend 打电话,让他​​帮你查点东西。虽然这可能需要一段时间,但您会等待电话并凝视太空,直到您的 friend 给您所需的答案。
当您进行包含“正常”代码的函数调用时,也会发生同样的情况:
function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();
即使 findItem执行可能需要很长时间,var item = findItem(); 之后的任何代码必须等到函数返回结果。
异步
出于同样的原因,您再次致电您的 friend 。但这一次你告诉他你有急事,他应该用你的手机给你回电。你挂断电话,离开家,做你计划做的任何事情。一旦你的 friend 给你回电,你就在处理他给你的信息。
这正是您执行 Ajax 请求时发生的情况。
findItem(function(item) {
    // Do something with the item
});
doSomethingElse();
不是等待响应,而是立即继续执行并执行 Ajax 调用之后的语句。为了最终获得响应,您提供了一个在收到响应后要调用的函数,即回调(注意什么?回调?)。在调用回调之前执行该调用之后的任何语句。

解决方案
拥抱 JavaScript 的异步特性! 虽然某些异步操作提供同步对应物(“Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。
你问为什么不好?
JavaScript 在浏览器的 UI 线程中运行,任何长时间运行的进程都会锁定 UI,使其无响应。此外,JavaScript 的执行时间有上限,浏览器会询问用户是否继续执行。
所有这些都会导致非常糟糕的用户体验。用户将无法判断是否一切正常。此外,对于连接速度较慢的用户,效果会更差。
在下文中,我们将研究三种不同的解决方案,它们都建立在彼此之上:
  • async/await 的 promise (ES2017+,如果您使用转译器或再生器,则可在旧浏览器中使用)
  • 回电 (流行节点)
  • then() 的 promise (ES2015+,如果您使用众多 promise 库之一,则可在旧浏览器中使用)

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

    ES2017+: promise async/await
    2017 年发布的 ECMAScript 版本引入了对异步函数的语法级支持。在 async 的帮助下和 await ,您可以以“同步风格”编写异步。代码仍然是异步的,但更容易阅读/理解。async/await建立在 promise 之上:async函数总是返回一个 promise 。 await “解开”一个promise 并且要么导致promise 被解决的值,要么在promise 被拒绝时抛出一个错误。
    重要提示:您只能使用 awaitasync 内功能。现在,顶级await尚不支持,因此您可能需要创建一个异步 IIFE ( Immediately Invoked Function Expression ) 来启动一个 async语境。
    您可以阅读更多关于 async await 在 MDN 上。
    这里有一个例子,详细说明了延迟函数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 的帮助下将代码转换为 ES5 来支持旧环境。 (或使用再生器的工具,例如 Babel )。

    让函数接受回调
    回调是指将函数 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 . IE。一旦 Ajax 请求成功,$.ajax将致电 callback并将响应传递给回调(可以用 result 引用,因为这是我们定义回调的方式)。
    您还可以在将响应传递给回调之前对其进行处理:
    function foo(callback) {
        $.ajax({
            // ...
            success: function(response) {
                // For example, filter the response
                callback(filtered_response);
            }
        });
    }
    
    使用回调编写代码比看起来更容易。毕竟,浏览器中的 JavaScript 是高度事件驱动的(DOM 事件)。接收 Ajax 响应只不过是一个事件。
    当您必须使用第三方代码时,可能会出现困难,但大多数问题都可以通过考虑应用程序流程来解决。

    ES2015+: promise then()
    Promise API是 ECMAScript 6 (ES2015) 的一个新特性,但它有很好的 browser support已经。还有许多库实现了标准的 Promises API,并提供了额外的方法来简化异步函数的使用和组合(例如, bluebird )。
    promise 是 future 值(value)的容器。当 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).
      });
    .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 和 deferred 对象只是 future 值的容器,它们不是值本身。例如,假设您有以下内容:
    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语句将始终获得此延迟对象,将其视为 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 对象,通过 false作为 .open 的第三个参数.
    jQuery
    如果您使用 jQuery ,您可以设置 async选项 false .请注意,此选项自 jQuery 1.8 起已弃用。
    然后您仍然可以使用 success回调或访问responseText jqXHR object 的属性(property):
    function foo() {
        var jqXHR = $.ajax({
            //...
            async: false
        });
        return jqXHR.responseText;
    }
    
    如果您使用任何其他 jQuery Ajax 方法,例如 $.get , $.getJSON等,你必须把它改成$.ajax (因为您只能将配置参数传递给 $.ajax )。
    当心! 无法进行同步 JSONP要求。 JSONP 就其本质而言始终是异步的(甚至不考虑此选项的另一个原因)。

    关于javascript - 如何从异步调用返回响应,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14220321/

    相关文章:

    javascript - 使用 jQuery 从 JS 数组动态创建 HTML 表

    jquery # 颜色转rgba?

    php - 在后台发送论坛而不重新加载页面

    javascript - 将引用其他函数的函数传递给React中的子组件

    jquery - 在.Net MVC 5的同一页面中创建 "One to many"的 View

    javascript - 如何通过添加按钮动态添加选择框,然后通过php将值保存在mysql中

    php - 无法读取未定义的属性 'readyState'

    javascript - "SyntaxError: Unexpected token :"在控制台中输入 { "a": "", "b": ""} json

    javascript - jQuery find - 它以什么顺序返回元素?

    javascript - Angular queryParams 与大括号混合绑定(bind)