ฉันจะคืนการตอบสนองจากการโทรแบบอะซิงโครนัสได้อย่างไร


5509

ฉันมีฟังก์ชั่น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`.

คำตอบ:


5703

→สำหรับคำอธิบายทั่วไปเกี่ยวกับพฤติกรรมของอะซิงก์กับตัวอย่างที่แตกต่างกันโปรดดู ทำไมตัวแปรของฉันไม่เปลี่ยนแปลงหลังจากที่ฉันแก้ไขภายในฟังก์ชั่น? - การอ้างอิงรหัสแบบอะซิงโครนัส

→หากคุณเข้าใจปัญหาแล้วให้ข้ามไปยังแนวทางแก้ไขที่เป็นไปได้ด้านล่าง

ปัญหา

ในอาแจ็กซ์ย่อมาไม่ตรงกัน นั่นหมายถึงการส่งคำขอ (หรือรับการตอบสนองมากกว่า) ถูกนำออกจากโฟลว์การดำเนินการปกติ ในตัวอย่างของคุณคืนค่าทันทีและคำสั่งถัดไปจะถูกดำเนินการก่อนที่ฟังก์ชันที่คุณส่งผ่านเมื่อมีการเรียกกลับ$.ajaxreturn 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();นั้นต้องรอจนกว่าฟังก์ชันจะส่งคืนผลลัพธ์

ไม่ตรงกัน

คุณโทรหาเพื่อนของคุณอีกครั้งด้วยเหตุผลเดียวกัน แต่คราวนี้คุณบอกเขาว่าคุณกำลังรีบและเขาควรโทรกลับหาคุณบนโทรศัพท์มือถือของคุณ คุณวางสายออกจากบ้านและทำสิ่งที่คุณวางแผนจะทำ เมื่อเพื่อนของคุณโทรกลับคุณกำลังติดต่อกับข้อมูลที่เขาให้กับคุณ

นั่นคือสิ่งที่เกิดขึ้นเมื่อคุณทำการร้องขอ Ajax

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

แทนที่จะรอการตอบสนองการดำเนินการจะดำเนินการทันทีและคำสั่งหลังจากการเรียกใช้ Ajax ในการรับการตอบสนองในที่สุดคุณต้องจัดเตรียมฟังก์ชันที่จะเรียกใช้เมื่อได้รับการตอบกลับการโทรกลับ (สังเกตเห็นบางสิ่งหรือไม่โทรกลับ ) คำสั่งใด ๆ ที่มาหลังจากการโทรนั้นจะถูกดำเนินการก่อนที่จะมีการโทรกลับ


วิธีการแก้ปัญหา (s)

ยอมรับธรรมชาติของ JavaScript แบบอะซิงโครนัส! ในขณะที่การดำเนินการแบบอะซิงโครนัสบางอย่างให้คู่แบบซิงโครนัส (เช่น "Ajax") แต่ก็ไม่แนะนำให้ใช้โดยเฉพาะในบริบทเบราว์เซอร์

ทำไมคุณถึงถามว่าไม่ดี

JavaScript ทำงานในเธรด UI ของเบราว์เซอร์และกระบวนการที่ทำงานเป็นเวลานานจะล็อค UI ทำให้ไม่ตอบสนอง นอกจากนี้มีข้อ จำกัด สูงสุดสำหรับเวลาดำเนินการสำหรับ JavaScript และเบราว์เซอร์จะถามผู้ใช้ว่าจะดำเนินการต่อหรือไม่

ทั้งหมดนี้เป็นประสบการณ์การใช้งานที่แย่จริงๆ ผู้ใช้จะไม่สามารถบอกได้ว่าทุกอย่างทำงานได้ดีหรือไม่ นอกจากนี้เอฟเฟกต์จะแย่ลงสำหรับผู้ใช้ที่เชื่อมต่อช้า

ในตัวอย่างต่อไปนี้เราจะดูโซลูชันที่แตกต่างกันสามแบบซึ่งทั้งหมดอยู่ด้านบนของกันและกัน:

  • สัญญากับasync/await (ES2017 + มีให้ในเบราว์เซอร์รุ่นเก่าถ้าคุณใช้ transpiler หรือตัวสร้างใหม่)
  • Callbacks (เป็นที่นิยมในโหนด)
  • สัญญากับthen() (ES2015 + มีให้ในเบราว์เซอร์รุ่นเก่าถ้าคุณใช้หนึ่งในห้องสมุดสัญญามากมาย)

ทั้งสามมีอยู่ในเบราว์เซอร์ปัจจุบันและโหนด 7+


ES2017 +: สัญญากับ async/await

รุ่น ECMAScript ที่วางจำหน่ายในปี 2560 แนะนำการรองรับระดับซินแทกซ์สำหรับฟังก์ชันอะซิงโครนัส ด้วยความช่วยเหลือของasyncและawaitคุณสามารถเขียนแบบอะซิงโครนัสใน "ซิงโครนัสสไตล์" รหัสยังคงไม่ตรงกัน แต่ง่ายต่อการอ่าน / ทำความเข้าใจ

async/awaitสร้างอยู่ด้านบนของสัญญา: asyncฟังก์ชั่นมักจะส่งกลับสัญญา await"ยกเลิกการห่อหุ้ม" สัญญาและส่งผลให้มูลค่าสัญญาถูกแก้ไขด้วยหรือส่งข้อผิดพลาดหากสัญญาถูกปฏิเสธ

สำคัญ:คุณสามารถใช้awaitภายในasyncฟังก์ชั่นเท่านั้น ตอนนี้awaitยังไม่ได้รับการสนับสนุนระดับสูงสุดดังนั้นคุณอาจต้องสร้าง async IIFE (การเรียกใช้ฟังก์ชั่นการแสดงออกทันที ) เพื่อเริ่มต้นasyncบริบท

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับasyncและawaitใน MDN

นี่คือตัวอย่างที่สร้างขึ้นจากความล่าช้าด้านบน:

// 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รุ่นสนับสนุน นอกจากนี้คุณยังสามารถสนับสนุนสภาพแวดล้อมที่เก่ากว่าโดยเปลี่ยนรหัสของคุณเป็น ES5 ด้วยความช่วยเหลือของตัวสร้างใหม่ (หรือเครื่องมือที่ใช้ตัวสร้างใหม่เช่นBabel )


ให้ฟังก์ชั่นยอมรับการเรียกกลับ

การเรียกกลับเป็นเพียงฟังก์ชันที่ส่งผ่านไปยังฟังก์ชันอื่น ฟังก์ชั่นอื่นนั้นสามารถเรียกใช้ฟังก์ชันที่ส่งผ่านเมื่อใดก็ตามที่พร้อม ในบริบทของกระบวนการอะซิงโครนัสการโทรกลับจะถูกเรียกเมื่อใดก็ตามที่กระบวนการอะซิงโครนัสเสร็จสิ้น โดยปกติแล้วผลลัพธ์จะถูกส่งไปยังการติดต่อกลับ

ในตัวอย่างของคำถามคุณสามารถ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);
        }
    });
}

การเขียนโค้ดโดยใช้การเรียกกลับง่ายกว่าที่คิด ท้ายที่สุดจาวาสคริปต์ในเบราว์เซอร์นั้นเป็นตัวผลักดันเหตุการณ์ (เหตุการณ์ DOM) การรับการตอบสนองของอาแจ็กซ์นั้นไม่มีอะไรอื่นนอกจากเป็นเหตุการณ์
ความยากลำบากอาจเกิดขึ้นเมื่อคุณต้องทำงานกับโค้ดของบุคคลที่สาม แต่ปัญหาส่วนใหญ่สามารถแก้ไขได้โดยเพียงแค่คิดผ่านโฟลว์แอปพลิเคชัน


ES2015 +: สัญญาด้วยแล้ว ()

สัญญา APIเป็นคุณลักษณะใหม่ของ ECMAScript 6 (ES2015) แต่มันก็มีดีการสนับสนุนเบราว์เซอร์แล้ว นอกจากนี้ยังมีห้องสมุดจำนวนมากที่ใช้ API สัญญามาตรฐานและให้วิธีการเพิ่มเติมเพื่อลดความยุ่งยากในการใช้งานและองค์ประกอบของฟังก์ชั่นอะซิงโครนัส (เช่นBluebird )

สัญญาเป็นภาชนะสำหรับค่าในอนาคต เมื่อสัญญาได้รับค่า (ได้รับการแก้ไข ) หรือเมื่อถูกยกเลิก ( ปฏิเสธ ) ก็จะแจ้ง "ผู้ฟัง" ทั้งหมดที่ต้องการเข้าถึงค่านี้

ข้อได้เปรียบเหนือ callback ธรรมดาคือช่วยให้คุณสามารถแยกรหัสของคุณและง่ายต่อการเขียน

นี่คือตัวอย่างง่ายๆของการใช้สัญญา:

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 ของเราเราสามารถใช้สัญญาเช่นนี้:

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

การอธิบายข้อดีทั้งหมดที่ข้อเสนอสัญญาเกินขอบเขตของคำตอบนี้ แต่ถ้าคุณเขียนรหัสใหม่คุณควรพิจารณาอย่างจริงจัง พวกเขาให้ความเป็นนามธรรมที่ดีและการแยกรหัสของคุณ

ข้อมูลเพิ่มเติมเกี่ยวกับสัญญา: HTML5 หิน - สัญญา JavaScript

หมายเหตุด้าน: วัตถุที่เลื่อนออกของ jQuery

ออบเจ็กต์ที่รอการตัดบัญชีเป็นการปฏิบัติตามสัญญาที่กำหนดเองของ jQuery (ก่อนที่ API ของสัญญาจะเป็นมาตรฐาน) พวกเขาทำตัวเหมือนสัญญา แต่เปิดเผย API ที่แตกต่างกันเล็กน้อย

ทุกวิธีของ Ajax ของ jQuery จะส่งคืน "วัตถุที่ถูกเลื่อนออกไป" (จริง ๆ แล้วเป็นสัญญาของวัตถุที่ถูกเลื่อนออกไป) ซึ่งคุณสามารถส่งคืนจากฟังก์ชันของคุณได้:

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

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

หมายเหตุด้านข้าง: สัญญา gotchas

โปรดทราบว่าสัญญาและวัตถุที่เลื่อนออกไปเป็นเพียงภาชนะสำหรับมูลค่าในอนาคตเท่านั้น แต่ไม่ใช่มูลค่า ตัวอย่างเช่นสมมติว่าคุณมีสิ่งต่อไปนี้:

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()ไม่หยุดรหัสในขณะที่มันตรวจสอบ '/ รหัสผ่านหน้าบนเซิร์ฟเวอร์ของคุณ - มันจะส่งคำขอไปยังเซิร์ฟเวอร์และในขณะที่มันรอมันทันทีส่งกลับวัตถุรอตัดบัญชี jQuery อาแจ็กซ์ไม่ได้การตอบสนองจากเซิร์ฟเวอร์ นั่นหมายความว่า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 :

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

ถ้าคุณใช้วิธี jQuery Ajax อื่น ๆ เช่น$.get, $.getJSONฯลฯ คุณจะต้องเปลี่ยนไป$.ajax(ตั้งแต่คุณเท่านั้นที่สามารถผ่านการกำหนดค่าพารามิเตอร์ไป$.ajax)

หัวขึ้น! ไม่สามารถทำการร้องขอJSONP แบบซิงโครนัส JSONP โดยธรรมชาติแล้วมันจะไม่พร้อมกันเสมอ (อีกเหตุผลที่จะไม่พิจารณาตัวเลือกนี้)


74
@Pommy: หากคุณต้องการใช้ jQuery คุณต้องรวมไว้ด้วย โปรดดูdocs.jquery.com/Tutorials:Getting_Started_with_jQuery
เฟลิกซ์คลิง

11
ในโซลูชันที่ 1 ย่อย jQuery ฉันไม่เข้าใจบรรทัดนี้: If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.(ใช่ฉันรู้ว่านิคของฉันเป็นเรื่องน่าขันในกรณีนี้)
cssyphus

32
@ gibberish: อืมมมฉันไม่รู้ว่ามันจะทำให้ชัดขึ้นได้อย่างไร คุณเห็นวิธีการที่fooเรียกว่าและฟังก์ชั่นจะถูกส่งผ่านไปยังมัน ( foo(function(result) {....});)? resultใช้ภายในฟังก์ชั่นนี้และเป็นการตอบสนองของคำขอ Ajax เพื่ออ้างถึงฟังก์ชั่นนี้พารามิเตอร์แรกของ foo จะถูกเรียกcallbackและกำหนดให้successแทนที่จะเป็นฟังก์ชั่นที่ไม่ระบุชื่อ ดังนั้น$.ajaxจะโทรcallbackเมื่อคำขอสำเร็จ ฉันพยายามอธิบายเพิ่มเติมอีกเล็กน้อย
เฟลิกซ์ลิ่ง

43
การแชทสำหรับคำถามนี้ตายแล้วดังนั้นฉันไม่แน่ใจว่าจะเสนอการเปลี่ยนแปลงที่ระบุไว้ที่ไหน แต่ฉันเสนอว่า: 1) เปลี่ยนส่วนซิงโครนัสเป็นการสนทนาอย่างง่าย ๆ ว่าทำไมมันถึงไม่ดี 2) ลบ / รวมตัวอย่างการโทรกลับเพื่อแสดงเฉพาะวิธีการรอการตัดบัญชีที่ยืดหยุ่นมากขึ้นซึ่งฉันคิดว่าอาจจะง่ายกว่าเล็กน้อยสำหรับผู้ที่เรียนรู้จาวาสคริปต์
Chris Moschini

14
@ เจสซี: ฉันคิดว่าคุณเข้าใจผิดว่าเป็นส่วนหนึ่งของคำตอบ คุณไม่สามารถใช้$.getJSONหากคุณต้องการให้คำขอ Ajax เป็นแบบซิงโครนัส อย่างไรก็ตามคุณไม่ควรต้องการให้คำขอซิงโครนัสดังนั้นจึงไม่มีผล คุณควรใช้การเรียกกลับหรือสัญญาว่าจะจัดการกับการตอบสนองตามที่อธิบายไว้ก่อนหน้านี้ในคำตอบ
เฟลิกซ์คลิง

1071

หากคุณไม่ได้ใช้ 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'

เฟลิกซ์คลิงทำได้ดีเขียนคำตอบสำหรับผู้ที่ใช้ jQuery สำหรับ AJAX ฉันตัดสินใจที่จะให้ทางเลือกสำหรับคนที่ไม่ได้เป็น

( หมายเหตุสำหรับผู้ที่ใช้fetchAPI ใหม่, เชิงมุมหรือสัญญาฉันได้เพิ่มคำตอบด้านล่างอีก )


สิ่งที่คุณกำลังเผชิญ

นี่เป็นบทสรุปสั้น ๆ ของ "คำอธิบายปัญหา" จากคำตอบอื่น ๆ หากคุณไม่แน่ใจหลังจากอ่านข้อความนี้แล้วให้อ่าน

ใน AJAX ย่อมาไม่ตรงกัน นั่นหมายถึงการส่งคำขอ (หรือรับการตอบสนองมากกว่า) ถูกนำออกจากโฟลว์การดำเนินการปกติ ในตัวอย่างของคุณคืนค่าทันทีและคำสั่งถัดไปจะถูกดำเนินการก่อนที่ฟังก์ชันที่คุณส่งผ่านเมื่อมีการเรียกกลับ.sendreturn 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ดำเนินการเพื่อดำเนินการเมื่อเสร็จสมบูรณ์เราจะบอกโค้ดของเราว่าจะตอบสนองอย่างไรเมื่อเหตุการณ์เสร็จสมบูรณ์ (เช่นการโทร 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 และทำตามนั้น (สร้างตัวจัดการความล้มเหลวและอื่น ๆ ) การแก้ปัญหาของเราอย่างมีประสิทธิภาพ

หากคุณยังมีปัญหาในการทำความเข้าใจโปรดอ่านคู่มือการเริ่มต้นใช้งาน AJAXที่ MDN


20
"คำขอแบบซิงโครนัสจะบล็อกการเรียกใช้รหัสและสามารถทำให้หน่วยความจำและเหตุการณ์รั่วไหล" หน่วยความจำรั่วแบบคำขอได้อย่างไร
แมทธิว G

10
@ MatthewG ฉันได้เพิ่มความโปรดปรานลงไปในคำถามนี้ฉันจะดูว่าฉันสามารถหาปลาได้ที่ไหน ฉันลบคำพูดออกจากคำตอบในเวลาเฉลี่ย
Benjamin Gruenbaum

17
เพียงสำหรับการอ้างอิง, XHR 2 ช่วยให้เราสามารถใช้onloadจัดการซึ่งยิงเมื่อเป็นreadyState 4แน่นอนว่ามันไม่รองรับใน IE8 (iirc อาจต้องมีการยืนยัน)
Florian Margaine

9
คำอธิบายของคุณเกี่ยวกับวิธีการส่งผ่านฟังก์ชั่นที่ไม่ระบุชื่อเป็นการโทรกลับนั้นถูกต้อง แต่ทำให้เข้าใจผิด ตัวอย่าง var bar = foo (); กำลังขอตัวแปรที่จะกำหนดในขณะที่ foo ที่แนะนำของคุณ (functim () {}); ไม่ได้กำหนดบาร์
Robbie Averill

396

XMLHttpRequest 2 (ก่อนอื่นอ่านคำตอบจาก Benjamin Gruenbaum & Felix 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 ใหม่ (ดังนั้นคุณไม่จำเป็นต้องตรวจสอบสถานะ && ผู้อ่าน)
  4. มีบางสถานการณ์ที่ฉันจำไม่ได้ว่าทำให้ XMLHttpRequest 1 น่ารำคาญ

มีสองวิธีในการรับการตอบสนองของการเรียก Ajax นี้ (สามวิธีโดยใช้ชื่อ var XMLHttpRequest):

ง่ายที่สุด:

this.response

หรือถ้าด้วยเหตุผลบางอย่างคุณ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


ตอนนี้ถ้าคุณต้องการบางสิ่งที่ซับซ้อนมากขึ้นโดยใช้โพสต์และ 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);

อย่างที่คุณเห็นฉันไม่ได้ใช้การซิงค์ ... มันเป็นสิ่งที่ไม่ดี

ต้องบอกว่า ... ทำไมไม่ทำอย่างง่าย ๆ ล่ะ?


ดังที่ได้กล่าวไว้ในความคิดเห็นการใช้งานของข้อผิดพลาด & & ซิงโครนัสจะทำลายจุดของคำตอบอย่างสมบูรณ์ วิธีใดที่ดีในการใช้ 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 เป็นบัฟเฟอร์อาร์เรย์หยดหรืออะไรก็ตาม ...

แม้ว่าคุณจะผ่าน 'POSTAPAPAP' เป็นวิธีการก็จะไม่เกิดข้อผิดพลาด

แม้ว่าคุณจะผ่าน 'fdggdgilfdghfldj' เป็น formdata แต่จะไม่เกิดข้อผิดพลาด

ในกรณีแรกที่มีข้อผิดพลาดอยู่ภายในdisplayAjax()ภายใต้การเป็นthis.statusTextMethod not Allowed

ในกรณีที่สองมันใช้งานได้ง่าย คุณต้องตรวจสอบที่ฝั่งเซิร์ฟเวอร์หากคุณผ่านข้อมูลการโพสต์ที่ถูกต้อง

ข้ามโดเมนไม่ได้รับอนุญาตพ่นข้อผิดพลาดโดยอัตโนมัติ

ในการตอบสนองข้อผิดพลาดไม่มีรหัสข้อผิดพลาด

มีเพียงthis.typeชุดที่ตั้งเป็นข้อผิดพลาด

เพิ่มตัวจัดการข้อผิดพลาดทำไมถ้าคุณไม่มีการควบคุมข้อผิดพลาดทั้งหมด? 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:

ฉันสร้างตัวจัดการดาวน์โหลด (ใช้ช่วงทั้งสองด้านด้วยเรซูเม่, ตัวกรอง, ระบบไฟล์), ตัวแปลงรูปภาพที่หลากหลายโดยใช้ Canvas, สร้างฐานข้อมูลเว็บ SQL ด้วย base64images และอื่น ๆ ... แต่ในกรณีเหล่านี้คุณควรสร้างฟังก์ชั่น จุดประสงค์ ... บางครั้งคุณต้องมี blob, array buffer, คุณสามารถตั้งค่าส่วนหัว, แทนที่ mimetype และมีอีกมากมาย ...

แต่คำถามที่นี่คือวิธีการตอบกลับ Ajax ... (ฉันได้เพิ่มวิธีง่ายๆ)


15
ในขณะที่คำตอบนี้เป็นสิ่งที่ดี (และเราทุกคนรัก XHR2 และโพสต์ข้อมูลไฟล์และข้อมูลหลายส่วนก็ยอดเยี่ยมมาก) - นี่แสดงให้เห็นว่าน้ำตาล syntactic สำหรับการโพสต์ XHR ด้วย JavaScript - คุณอาจต้องการใส่ไว้ในโพสต์บล็อก หรือแม้กระทั่งในห้องสมุด (ไม่แน่ใจเกี่ยวกับชื่อx, ajaxหรือxhrอาจจะดีกว่า :)) ฉันไม่เห็นว่าที่อยู่ตอบกลับจากการโทร AJAX อย่างไร (บางคนยังสามารถทำvar res = x("url")และไม่เข้าใจว่าทำไมมันไม่ทำงาน)) ในหมายเหตุด้าน - มันจะเจ๋งถ้าคุณกลับมาcจากวิธีการเพื่อให้ผู้ใช้สามารถขอเกี่ยวerrorและอื่น ๆ
Benjamin Gruenbaum

25
2.ajax is meant to be async.. so NO var res=x('url')..นั่นเป็นจุดทั้งหมดของคำถามนี้และคำตอบ :)
เบนจามิน Gruenbaum

3
ทำไมถึงมีพารามิเตอร์ 'c' ในฟังก์ชั่นถ้าในบรรทัดแรกคุณเขียนทับค่าอะไรก็ตาม ฉันพลาดอะไรไปรึเปล่า?
Brian H.

2
คุณสามารถใช้พารามิเตอร์เป็นตัวยึดตำแหน่งเพื่อหลีกเลี่ยงการเขียน "var" หลายครั้ง
cocco

11
@cocco คุณเขียนโค้ดที่เข้าใจผิดและไม่สามารถอ่านได้ในคำตอบ SO เพื่อบันทึกการกดแป้นบางครั้งหรือไม่ โปรดอย่าทำอย่างนั้น
ศิลา

316

หากคุณใช้สัญญาคำตอบนี้เหมาะสำหรับคุณ

ซึ่งหมายความว่า AngularJS, jQuery (ด้วยการเลื่อนเวลา), การแทนที่ XHR ดั้งเดิม (ดึงข้อมูล), EmberJS, การบันทึกของ BackboneJS หรือไลบรารีโหนดใด ๆ ที่ส่งคืนสัญญา

รหัสของคุณควรเป็นรหัสตาม:

function foo() {
    var data;
    // or $.get(...).then, or request(...).then, or query(...).then
    fetch("/echo/json").then(function(response){
        data = response.json();
    });
    return data;
}

var result = foo(); // result is always undefined no matter what.

Felix Kling ทำงานได้ดีเขียนคำตอบสำหรับผู้ใช้ jQuery ที่มี callback สำหรับ AJAX ฉันมีคำตอบสำหรับ XHR ดั้งเดิม คำตอบนี้สำหรับการใช้งานทั่วไปของสัญญาทั้งในส่วนหน้าหรือส่วนหลัง


ประเด็นหลัก

รูปแบบที่เห็นพ้องด้วย JavaScript ในเบราว์เซอร์และบนเซิร์ฟเวอร์ด้วย NodeJS / io.js เป็นตรงกันและปฏิกิริยา

เมื่อใดก็ตามที่คุณเรียกใช้เมธอดที่ส่งคืนสัญญาตัวthenจัดการจะถูกดำเนินการแบบอะซิงโครนัสเสมอนั่นคือหลังจากโค้ดด้านล่างพวกเขาที่ไม่ได้อยู่ใน.thenตัวจัดการ

ซึ่งหมายความว่าเมื่อคุณกลับมาจัดการคุณได้กำหนดไว้ไม่ได้ดำเนินการเลย ในทางกลับกันหมายความว่าค่าที่คุณส่งคืนไม่ได้ถูกตั้งค่าเป็นเวลาที่ถูกต้องdatathen

นี่คือการเปรียบเทียบที่ง่ายสำหรับปัญหา:

    function getFive(){
        var data;
        setTimeout(function(){ // set a timer for one second in the future
           data = 5; // after a second, do this
        }, 1000);
        return data;
    }
    document.body.innerHTML = getFive(); // `undefined` here and not 5

คุณค่าของ dataคือundefinedเนื่องจากdata = 5ส่วนยังไม่ได้ดำเนินการ มันอาจจะรันในเสี้ยววินาที แต่ในเวลานั้นมันไม่เกี่ยวข้องกับค่าที่ส่งคืน

เนื่องจากการดำเนินการยังไม่เกิดขึ้น (AJAX, การเรียกเซิร์ฟเวอร์, IO, ตัวจับเวลา) คุณจะส่งคืนค่าก่อนที่คำขอจะมีโอกาสบอกรหัสของคุณว่าค่านั้นคืออะไร

ทางออกหนึ่งที่เป็นไปได้สำหรับปัญหานี้คือการใช้รหัส ซ้ำอีกครั้งโดยบอกโปรแกรมของคุณว่าควรทำอย่างไรเมื่อการคำนวณเสร็จสมบูรณ์ สัญญาเปิดใช้งานสิ่งนี้โดยการชั่วคราว (ไวต่อเวลา) ในธรรมชาติ

สรุปย่อเกี่ยวกับสัญญา

สัญญาเป็นมูลค่าเมื่อเวลาผ่านไป สัญญามีสถานะพวกเขาเริ่มต้นเป็นรอดำเนินการโดยไม่มีค่าและสามารถชำระให้:

  • เติมเต็มความหมายว่าการคำนวณเสร็จสมบูรณ์แล้ว
  • ปฏิเสธหมายความว่าการคำนวณล้มเหลว

สัญญาสามารถเปลี่ยนแปลงสถานะได้เพียงครั้งเดียวหลังจากนั้นสัญญานั้นจะยังคงอยู่ในสถานะเดิมตลอดไป คุณสามารถแนบthenตัวจัดการกับสัญญาเพื่อแยกค่าและจัดการข้อผิดพลาด thenตัวจัดการอนุญาตให้ผูกสาย สัญญาถูกสร้างขึ้นโดยใช้ API ที่พวกเขากลับ ตัวอย่างเช่นการเปลี่ยน AJAX ที่ทันสมัยกว่าfetchหรือ jQuery$.getสัญญาการคืนสินค้า

เมื่อเราเรียก.thenในสัญญาและผลตอบแทนอะไรบางอย่างจากมัน - เราได้รับสัญญาสำหรับมูลค่าการประมวลผล ถ้าเราคืนสัญญาอีกครั้งเราจะได้สิ่งที่น่าอัศจรรย์ แต่มาจับม้ากันเถอะ

พร้อมสัญญา

เรามาดูกันว่าเราจะแก้ปัญหาข้างต้นได้อย่างไรด้วยสัญญา อันดับแรกให้แสดงให้เห็นถึงความเข้าใจของเราเกี่ยวกับสถานะสัญญาจากด้านบนโดยใช้ตัวสร้างสัญญาสำหรับการสร้างฟังก์ชันการหน่วงเวลา:

function delay(ms){ // takes amount of milliseconds
    // returns a new promise
    return new Promise(function(resolve, reject){
        setTimeout(function(){ // when the time is up
            resolve(); // change the promise to the fulfilled state
        }, ms);
    });
}

ตอนนี้หลังจากที่เราแปลง setTimeout เพื่อใช้สัญญาเราสามารถใช้thenเพื่อให้นับ:

function delay(ms){ // takes amount of milliseconds
  // returns a new promise
  return new Promise(function(resolve, reject){
    setTimeout(function(){ // when the time is up
      resolve(); // change the promise to the fulfilled state
    }, ms);
  });
}

function getFive(){
  // we're RETURNING the promise, remember, a promise is a wrapper over our value
  return delay(100).then(function(){ // when the promise is ready
      return 5; // return the value 5, promises are all about return values
  })
}
// we _have_ to wrap it like this in the call site, we can't access the plain value
getFive().then(function(five){ 
   document.body.innerHTML = five;
});

โดยทั่วไปแทนการกลับค่าที่เราไม่สามารถทำได้เนื่องจากรูปแบบการทำงานพร้อมกัน - เรากำลังกลับเสื้อคลุมสำหรับค่าที่เราสามารถแกะthenด้วย มันเหมือนกับกล่องที่คุณเปิดthenได้

ใช้สิ่งนี้

สิ่งนี้จะเหมือนกันสำหรับการโทร API ดั้งเดิมของคุณคุณสามารถ:

function foo() {
    // RETURN the promise
    return fetch("/echo/json").then(function(response){
        return response.json(); // process it inside the `then`
    });
}

foo().then(function(response){
    // access the value inside the `then`
})

ดังนั้นมันก็ใช้ได้เช่นกัน เราได้เรียนรู้ว่าเราไม่สามารถคืนค่าจากการโทรแบบอะซิงโครนัสได้ แต่เราสามารถใช้สัญญาและเชื่อมโยงพวกเขาเพื่อทำการประมวลผล ตอนนี้เรารู้วิธีการตอบกลับจากการโทรแบบอะซิงโครนัส

ES2015 (ES6)

ES6 แนะนำเครื่องกำเนิดไฟฟ้าซึ่งเป็นฟังก์ชั่นที่สามารถกลับมาอยู่ตรงกลางแล้วกลับสู่จุดที่พวกเขาอยู่ ซึ่งโดยทั่วไปจะมีประโยชน์สำหรับลำดับตัวอย่างเช่น:

function* foo(){ // notice the star, this is ES6 so new browsers/node/io only
    yield 1;
    yield 2;
    while(true) yield 3;
}

เป็นฟังก์ชันที่ส่งคืนตัววนซ้ำตามลำดับ1,2,3,3,3,3,....ซึ่งสามารถทำซ้ำได้ ในขณะที่เรื่องนี้น่าสนใจด้วยตัวเองและเปิดโอกาสให้มีความเป็นไปได้มากมาย

หากลำดับที่เราผลิตนั้นเป็นลำดับของการกระทำแทนที่จะเป็นตัวเลข - เราสามารถหยุดการทำงานชั่วคราวเมื่อใดก็ตามที่การกระทำถูกให้ผลและรอก่อนที่เราจะกลับมาทำงาน ดังนั้นแทนที่จะเรียงตามลำดับตัวเลขเราต้องการลำดับของค่าในอนาคตนั่นคือ: สัญญา

เคล็ดลับนี้ค่อนข้างซับซ้อน แต่มีประสิทธิภาพมากช่วยให้เราเขียนโค้ดแบบอะซิงโครนัสในลักษณะซิงโครนัส มี "นักวิ่ง" หลายคนที่ทำสิ่งนี้เพื่อคุณการเขียนหนึ่งคือรหัสสั้น ๆ แต่อยู่นอกเหนือขอบเขตของคำตอบนี้ ฉันจะใช้ครามเป็นPromise.coroutineที่นี่ แต่มีห่ออื่น ๆ เช่นหรือcoQ.async

var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // notice the yield
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
});

วิธีนี้คืนค่าสัญญาซึ่งเราสามารถบริโภคได้จาก coroutines อื่น ๆ ตัวอย่างเช่น:

var main = coroutine(function*(){
   var bar = yield foo(); // wait our earlier coroutine, it returns a promise
   // server call done here, code below executes when done
   var baz = yield fetch("/api/users/"+bar.userid); // depends on foo's result
   console.log(baz); // runs after both requests done
});
main();

ES2016 (ES7)

ใน ES7 นี้เป็นมาตรฐานเพิ่มเติมมีหลายข้อเสนอในขณะนี้ แต่คุณสามารถawaitสัญญาได้ นี่เป็นเพียง "น้ำตาล" (ไวยากรณ์ที่ดีกว่า) สำหรับข้อเสนอ ES6 ข้างต้นโดยการเพิ่มasyncและawaitคำหลัก ทำตัวอย่างข้างต้น:

async function foo(){
    var data = await fetch("/echo/json"); // notice the await
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
}

มันยังคงสัญญาเหมือนเดิม :)


นี่ควรเป็นคำตอบที่ยอมรับได้ +1 สำหรับ async / await (แม้ว่าเราจะไม่ควรทำreturn await data.json();อย่างไร)
ลูอิสโดโนแวน

247

คุณใช้ Ajax ไม่ถูกต้อง ความคิดนี้ไม่ใช่การส่งคืนสิ่งใด แต่ส่งมอบข้อมูลให้กับสิ่งที่เรียกว่าฟังก์ชันการเรียกกลับซึ่งจัดการกับข้อมูลแทน

นั่นคือ:

function handleData( responseData ) {

    // Do what you want with the data
    console.log(responseData);
}

$.ajax({
    url: "hi.php",
    ...
    success: function ( data, status, XHR ) {
        handleData(data);
    }
});

การส่งคืนสิ่งใด ๆ ในตัวจัดการการส่งจะไม่ทำอะไรเลย คุณต้องส่งมอบข้อมูลหรือทำในสิ่งที่คุณต้องการด้วยฟังก์ชันความสำเร็จโดยตรง


13
คำตอบนี้มีความหมายอย่างสมบูรณ์ ... วิธีการที่ประสบความสำเร็จของคุณเป็นเพียงการติดต่อกลับภายในการติดต่อกลับ คุณสามารถมีsuccess: handleDataและมันจะทำงาน
Jacques ジャック

5
และถ้าคุณต้องการส่งคืน "responseData" ด้านนอกของ "handleData" ... :) ... คุณจะทำอย่างไร ... ? ... สาเหตุกลับง่ายจะกลับไปที่ "ความสำเร็จ" โทรกลับของ ajax ... และไม่นอก "handleData" ...
pesho hristov

@Jacques & @pesho hristov คุณพลาดจุดนี้ไปแล้ว ส่งจัดการไม่ได้เป็นวิธีการที่มันเป็นขอบเขตโดยรอบของsuccess $.ajax
travnik

@travnik ฉันไม่ควรพลาด ถ้าคุณเอาเนื้อหาของ handleData และวางไว้ในวิธีการที่ประสบความสำเร็จก็จะทำหน้าที่เหมือนกัน ...
ฌาคส์ジャック

234

ทางออกที่ง่ายที่สุดคือการสร้างฟังก์ชั่น JavaScript และเรียกมันสำหรับการsuccessโทรกลับAjax

function callServerAsync(){
    $.ajax({
        url: '...',
        success: function(response) {

            successCallback(response);
        }
    });
}

function successCallback(responseObj){
    // Do something like read the response and show data
    alert(JSON.stringify(responseObj)); // Only applicable to JSON response
}

function foo(callback) {

    $.ajax({
        url: '...',
        success: function(response) {
           return callback(null, response);
        }
    });
}

var result = foo(function(err, result){
          if (!err)
           console.log(result);    
}); 

3
ฉันไม่รู้ว่าใครโหวตให้เป็นลบ แต่นี่เป็นวิธีการทำงานที่จริงแล้วฉันใช้วิธีนี้เพื่อสร้างแอปพลิเคชันทั้งหมด jquery.ajax จะไม่ส่งคืนข้อมูลดังนั้นควรใช้วิธีการข้างต้นดีกว่า หากผิดโปรดอธิบายและแนะนำวิธีที่ดีกว่า
Hemant Bavle

11
ขออภัยฉันลืมแสดงความคิดเห็น (ฉันมักจะทำ!) ฉันลงคะแนนมัน Downvote ไม่ได้บ่งบอกถึงความถูกต้องตามความเป็นจริงหรือไม่ชัดเจน แต่ก็บ่งบอกถึงประโยชน์ในบริบทหรือการขาด ฉันไม่คิดว่าคำตอบของคุณมีประโยชน์เนื่องจากเฟลิกซ์ได้อธิบายสิ่งนี้ในรายละเอียดเพิ่มเติมแล้ว ในบันทึกย่อด้านทำไมคุณจะพิจารณาการตอบสนองถ้ามันเป็น JSON
Benjamin Gruenbaum

5
ตกลง .. @Benjamin ฉันใช้ stringify เพื่อแปลงวัตถุ JSON เป็นสตริง และขอบคุณสำหรับการชี้แจงจุดของคุณ จะเก็บไว้ในใจเพื่อโพสต์คำตอบที่ซับซ้อนมากขึ้น
Hemant Bavle

และถ้าคุณต้องการคืน "responseObj" ด้านนอกของ "successCallback" ... :) ... คุณจะทำอย่างไร ... ? ... สาเหตุการคืนค่าแบบง่ายจะส่งคืนไปยังการเรียกกลับ "ความสำเร็จ" ของ ajax ... และไม่ใช่ด้านนอกของ "successCallback" ...
pesho hristov

221

ฉันจะตอบด้วยการ์ตูนที่วาดด้วยมือที่ดูน่ากลัว ภาพที่สองคือเหตุผลที่resultอยู่undefinedในตัวอย่างรหัสของคุณ

ป้อนคำอธิบายรูปภาพที่นี่


32
ภาพที่มีค่าพันคำ , นางสาวเอ - ขอให้บุคคลรายละเอียดของ B เพื่อแก้ไขปัญหารถของเขาในทางกลับกันคน B - ทำให้อาแจ็กซ์โทรและรอการตอบสนองจากเซิร์ฟเวอร์สำหรับรถแก้ไขรายละเอียดเมื่อรับการตอบสนองการทำงานของอาแจ็กซ์ที่ประสบความสำเร็จเรียกบุคคล ฟังก์ชัน B และส่งผ่านการตอบกลับเป็นอาร์กิวเมนต์ไปยังบุคคล A ได้รับคำตอบ
shaijut

10
คงจะดีมากถ้าคุณเพิ่มบรรทัดของรหัสด้วยแต่ละรูปภาพเพื่อแสดงแนวคิด
Hassan Baig

1
ในขณะเดียวกันคนที่มีรถติดอยู่ข้างถนน เขาต้องการรถที่ได้รับการแก้ไขก่อนดำเนินการต่อ ตอนนี้เขาอยู่คนเดียวข้างถนนที่รอ ... เขาอยากจะโทรศัพท์เพื่อรอการเปลี่ยนสถานะ แต่ช่างจะไม่ทำ ... ช่างบอกว่าเขาต้องทำงานต่อไปและไม่สามารถทำได้ เพียงออกไปเที่ยวกับโทรศัพท์ ช่างสัญญาว่าจะเรียกเขากลับมาโดยเร็วที่สุด หลังจากผ่านไปประมาณ 4 ชั่วโมงชายผู้นั้นก็เลิกโทรหา Uber - ตัวอย่างการหมดเวลา
barrypicker

@barrypicker :-D ยอดเยี่ยม!
โยฮันเนส Fahrenkrug

159

Angular1

สำหรับผู้ที่กำลังใช้AngularJSPromisesสามารถจัดการกับสถานการณ์นี้โดยใช้

ที่นี่มันบอกว่า

คำสัญญาสามารถใช้เพื่อฟังก์ชั่นอะซิงโครนัสที่ไม่ถูกต้องและอนุญาตให้หนึ่งฟังก์ชั่นเชื่อมโยงหลายฟังก์ชั่นเข้าด้วยกัน

คุณสามารถหาคำอธิบายที่ดีที่นี่เช่นกัน

ตัวอย่างที่พบในเอกสารที่กล่าวถึงด้านล่าง

  promiseB = promiseA.then(
    function onSuccess(result) {
      return result + 1;
    }
    ,function onError(err) {
      //Handle error
    }
  );

 // promiseB will be resolved immediately after promiseA is resolved 
 // and its value will be the result of promiseA incremented by 1.

Angular2 และใหม่กว่า

ในAngular2ที่มีลักษณะตัวอย่างต่อไปนี้ แต่ขอแนะนำให้ใช้กับObservablesAngular2

 search(term: string) {
     return this.http
  .get(`https://api.spotify.com/v1/search?q=${term}&type=artist`)
  .map((response) => response.json())
  .toPromise();

}

คุณสามารถบริโภคมันด้วยวิธีนี้

search() {
    this.searchService.search(this.searchField.value)
      .then((result) => {
    this.result = result.artists.items;
  })
  .catch((error) => console.error(error));
}

ดูโพสต์ต้นฉบับที่นี่ แต่ typescript ไม่รองรับสัญญา es6 ดั้งเดิมหากคุณต้องการใช้คุณอาจต้องใช้ปลั๊กอินสำหรับสิ่งนั้น

นอกจากนี้ที่นี่เป็นสัญญาที่สเปคกำหนดที่นี่


15
นี่ไม่ได้อธิบายว่าสัญญาจะแก้ไขปัญหานี้ได้อย่างไร
Benjamin Gruenbaum

4
jQuery และวิธีดึงข้อมูลทั้งคู่ส่งคืนสัญญาเช่นกัน ฉันขอแนะนำให้ทบทวนคำตอบของคุณ แม้ว่า jQuery จะไม่เหมือนกัน (จากนั้นมี แต่จับไม่ได้)
Tracker1

153

คำตอบส่วนใหญ่ที่นี่ให้คำแนะนำที่เป็นประโยชน์สำหรับเมื่อคุณมีการดำเนินการ async เดียว แต่บางครั้งสิ่งนี้เกิดขึ้นเมื่อคุณต้องการดำเนินการแบบอะซิงโครนัสสำหรับแต่ละรายการในอาร์เรย์หรือโครงสร้างรายการที่คล้ายกันอื่น ๆ สิ่งล่อใจคือการทำสิ่งนี้:

// WRONG
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log(results); // E.g., using them, returning them, etc.

ตัวอย่าง:

เหตุผลที่ใช้ไม่ได้คือการโทรกลับdoSomethingAsyncไม่ได้ทำงานตามเวลาที่คุณพยายามใช้ผลลัพธ์

ดังนั้นถ้าคุณมีอาร์เรย์ (หรือรายการบางชนิด) และต้องการดำเนินการ async สำหรับแต่ละรายการคุณมีสองตัวเลือก: ทำการดำเนินการแบบขนาน (ทับซ้อนกัน) หรือในซีรีส์ (เรียงลำดับกัน)

ขนาน

คุณสามารถเริ่มได้ทั้งหมดและติดตามจำนวนการโทรกลับที่คุณคาดหวังจากนั้นใช้ผลลัพธ์เมื่อคุณได้รับการโทรกลับจำนวนมาก:

var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

ตัวอย่าง:

(เราสามารถทำได้ด้วยexpectingและเพียงแค่ใช้results.length === theArray.lengthแต่นั่นทำให้เราเปิดรับความเป็นไปได้ที่theArrayมีการเปลี่ยนแปลงในขณะที่การโทรโดดเด่น ... )

สังเกตว่าเราใช้indexจากforEachเพื่อบันทึกผลลัพธ์อย่างไรresultsตำแหน่งเดียวกับรายการที่เกี่ยวข้องแม้ว่าผลลัพธ์จะออกมาตามลำดับ (เนื่องจากการโทรแบบ async ไม่จำเป็นต้องสมบูรณ์ตามลำดับที่เริ่มต้น)

แต่ถ้าคุณต้องการส่งคืนผลลัพธ์เหล่านั้นจากฟังก์ชัน ในขณะที่คำตอบอื่น ๆ ชี้ให้เห็นคุณไม่สามารถ; คุณต้องให้ฟังก์ชันของคุณยอมรับและโทรกลับ (หรือส่งคืนสัญญา ) นี่คือรุ่นติดต่อกลับ:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

ตัวอย่าง:

หรือนี่คือรุ่นที่ส่งคืนPromiseแทน:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

แน่นอนถ้าdoSomethingAsyncเราส่งข้อผิดพลาดเราจะใช้rejectเพื่อปฏิเสธสัญญาเมื่อเราได้รับข้อผิดพลาด)

ตัวอย่าง:

(หรืออีกวิธีหนึ่งคุณสามารถทำเสื้อคลุมสำหรับdoSomethingAsyncส่งคืนสัญญาแล้วทำด้านล่าง ... )

หากdoSomethingAsyncให้สัญญากับคุณคุณสามารถใช้Promise.all:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry);
    }));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

หากคุณรู้ว่าdoSomethingAsyncจะเพิกเฉยต่ออาร์กิวเมนต์ที่สองและสามคุณสามารถส่งต่อโดยตรงไปที่map( mapโทรกลับด้วยอาร์กิวเมนต์สามตัว แต่คนส่วนใหญ่ใช้เวลาส่วนใหญ่เพียงครั้งแรก):

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

ตัวอย่าง:

โปรดทราบว่าPromise.allแก้ไขสัญญาด้วยอาร์เรย์ของผลลัพธ์ของสัญญาทั้งหมดที่คุณให้ไว้เมื่อแก้ไขทั้งหมดหรือปฏิเสธสัญญาเมื่อสัญญาแรกที่คุณให้สัญญาปฏิเสธ

ชุด

สมมติว่าคุณไม่ต้องการให้การดำเนินการขนานกันหรือ? หากคุณต้องการเรียกใช้ทีละรายการคุณต้องรอให้การดำเนินการแต่ละรายการเสร็จสิ้นก่อนที่คุณจะเริ่มต้นการดำเนินการถัดไป นี่คือตัวอย่างของฟังก์ชั่นที่ทำและโทรกลับด้วยผลลัพธ์:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

(เนื่องจากเรากำลังทำงานในซีรีส์เราสามารถใช้งานได้results.push(result)เนื่องจากเรารู้ว่าเราจะไม่ได้ผลตามที่กล่าวข้างต้นเราสามารถใช้งานได้results[index] = result;แต่ในตัวอย่างต่อไปนี้บางตัวเราไม่มีดัชนี ใช้.)

ตัวอย่าง:

(หรืออีกครั้งสร้างเสื้อคลุมสำหรับdoSomethingAsyncที่ให้สัญญาและทำด้านล่าง ... )

หากdoSomethingAsyncให้สัญญากับคุณหากคุณสามารถใช้ไวยากรณ์ ES2017 + (อาจมี transpiler เช่นBabel ) คุณสามารถใช้asyncฟังก์ชันด้วยfor-ofและawait:

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

ตัวอย่าง:

หากคุณไม่สามารถใช้ไวยากรณ์ ES2017 + (ยัง) คุณสามารถใช้รูปแบบที่หลากหลายในรูปแบบ"สัญญาลด" (นี่ซับซ้อนกว่าการลดสัญญาปกติเนื่องจากเราไม่ได้ส่งผลลัพธ์จากหนึ่งไปสู่อีก รวบรวมผลลัพธ์ในอาร์เรย์):

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

ตัวอย่าง:

... ซึ่งยุ่งยากน้อยกว่าด้วยฟังก์ชั่นลูกศร ES2015 :

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

ตัวอย่าง:


1
คุณช่วยอธิบายได้อย่างไรว่าif (--expecting === 0)ส่วนของรหัสทำงานอย่างไร โซลูชันการโทรกลับของคุณใช้งานได้ดีสำหรับฉันฉันไม่เข้าใจว่าด้วยคำสั่งนั้นคุณกำลังตรวจสอบจำนวนคำตอบที่เสร็จสมบูรณ์ ขอบคุณที่มันขาดความรู้ในส่วนของฉัน มีวิธีอื่นที่สามารถเขียนเช็คได้หรือไม่?
ซาร่าห์

@Sarah: expectingเริ่มด้วยมูลค่าarray.lengthซึ่งเป็นจำนวนคำขอที่เราจะทำ เรารู้ว่าการโทรกลับจะไม่ถูกเรียกจนกว่าจะเริ่มคำขอทั้งหมด ในการติดต่อกลับif (--expecting === 0)ทำสิ่งนี้: 1. ลดลงexpecting(เราได้รับการตอบกลับดังนั้นเราคาดหวังการตอบสนองน้อยลงหนึ่งครั้ง) และหากค่าหลังจากการลดลงเป็น 0 (เราไม่ได้คาดหวังการตอบสนองใด ๆ เพิ่มเติม) ทำ!
TJ Crowder

1
@PatrickRoberts - ขอบคุณ !! ใช่ข้อผิดพลาดในการคัดลอกและวางอาร์กิวเมนต์ที่สองนั้นถูกละเว้นอย่างสมบูรณ์ในตัวอย่างนั้น (ซึ่งเป็นเหตุผลเดียวที่มันไม่ได้ล้มเหลวเนื่องจากคุณresultsไม่ได้ชี้ให้เห็น) :-) ซ่อมมัน.
TJ Crowder

111

ดูตัวอย่างนี้:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope,$http) {

    var getJoke = function(){
        return $http.get('http://api.icndb.com/jokes/random').then(function(res){
            return res.data.value;  
        });
    }

    getJoke().then(function(res) {
        console.log(res.joke);
    });
});

อย่างที่คุณเห็นgetJokeคือคืนสัญญาที่ได้รับการแก้ไขแล้ว(แก้ไขได้เมื่อส่งคืนres.data.value) ดังนั้นคุณรอจนกระทั่งคำขอ$ http.getเสร็จสมบูรณ์จากนั้นconsole.log (res.joke)จะถูกดำเนินการ (เป็นโฟลว์แบบอะซิงโครนัสปกติ)

นี่คือ plnkr:

http://embed.plnkr.co/XlNR7HpCaIhJxskMJfSg/

วิธี ES6 (async - รอ)

(function(){
  async function getJoke(){
    let response = await fetch('http://api.icndb.com/jokes/random');
    let data = await response.json();
    return data.value;
  }

  getJoke().then((joke) => {
    console.log(joke);
  });
})();

107

นี่คือหนึ่งในสถานที่ซึ่งข้อมูลสองวิธีผูกข้อมูลหรือเก็บแนวคิดที่ใช้ในเฟรมเวิร์ก JavaScript ใหม่ ๆ จะทำงานได้ดีสำหรับคุณ ...

ดังนั้นหากคุณใช้Angular, Reactหรือกรอบงานอื่น ๆ ที่ทำสองวิธีผูกข้อมูลหรือแนวคิดการจัดเก็บปัญหานี้แก้ไขได้ง่ายสำหรับคุณดังนั้นในคำง่าย ๆ ผลลัพธ์ของคุณจะundefinedอยู่ในขั้นแรกดังนั้นคุณจึงมีresult = undefinedก่อน ข้อมูลจากนั้นทันทีที่คุณได้รับผลลัพธ์มันจะได้รับการอัปเดตและกำหนดให้กับค่าใหม่ที่ตอบสนองการโทร Ajax ของคุณ ...

แต่วิธีที่คุณสามารถทำได้ในจาวาสคริปต์บริสุทธิ์หรือjQueryเช่นที่คุณถามในคำถามนี้?

คุณสามารถใช้โทรกลับ , สัญญาและเมื่อเร็ว ๆ นี้ที่สังเกตจะจัดการกับมันสำหรับคุณเช่นในสัญญาที่เรามีฟังก์ชั่นบางอย่างเช่นsuccess()หรือthen()ซึ่งจะต้องถูกประหารชีวิตเมื่อข้อมูลของคุณพร้อมสำหรับคุณเช่นเดียวกันกับการเรียกกลับหรือสมัครฟังก์ชั่นในการสังเกต

ตัวอย่างเช่นในกรณีของคุณที่คุณใช้jQueryคุณสามารถทำสิ่งนี้:

$(document).ready(function(){
    function foo() {
        $.ajax({url: "api/data", success: function(data){
            fooDone(data); //after we have data, we pass it to fooDone
        }});
    };

    function fooDone(data) {
        console.log(data); //fooDone has the data and console.log it
    };

    foo(); //call happens here
});

สำหรับข้อมูลเพิ่มเติมศึกษาเกี่ยวกับสัญญาและสิ่งที่สังเกตได้ซึ่งเป็นวิธีการใหม่ในการทำ async stuff นี้


สิ่งนี้ใช้ได้ในขอบเขตทั่วโลก แต่ในบางบริบทของโมดูลคุณอาจต้องการตรวจสอบบริบทที่เหมาะสมสำหรับการโทรกลับเช่น$.ajax({url: "api/data", success: fooDone.bind(this)});
steve.sims

8
สิ่งนี้ไม่ถูกต้องเนื่องจาก React มีการเชื่อมโยงข้อมูลขา
Matthew Brent

@MatthewBrent คุณไม่ผิด แต่ไม่ถูกต้องนอกจากนี้ยังตอบสนองอุปกรณ์ประกอบฉากเป็นวัตถุและถ้าเปลี่ยนพวกเขาเปลี่ยนแปลงตลอดใบสมัคร แต่ไม่ได้ทางที่ตอบสนองนักพัฒนาแนะนำให้ใช้มัน ...
Alireza

98

มันเป็นปัญหาที่พบบ่อยมากที่เราเผชิญในขณะที่ดิ้นรนกับ 'ความลึกลับ' ของ JavaScript ผมขอลองไขปริศนาอันลึกลับนี้วันนี้

เริ่มจากฟังก์ชั่น JavaScript ง่ายๆ:

function foo(){
// do something 
 return 'wohoo';
}

let bar = foo(); // bar is 'wohoo' here

นั่นคือการเรียกใช้ฟังก์ชั่นซิงโครนัสอย่างง่าย ๆ (โดยที่โค้ดแต่ละบรรทัดนั้น 'เสร็จสิ้นด้วยงาน' ก่อนที่จะเรียงตามลำดับถัดไป) และผลลัพธ์จะเป็นไปตามที่คาดไว้

ทีนี้ลองเพิ่มการบิดหน่อยโดยแนะนำความล่าช้าเล็กน้อยในฟังก์ชั่นของเราเพื่อให้โค้ดทุกบรรทัดไม่ 'เสร็จสิ้น' ตามลำดับ ดังนั้นมันจะเลียนแบบพฤติกรรมแบบอะซิงโครนัสของฟังก์ชัน:

function foo(){
 setTimeout( ()=>{
   return 'wohoo';
  }, 1000 )
}

let bar = foo() // bar is undefined here

คุณไปแล้วความล่าช้านั้นก็ทำลายการทำงานที่เราคาดไว้! แต่เกิดอะไรขึ้นกันแน่? จริงๆแล้วมันค่อนข้างสมเหตุสมผลถ้าคุณดูรหัส ฟังก์ชั่นfoo(), เมื่อทำการประมวลผล, จะไม่ส่งคืนสิ่งใด (มีค่าที่ส่งคืนundefined), แต่มันจะเริ่มจับเวลา, ซึ่งจะเรียกใช้ฟังก์ชันหลังจาก 1s เพื่อส่งคืน 'wohoo' แต่อย่างที่คุณเห็นค่าที่กำหนดให้กับบาร์คือสิ่งที่ส่งคืนทันทีจาก foo () ซึ่งไม่ได้เป็นเช่นundefinedนั้น

ดังนั้นเราจะแก้ไขปัญหานี้ได้อย่างไร

ลองถามฟังก์ชั่นของเราสำหรับPROMISE คำมั่นสัญญาเป็นเรื่องเกี่ยวกับความหมาย: หมายความว่าฟังก์ชันรับประกันว่าคุณจะให้ผลลัพธ์ที่ได้รับในอนาคต ดังนั้นเรามาดูกันว่ามันมีผลกับปัญหาเล็ก ๆ ของเราด้านบน:

function foo(){
   return new Promise( (resolve, reject) => { // I want foo() to PROMISE me something
    setTimeout ( function(){ 
      // promise is RESOLVED , when execution reaches this line of code
       resolve('wohoo')// After 1 second, RESOLVE the promise with value 'wohoo'
    }, 1000 )
  })
}

let bar ; 
foo().then( res => {
 bar = res;
 console.log(bar) // will print 'wohoo'
});

ดังนั้นบทสรุปคือ - เพื่อจัดการกับฟังก์ชั่นแบบอะซิงโครนัสเช่นการโทรตาม ajax เป็นต้นคุณสามารถใช้สัญญากับresolveค่า (ซึ่งคุณตั้งใจจะส่งคืน) ดังนั้นในระยะสั้นคุณจะแก้ไขค่าแทนที่จะส่งกลับในฟังก์ชันอะซิงโครนัส

อัปเดต (สัญญากับ async / รอ)

นอกเหนือจากการใช้then/catchเพื่อทำงานกับคำสัญญายังมีอีกหนึ่งวิธี แนวคิดคือการรู้จักฟังก์ชั่นอะซิงโครนัสแล้วรอสัญญาที่จะแก้ไขก่อนที่จะย้ายไปยังบรรทัดถัดไปของรหัส มันยังคงpromisesอยู่ภายใต้ประทุน แต่ด้วยวิธีการสร้างประโยคที่แตกต่างกัน เพื่อให้สิ่งต่าง ๆ ชัดเจนขึ้นคุณสามารถค้นหาการเปรียบเทียบด้านล่าง:

รุ่นที่จับแล้ว:

function saveUsers(){
     getUsers()
      .then(users => {
         saveSomewhere(users);
      })
      .catch(err => {
          console.error(err);
       })
 }

เวอร์ชัน async / รอ:

  async function saveUsers(){
     try{
        let users = await getUsers()
        saveSomewhere(users);
     }
     catch(err){
        console.error(err);
     }
  }

นี่ยังถือว่าเป็นวิธีที่ดีที่สุดในการส่งคืนค่าจากสัญญาหรือ async / รอใช่ไหม?
edwardsmarkf

3
@edwardsmarkf โดยส่วนตัวฉันไม่คิดว่าจะมีวิธีที่ดีที่สุดเช่นนี้ ฉันใช้สัญญาด้วย / catch, async / คอยรวมทั้งกำเนิดสำหรับ async ส่วนของรหัสของฉัน ส่วนใหญ่ขึ้นอยู่กับบริบทของการใช้งาน
Anish K.

96

อีกวิธีในการส่งคืนค่าจากฟังก์ชันอะซิงโครนัสคือส่งผ่านวัตถุที่จะเก็บผลลัพธ์จากฟังก์ชันอะซิงโครนัส

นี่คือตัวอย่างของสิ่งเดียวกัน:

var async = require("async");

// This wires up result back to the caller
var result = {};
var asyncTasks = [];
asyncTasks.push(function(_callback){
    // some asynchronous operation
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;
            _callback();
        }
    });
});

async.parallel(asyncTasks, function(){
    // result is available after performing asynchronous operation
    console.log(result)
    console.log('Done');
});

ฉันกำลังใช้ resultวัตถุเพื่อเก็บค่าระหว่างการดำเนินการแบบอะซิงโครนัส สิ่งนี้อนุญาตให้ผลลัพธ์พร้อมใช้งานแม้หลังจากงานอะซิงโครนัส

ฉันใช้วิธีนี้มาก ฉันสนใจที่จะทราบว่าวิธีการนี้ทำงานได้ดีเพียงใดหากการเดินสายผลลัพธ์ย้อนกลับผ่านโมดูลที่เกี่ยวข้องนั้น


9
ไม่มีอะไรพิเศษเกี่ยวกับการใช้วัตถุที่นี่ resultมันจะทำงานได้ดีถ้าคุณได้รับมอบหมายให้เขาตอบสนองโดยตรงกับ มันทำงานได้เพราะคุณกำลังอ่านตัวแปรหลังจากฟังก์ชั่น async เสร็จสมบูรณ์
เฟลิกซ์คลิง

85

แม้ว่าสัญญาและการโทรกลับจะทำงานได้ดีในหลาย ๆ สถานการณ์ แต่มันเป็นความเจ็บปวดที่ด้านหลัง

if (!name) {
  name = async1();
}
async2(name);

คุณจะจบลงไปผ่านasync1; ตรวจสอบnameว่าไม่ได้กำหนดหรือไม่และโทรติดต่อกลับตาม

async1(name, callback) {
  if (name)
    callback(name)
  else {
    doSomething(callback)
  }
}

async1(name, async2)

ในขณะที่มันโอเคในตัวอย่างเล็ก ๆ มันก็น่ารำคาญเมื่อคุณมีหลายกรณีที่คล้ายกันและการจัดการข้อผิดพลาดที่เกี่ยวข้อง

Fibers ช่วยในการแก้ปัญหา

var Fiber = require('fibers')

function async1(container) {
  var current = Fiber.current
  var result
  doSomething(function(name) {
    result = name
    fiber.run()
  })
  Fiber.yield()
  return result
}

Fiber(function() {
  var name
  if (!name) {
    name = async1()
  }
  async2(name)
  // Make any number of async calls from here
}

คุณสามารถเช็คเอาโครงการที่นี่


1
@recurf - ไม่ใช่โครงการของฉัน คุณสามารถลองใช้ตัวติดตามปัญหาของพวกเขาได้
rohithpr

1
นี้คล้ายกับฟังก์ชั่นเครื่องกำเนิดไฟฟ้าหรือไม่? developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
......

1
สิ่งนี้ยังเกี่ยวข้องหรือไม่
Aluan Haddad

คุณสามารถใช้ประโยชน์ได้async-awaitหากคุณใช้โหนดรุ่นใหม่ล่าสุดบางส่วน หากมีคนติดอยู่กับรุ่นเก่ากว่าพวกเขาสามารถใช้วิธีนี้
rohithpr

83

ตัวอย่างต่อไปนี้ที่ฉันเขียนแสดงให้เห็นถึงวิธีการ

  • จัดการการโทร HTTP แบบอะซิงโครนัส;
  • รอการตอบกลับจากการเรียก API แต่ละครั้ง
  • ใช้รูปแบบสัญญา
  • ใช้รูปแบบPromise.allเพื่อเข้าร่วมการโทร HTTP หลายรายการ

ตัวอย่างการทำงานนี้มีอยู่ในตัวเอง มันจะกำหนดวัตถุคำของ่าย ๆ ที่ใช้XMLHttpRequestวัตถุหน้าต่างเพื่อโทรออก มันจะกำหนดฟังก์ชั่นง่าย ๆ เพื่อรอคำสัญญาที่สมบูรณ์

บริบท. ตัวอย่างคือการสอบถามจุดสิ้นสุดSpotify Web APIเพื่อค้นหาplaylistวัตถุสำหรับชุดสตริงการสืบค้นที่กำหนด:

[
 "search?type=playlist&q=%22doom%20metal%22",
 "search?type=playlist&q=Adele"
]

สำหรับแต่ละรายการสัญญาใหม่จะทำการบล็อก - ExecutionBlockแยกวิเคราะห์ผลลัพธ์กำหนดตารางชุดสัญญาใหม่ตามอาร์เรย์ผลลัพธ์นั่นคือรายการของuserวัตถุSpotify และดำเนินการเรียก HTTP ใหม่ภายในExecutionProfileBlockแบบอะซิงโครนัส

แล้วคุณจะเห็นโครงสร้างสัญญาซ้อนกันที่ช่วยให้คุณหลายวางไข่และไม่ตรงกันสมบูรณ์ซ้อนกันโทร HTTP Promise.allและเข้าร่วมผลที่ได้จากส่วนย่อยของแต่ละสายผ่าน

หมายเหตุ Spotify searchAPIs ล่าสุดจะต้องระบุโทเค็นการเข้าถึงในส่วนหัวของคำขอ:

-H "Authorization: Bearer {your access token}" 

ดังนั้นคุณต้องรันตัวอย่างต่อไปนี้คุณต้องใส่โทเค็นการเข้าถึงของคุณในส่วนหัวของคำขอ:

var spotifyAccessToken = "YourSpotifyAccessToken";
var console = {
    log: function(s) {
        document.getElementById("console").innerHTML += s + "<br/>"
    }
}

// Simple XMLHttpRequest
// based on https://davidwalsh.name/xmlhttprequest
SimpleRequest = {
    call: function(what, response) {
        var request;
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            request = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // Internet Explorer
            try {
                request = new ActiveXObject('Msxml2.XMLHTTP');
            }
            catch (e) {
                try {
                  request = new ActiveXObject('Microsoft.XMLHTTP');
                } catch (e) {}
            }
        }

        // State changes
        request.onreadystatechange = function() {
            if (request.readyState === 4) { // Done
                if (request.status === 200) { // Complete
                    response(request.responseText)
                }
                else
                    response();
            }
        }
        request.open('GET', what, true);
        request.setRequestHeader("Authorization", "Bearer " + spotifyAccessToken);
        request.send(null);
    }
}

//PromiseAll
var promiseAll = function(items, block, done, fail) {
    var self = this;
    var promises = [],
                   index = 0;
    items.forEach(function(item) {
        promises.push(function(item, i) {
            return new Promise(function(resolve, reject) {
                if (block) {
                    block.apply(this, [item, index, resolve, reject]);
                }
            });
        }(item, ++index))
    });
    Promise.all(promises).then(function AcceptHandler(results) {
        if (done) done(results);
    }, function ErrorHandler(error) {
        if (fail) fail(error);
    });
}; //promiseAll

// LP: deferred execution block
var ExecutionBlock = function(item, index, resolve, reject) {
    var url = "https://api.spotify.com/v1/"
    url += item;
    console.log( url )
    SimpleRequest.call(url, function(result) {
        if (result) {

            var profileUrls = JSON.parse(result).playlists.items.map(function(item, index) {
                return item.owner.href;
            })
            resolve(profileUrls);
        }
        else {
            reject(new Error("call error"));
        }
    })
}

arr = [
    "search?type=playlist&q=%22doom%20metal%22",
    "search?type=playlist&q=Adele"
]

promiseAll(arr, function(item, index, resolve, reject) {
    console.log("Making request [" + index + "]")
    ExecutionBlock(item, index, resolve, reject);
}, function(results) { // Aggregated results

    console.log("All profiles received " + results.length);
    //console.log(JSON.stringify(results[0], null, 2));

    ///// promiseall again

    var ExecutionProfileBlock = function(item, index, resolve, reject) {
        SimpleRequest.call(item, function(result) {
            if (result) {
                var obj = JSON.parse(result);
                resolve({
                    name: obj.display_name,
                    followers: obj.followers.total,
                    url: obj.href
                });
            } //result
        })
    } //ExecutionProfileBlock

    promiseAll(results[0], function(item, index, resolve, reject) {
        //console.log("Making request [" + index + "] " + item)
        ExecutionProfileBlock(item, index, resolve, reject);
    }, function(results) { // aggregated results
        console.log("All response received " + results.length);
        console.log(JSON.stringify(results, null, 2));
    }

    , function(error) { // Error
        console.log(error);
    })

    /////

  },
  function(error) { // Error
      console.log(error);
  });
<div id="console" />

ฉันได้กล่าวถึงวิธีแก้ปัญหานี้อย่างกว้างขวางที่นี่ที่นี่


80

คำตอบสั้น ๆ คือคุณต้องทำการติดต่อกลับดังนี้

function callback(response) {
    // Here you can do what ever you want with the response object.
    console.log(response);
}

$.ajax({
    url: "...",
    success: callback
});

78

2017 คำตอบ: ตอนนี้คุณสามารถทำสิ่งที่คุณต้องการในทุกเบราว์เซอร์และโหนดปัจจุบัน

มันค่อนข้างง่าย:

  • ส่งคืนสัญญา
  • ใช้'รอ'ซึ่งจะบอกจาวาสคริปต์ให้รอสัญญาที่จะแก้ไขเป็นค่า (เช่นการตอบกลับ HTTP)
  • เพิ่มคำหลัก'async'ให้กับฟังก์ชันหลัก

นี่คือรหัสที่ใช้งานได้:

(async function(){

var response = await superagent.get('...')
console.log(response)

})()

การรอคอยได้รับการสนับสนุนในเบราว์เซอร์ปัจจุบันและโหนด 8 ทั้งหมด


7
น่าเสียดายที่ฟังก์ชันนี้ใช้ได้เฉพาะกับฟังก์ชันที่ส่งคืนสัญญาเช่นไม่สามารถใช้กับ Node.js API ซึ่งใช้การโทรกลับ และฉันจะไม่แนะนำให้ใช้หากไม่มี Babel เพราะทุกคนไม่ได้ใช้ "เบราว์เซอร์ปัจจุบัน"
MichałPerłakowski

2
@ MichałPerłakowskiโหนด 8 รวมถึงnodejs.org/api/util.html#util_util_promisify_originalซึ่งสามารถใช้เพื่อทำให้ node.js API ส่งคืนสัญญา ไม่ว่าคุณจะมีเวลาและเงินในการสนับสนุนเบราว์เซอร์ที่ไม่หมุนเวียนนั้นขึ้นอยู่กับสถานการณ์ของคุณ
mikemaccana

IE 11 ยังคงเป็นเบราว์เซอร์ปัจจุบันในปี 2018 น่าเศร้าและไม่รองรับawait/async
Juan Mendes

IE11 ไม่ใช่เบราว์เซอร์ปัจจุบัน เผยแพร่เมื่อ 5 ปีที่แล้วมีส่วนแบ่งการตลาดทั่วโลก 2.5% อ้างอิงจาก caniuse และหากใครบางคนกำลังเพิ่มงบประมาณของคุณเพื่อเพิกเฉยต่อเทคโนโลยีปัจจุบันทั้งหมด
mikemaccana

76

Js เป็นเธรดเดี่ยว

เบราว์เซอร์สามารถแบ่งออกเป็นสามส่วน:

1) ห่วงเหตุการณ์

2) เว็บ API

3) คิวเหตุการณ์

Event Loop จะทำงานตลอดไปเช่นชนิดของ infinite loop.Event Queue คือที่ที่ฟังก์ชันทั้งหมดของคุณถูกผลักในบางเหตุการณ์ (ตัวอย่าง: คลิก) นี่คือหนึ่งในหนึ่งที่ดำเนินการออกจากคิวและใส่เข้าไปใน Event loop ที่ดำเนินการฟังก์ชันนี้ สำหรับอันถัดไปหลังจากที่ถูกเรียกใช้ครั้งแรกนั่นหมายถึงการดำเนินการของฟังก์ชั่นหนึ่งจะไม่เริ่มต้นจนกระทั่งฟังก์ชันก่อนหน้าที่จะอยู่ในคิวจะถูกดำเนินการในเหตุการณ์ลูป

ตอนนี้ให้เราคิดว่าเราผลักสองฟังก์ชันในคิวหนึ่งเพื่อรับข้อมูลจากเซิร์ฟเวอร์และอีกอันใช้ข้อมูลนั้นเราผลักฟังก์ชัน serverRequest () ในคิวก่อนจากนั้นใช้ฟังก์ชัน utiliseData () ฟังก์ชั่น serverRequest ไปในวนรอบเหตุการณ์และทำการเรียกไปยังเซิร์ฟเวอร์เนื่องจากเราไม่เคยรู้ว่าจะต้องใช้เวลานานเท่าใดในการรับข้อมูลจากเซิร์ฟเวอร์ดังนั้นกระบวนการนี้คาดว่าจะใช้เวลาดังนั้นเราจึงยุ่งห่วงเหตุการณ์ของเราจึงแขวนหน้าเว็บของเรา API มีหน้าที่รับฟังก์ชั่นนี้จาก event loop และจัดการกับเซิร์ฟเวอร์ทำให้ loop event ฟรีเพื่อให้เราสามารถเรียกใช้ฟังก์ชันถัดไปจากคิวได้ฟังก์ชันถัดไปในคิวคือ utiliseData () ซึ่งไปในลูป แต่เนื่องจากไม่มีข้อมูลเลย การสูญเสียและการดำเนินการของฟังก์ชั่นต่อไปจะดำเนินต่อไปจนถึงจุดสิ้นสุดของคิว (ซึ่งเรียกว่าการโทรแบบ Async คือเราสามารถทำอย่างอื่นจนกว่าเราจะได้รับข้อมูล)

ให้สมมติว่าฟังก์ชั่น serverRequest () ของเรามีคำสั่ง return ในโค้ดเมื่อเรารับข้อมูลจากเซิร์ฟเวอร์ Web API จะส่งมันในคิวเมื่อสิ้นสุดคิว เนื่องจากมันถูกส่งไปที่จุดสิ้นสุดของคิวเราจึงไม่สามารถใช้ข้อมูลได้เนื่องจากไม่มีฟังก์ชั่นเหลืออยู่ในคิวของเราที่จะใช้ข้อมูลนี้ ดังนั้นจึงไม่สามารถส่งคืนบางสิ่งจาก Async Call

ดังนั้นวิธีแก้ปัญหานี้คือการเรียกกลับหรือสัญญา

ภาพลักษณ์จากหนึ่งในคำตอบที่นี่ได้อย่างถูกต้องอธิบายการใช้โทรกลับ ... เราให้ฟังก์ชั่นของเรา (ฟังก์ชั่นการใช้ข้อมูลที่ส่งกลับจากเซิร์ฟเวอร์) ไปยังเซิร์ฟเวอร์ฟังก์ชั่นการโทร

โทรกลับ

 function doAjax(callbackFunc, method, url) {
  var xmlHttpReq = new XMLHttpRequest();
  xmlHttpReq.open(method, url);
  xmlHttpReq.onreadystatechange = function() {

      if (xmlHttpReq.readyState == 4 && xmlHttpReq.status == 200) {
        callbackFunc(xmlHttpReq.responseText);
      }


  }
  xmlHttpReq.send(null);

}

ในรหัสของฉันมันถูกเรียกว่าเป็น

function loadMyJson(categoryValue){
  if(categoryValue==="veg")
  doAjax(print,"GET","http://localhost:3004/vegetables");
  else if(categoryValue==="fruits")
  doAjax(print,"GET","http://localhost:3004/fruits");
  else 
  console.log("Data not found");
}

Javscript.info โทรกลับ


68

คุณสามารถใช้ไลบรารีแบบกำหนดเองนี้ (เขียนโดยใช้ Promise) เพื่อโทรระยะไกล

function $http(apiConfig) {
    return new Promise(function (resolve, reject) {
        var client = new XMLHttpRequest();
        client.open(apiConfig.method, apiConfig.url);
        client.send();
        client.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                // Performs the function "resolve" when this.status is equal to 2xx.
                // Your logic here.
                resolve(this.response);
            }
            else {
                // Performs the function "reject" when this.status is different than 2xx.
                reject(this.statusText);
            }
        };
        client.onerror = function () {
            reject(this.statusText);
        };
    });
}

ตัวอย่างการใช้งานง่าย:

$http({
    method: 'get',
    url: 'google.com'
}).then(function(response) {
    console.log(response);
}, function(error) {
    console.log(error)
});

67

อีกวิธีการหนึ่งคือการเรียกใช้งานรหัสผ่านทางผู้ปฏิบัติการตามลำดับnsynjs nsynjs

ถ้าฟังก์ชั่นพื้นฐานมีการแนะนำ

nsynjs จะประเมินสัญญาทั้งหมดตามลำดับและนำผลลัพธ์สัญญาไปไว้ในdataอสังหาริมทรัพย์:

function synchronousCode() {

    var getURL = function(url) {
        return window.fetch(url).data.text().data;
    };
    
    var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
    console.log('received bytes:',getURL(url).length);
    
};

nsynjs.run(synchronousCode,{},function(){
    console.log('synchronousCode done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

หากฟังก์ชั่นพื้นฐานไม่ได้รับการแนะนำ

ขั้นตอนที่ 1 ตัดฟังก์ชั่นด้วยการโทรกลับเข้าไปใน wrapper ที่รับรู้ nsynjs (หากมีเวอร์ชั่นที่ได้รับการรับรองคุณสามารถข้ามขั้นตอนนี้ได้):

var ajaxGet = function (ctx,url) {
    var res = {};
    var ex;
    $.ajax(url)
    .done(function (data) {
        res.data = data;
    })
    .fail(function(e) {
        ex = e;
    })
    .always(function() {
        ctx.resume(ex);
    });
    return res;
};
ajaxGet.nsynjsHasCallback = true;

ขั้นตอนที่ 2 ใส่ตรรกะซิงโครนัสในฟังก์ชัน:

function process() {
    console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}

ขั้นตอน 3. เรียกใช้ฟังก์ชันในลักษณะซิงโครนัสผ่าน nsynjs:

nsynjs.run(process,this,function () {
    console.log("synchronous function finished");
});

Nsynjs จะประเมินตัวดำเนินการและนิพจน์ทั้งหมดทีละขั้นตอนหยุดการทำงานชั่วคราวในกรณีที่ผลลัพธ์ของฟังก์ชันที่ช้าบางอย่างยังไม่พร้อม

ตัวอย่างเพิ่มเติมได้ที่นี่: https://github.com/amaksr/nsynjs/tree/master/examples


2
สิ่งนี้น่าสนใจ ฉันชอบวิธีที่อนุญาตให้ใช้รหัส async ในแบบที่คุณต้องการในภาษาอื่น แต่ในทางเทคนิคแล้วมันไม่ได้เป็นจาวาสคริปต์จริงเหรอ?
J Morris

41

ECMAScript 6 มี 'เครื่องกำเนิดไฟฟ้า' ที่ให้คุณตั้งโปรแกรมได้อย่างง่ายดายในสไตล์อะซิงโครนัส

function* myGenerator() {
    const callback = yield;
    let [response] = yield $.ajax("https://stackoverflow.com", {complete: callback});
    console.log("response is:", response);

    // examples of other things you can do
    yield setTimeout(callback, 1000);
    console.log("it delayed for 1000ms");
    while (response.statusText === "error") {
        [response] = yield* anotherGenerator();
    }
}

ในการใช้งานโค้ดด้านบนให้ทำดังนี้

const gen = myGenerator(); // Create generator
gen.next(); // Start it
gen.next((...args) => gen.next([...args])); // Set its callback function

หากคุณต้องการกำหนดเป้าหมายเบราว์เซอร์ที่ไม่รองรับ ES6 คุณสามารถเรียกใช้รหัสผ่าน Babel หรือ closure-compiler เพื่อสร้าง ECMAScript 5

การเรียกกลับ...argsถูกห่อในอาร์เรย์และถูกทำลายเมื่อคุณอ่านเพื่อให้รูปแบบสามารถรับมือกับการเรียกกลับที่มีหลายอาร์กิวเมนต์ ตัวอย่างเช่นกับnode fs :

const [err, data] = yield fs.readFile(filePath, "utf-8", callback);

39

นี่คือวิธีการบางอย่างในการทำงานกับคำขอแบบอะซิงโครนัส:

  1. เบราว์เซอร์วัตถุสัญญา
  2. Q - ห้องสมุดสัญญาสำหรับ JavaScript
  3. A + Promises.js
  4. jQuery รอการตัดบัญชี
  5. XMLHttpRequest API
  6. การใช้แนวคิดการติดต่อกลับ - เป็นการใช้งานในคำตอบแรก

ตัวอย่าง: jQuery เลื่อนการใช้งานเพื่อทำงานกับคำขอหลายรายการ

var App = App || {};

App = {
    getDataFromServer: function(){

      var self = this,
                 deferred = $.Deferred(),
                 requests = [];

      requests.push($.getJSON('request/ajax/url/1'));
      requests.push($.getJSON('request/ajax/url/2'));

      $.when.apply(jQuery, requests).done(function(xhrResponse) {
        return deferred.resolve(xhrResponse.result);
      });
      return deferred;
    },

    init: function(){

        this.getDataFromServer().done(_.bind(function(resp1, resp2) {

           // Do the operations which you wanted to do when you
           // get a response from Ajax, for example, log response.
        }, this));
    }
};
App.init();


38

เราพบว่าตัวเองอยู่ในจักรวาลที่ดูเหมือนจะก้าวหน้าไปในมิติที่เราเรียกว่า "เวลา" เราไม่เข้าใจว่าเวลาคืออะไร แต่เราได้พัฒนาบทคัดย่อและคำศัพท์ที่ให้เรามีเหตุผลและพูดคุยเกี่ยวกับมัน: "อดีต", "ปัจจุบัน", "อนาคต", "อนาคต", "ก่อน", "หลัง"

ระบบคอมพิวเตอร์ที่เราสร้างขึ้น - มากขึ้น - มีเวลาเป็นมิติที่สำคัญ มีบางสิ่งที่จะเกิดขึ้นในอนาคต จากนั้นสิ่งอื่น ๆ จะต้องเกิดขึ้นหลังจากสิ่งแรกที่เกิดขึ้นในที่สุด นี่คือแนวคิดพื้นฐานที่เรียกว่า "asynchronicity" ในโลกที่มีเครือข่ายของเรามากขึ้นกรณีที่พบบ่อยที่สุดของ asynchronicity กำลังรอให้ระบบรีโมตบางตัวตอบสนองต่อคำร้องขอบางอย่าง

ลองพิจารณาตัวอย่าง คุณโทรหาคนส่งนมและสั่งซื้อนม เมื่อมาถึงคุณต้องการใส่ในกาแฟของคุณ คุณไม่สามารถใส่นมลงในกาแฟของคุณได้ในตอนนี้เพราะมันยังไม่อยู่ที่นี่ คุณต้องรอให้มันมาก่อนวางลงในกาแฟของคุณ ในคำอื่น ๆ ต่อไปนี้จะไม่ทำงาน:

var milk = order_milk();
put_in_coffee(milk);

เพราะ JS มีวิธีการที่จะรู้ว่ามันต้องไม่รอสำหรับการให้เสร็จก่อนที่จะดำเนินการorder_milk put_in_coffeeกล่าวอีกนัยหนึ่งก็ไม่ทราบว่าorder_milkเป็นแบบอะซิงโครนัส - เป็นสิ่งที่จะไม่ส่งผลให้นมจนกว่าจะถึงเวลาในอนาคต JS และภาษาที่มีการประกาศอื่นใช้คำสั่งเดียวหลังจากที่อื่นโดยไม่ต้องรอ

วิธีการ JS แบบคลาสสิกสำหรับปัญหานี้การใช้ประโยชน์จากข้อเท็จจริงที่ว่า JS สนับสนุนฟังก์ชั่นเป็นวัตถุชั้นหนึ่งที่สามารถส่งผ่านได้คือการส่งผ่านฟังก์ชันเป็นพารามิเตอร์ไปยังคำขอแบบอะซิงโครนัสซึ่งจะเรียกใช้เมื่อเสร็จสิ้น งานของมันบางครั้งในอนาคต นั่นคือวิธีการ "โทรกลับ" ดูเหมือนว่านี้:

order_milk(put_in_coffee);

order_milkkicks put_in_coffeeปิดคำสั่งซื้อนมแล้วเมื่อและเมื่อมันมาถึงมันจะเรียก

ปัญหาเกี่ยวกับวิธีการติดต่อกลับนี้คือมันก่อให้เกิดมลพิษความหมายปกติของฟังก์ชั่นการรายงานผลของมันด้วยreturn; ฟังก์ชั่นจะต้องไม่รายงานผลลัพธ์ของตนโดยการโทรกลับที่ได้รับเป็นพารามิเตอร์ นอกจากนี้วิธีการนี้อาจกลายเป็นความไม่สะดวกอย่างรวดเร็วเมื่อต้องรับมือกับลำดับเหตุการณ์ที่ยาวขึ้น ตัวอย่างเช่นสมมติว่าฉันต้องการรอให้นมใส่ในกาแฟและจากนั้นทำเพียงขั้นตอนที่สามคือการดื่มกาแฟ ท้ายที่สุดฉันต้องการเขียนแบบนี้:

order_milk(function(milk) { put_in_coffee(milk, drink_coffee); }

ที่ฉันกำลังส่งผ่านไปput_in_coffeeยังนมทั้งสองเพื่อใส่ในและการกระทำ ( drink_coffee) เพื่อดำเนินการเมื่อนมได้รับการใส่รหัสดังกล่าวกลายเป็นเรื่องยากที่จะเขียนและอ่านและการแก้ปัญหา

ในกรณีนี้เราสามารถเขียนรหัสใหม่ในคำถามดังนี้

var answer;
$.ajax('/foo.json') . done(function(response) {
  callback(response.data);
});

function callback(data) {
  console.log(data);
}

ป้อนสัญญา

นี่คือแรงจูงใจสำหรับความคิดของ "สัญญา" ซึ่งเป็นประเภทเฉพาะของค่าซึ่งแสดงถึงอนาคตหรือผลลัพธ์แบบอะซิงโครนัสของบางประเภท มันสามารถแสดงสิ่งที่เกิดขึ้นแล้วหรือที่จะเกิดขึ้นในอนาคตหรืออาจไม่เกิดขึ้นเลย สัญญามีวิธีการเดียวชื่อthenซึ่งคุณผ่านการกระทำที่จะดำเนินการเมื่อผลลัพธ์ที่สัญญาหมายถึงได้รับการรับรู้

ในกรณีของนมและกาแฟของเราเราออกแบบorder_milkเพื่อคืนสัญญาสำหรับนมที่มาถึงแล้วระบุput_in_coffeeว่าเป็นการthenกระทำดังต่อไปนี้:

order_milk() . then(put_in_coffee)

ข้อดีอย่างหนึ่งของสิ่งนี้คือเราสามารถรวมสิ่งเหล่านี้เข้าด้วยกันเพื่อสร้างลำดับเหตุการณ์ที่เกิดขึ้นในอนาคต ("การผูกมัด"):

order_milk() . then(put_in_coffee) . then(drink_coffee)

มาใช้สัญญากับปัญหาของคุณโดยเฉพาะ เราจะห่อตรรกะคำขอของเราไว้ในฟังก์ชั่นซึ่งส่งคืนสัญญา:

function get_data() {
  return $.ajax('/foo.json');
}

ที่จริงแล้วทุกสิ่งที่เราได้ทำคือการเพิ่มการเรียกร้องให้return $.ajaxใช้งานได้เพราะ jQuery $.ajaxส่งคืนสัญญาที่มีลักษณะเหมือนสัญญาอยู่แล้ว (ในทางปฏิบัติโดยไม่ได้รับรายละเอียดเราต้องการปิดการโทรนี้เพื่อคืนสัญญาที่แท้จริงหรือใช้ทางเลือกอื่นในการ$.ajaxทำเช่นนั้น) ตอนนี้ถ้าเราต้องการโหลดไฟล์และรอให้เสร็จและ จากนั้นทำอะไรเราก็สามารถพูดได้

get_data() . then(do_something)

เช่น

get_data() . 
  then(function(data) { console.log(data); });

เมื่อใช้คำมั่นสัญญาเราได้ผ่านฟังก์ชั่นมากมายเข้ามาthenดังนั้นจึงมักจะเป็นประโยชน์ในการใช้ฟังก์ชั่นลูกศรสไตล์ ES6 ที่กะทัดรัดมากขึ้น:

get_data() . 
  then(data => console.log(data));

asyncคำหลัก

แต่ก็ยังมีบางสิ่งที่ไม่พอใจอย่างชัดเจนเกี่ยวกับการเขียนโค้ดทางเดียวถ้าแบบซิงโครนัสและวิธีที่แตกต่างกันมากถ้าแบบอะซิงโครนัส สำหรับซิงโครนัสเราเขียน

a();
b();

แต่ถ้าaเป็นแบบอะซิงโครนัสเราสัญญาว่าจะต้องเขียน

a() . then(b);

ด้านบนเราพูดว่า "JS ไม่มีทางรู้ว่าต้องรอให้สายแรกเสร็จก่อนที่จะดำเนินการครั้งที่สอง" มันจะไม่ดีถ้ามีเป็นวิธีที่จะบอกว่าบาง JS? ปรากฎว่ามี - awaitคำหลักที่ใช้ภายในฟังก์ชั่นพิเศษชนิดที่เรียกว่าฟังก์ชั่น "async" ฟีเจอร์นี้เป็นส่วนหนึ่งของ ES เวอร์ชั่นที่กำลังจะมา แต่มีอยู่ใน transpilers เช่น Babel ที่ได้รับการตั้งค่าไว้ล่วงหน้าแล้ว สิ่งนี้ทำให้เราสามารถเขียน

async function morning_routine() {
  var milk   = await order_milk();
  var coffee = await put_in_coffee(milk);
  await drink(coffee);
}

ในกรณีของคุณคุณจะสามารถเขียนสิ่งที่ชอบ

async function foo() {
  data = await get_data();
  console.log(data);
}

37

คำตอบสั้น ๆ : foo()วิธีการของคุณจะกลับมาทันทีในขณะที่การ$ajax()เรียกใช้งานแบบอะซิงโครนัสหลังจากฟังก์ชั่นกลับมา ปัญหาคือวิธีที่จะเก็บผลลัพธ์ที่ได้จากการโทรแบบ async เมื่อมันกลับมา

มีการแก้ปัญหาหลายอย่างในเธรดนี้ บางทีวิธีที่ง่ายที่สุดคือการส่งวัตถุไปยังfoo()วิธีการและเพื่อเก็บผลลัพธ์ไว้ในสมาชิกของวัตถุนั้นหลังจากการเรียก async เสร็จสมบูรณ์

function foo(result) {
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;   // Store the async result
        }
    });
}

var result = { response: null };   // Object to hold the async result
foo(result);                       // Returns before the async completes

โปรดทราบว่าการโทรไปfoo()ยังจะไม่มีประโยชน์ใด ๆ อย่างไรก็ตามผลของการโทร async result.responseที่จะถูกเก็บไว้ใน


14
แม้ว่าจะได้ผล แต่ก็ไม่ได้ดีไปกว่าการกำหนดให้กับตัวแปรทั่วโลก
Felix Kling

36

ใช้callback()ฟังก์ชันภายในfoo()ความสำเร็จ ลองด้วยวิธีนี้ มันง่ายและเข้าใจง่าย  

var lat = "";
var lon = "";
function callback(data) {
    lat = data.lat;
    lon = data.lon;
}
function getLoc() {
    var url = "http://ip-api.com/json"
    $.getJSON(url, function(data) {
        callback(data);
    });
}

getLoc();

29

คำถามคือ:

ฉันจะคืนการตอบสนองจากการโทรแบบอะซิงโครนัสได้อย่างไร

ซึ่งสามารถตีความได้ว่า:

วิธีที่จะทำให้ไม่ตรงกันรหัสดูซิงโคร ?

วิธีการแก้ปัญหาคือเพื่อหลีกเลี่ยงการโทรกลับและใช้การรวมกันของสัญญาและasync / รอ async

ฉันต้องการยกตัวอย่างสำหรับคำขอ Ajax

(ถึงแม้ว่ามันจะสามารถเขียนใน Javascript แต่ฉันชอบที่จะเขียนใน Python และรวบรวมไปยัง Javascript โดยใช้Transcryptมันจะชัดเจนพอ)

ก่อนอื่นให้เปิดใช้งานการใช้ JQuery เพื่อ$ให้สามารถใช้งานได้เป็นS:

__pragma__ ('alias', 'S', '$')

กำหนดฟังก์ชั่นที่ส่งคืนสัญญาในกรณีนี้คือการเรียก Ajax:

def read(url: str):
    deferred = S.Deferred()
    S.ajax({'type': "POST", 'url': url, 'data': { },
        'success': lambda d: deferred.resolve(d),
        'error': lambda e: deferred.reject(e)
    })
    return deferred.promise()

ใช้รหัสอะซิงโครนัสราวกับว่ามันเป็นซิงโครนัส :

async def readALot():
    try:
        result1 = await read("url_1")
        result2 = await read("url_2")
    except Exception:
        console.warn("Reading a lot failed")

29

ใช้สัญญา

Promiseคำตอบที่สมบูรณ์แบบที่สุดสำหรับคำถามนี้คือการใช้

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

การใช้

ajax("GET", "/test", "acrive=1").then(function(result) {
    // Code depending on result
})
.catch(function() {
    // An error occurred
});

แต่เดี๋ยวก่อน...!

มีปัญหากับการใช้สัญญา!

ทำไมเราควรใช้สัญญาที่กำหนดเองของเราเอง

ฉันใช้วิธีนี้ไปสักพักจนฉันพบว่ามีข้อผิดพลาดในเบราว์เซอร์รุ่นเก่า:

Uncaught ReferenceError: Promise is not defined

ดังนั้นฉันตัดสินใจที่จะใช้คลาส Promise ของฉันเองสำหรับES3 เพื่อคอมไพเลอร์ js ที่ต่ำกว่าหากไม่ได้กำหนดไว้ เพียงเพิ่มรหัสนี้ก่อนรหัสหลักของคุณแล้วใช้สัญญาอย่างปลอดภัย!

if(typeof Promise === "undefined"){
    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) { 
            throw new TypeError("Cannot call a class as a function"); 
        }
    }
    var Promise = function () {
        function Promise(main) {
            var _this = this;
            _classCallCheck(this, Promise);
            this.value = undefined;
            this.callbacks = [];
            var resolve = function resolve(resolveValue) {
                _this.value = resolveValue;
                _this.triggerCallbacks();
            };
            var reject = function reject(rejectValue) {
                _this.value = rejectValue;
                _this.triggerCallbacks();
            };
            main(resolve, reject);
        }
        Promise.prototype.then = function then(cb) {
            var _this2 = this;
            var next = new Promise(function (resolve) {
                _this2.callbacks.push(function (x) {
                    return resolve(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.catch = function catch_(cb) {
            var _this2 = this;
            var next = new Promise(function (reject) {
                _this2.callbacks.push(function (x) {
                    return reject(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.triggerCallbacks = function triggerCallbacks() {
            var _this3 = this;
            this.callbacks.forEach(function (cb) {
                cb(_this3.value);
            });
        };
        return Promise;
    }();
}

28

แน่นอนว่ามีหลายวิธีเช่นคำขอแบบซิงโครนัสสัญญา แต่จากประสบการณ์ของฉันฉันคิดว่าคุณควรใช้วิธีการโทรกลับ เป็นเรื่องปกติที่พฤติกรรมแบบอะซิงโครนัสของ Javascript ดังนั้นข้อมูลโค้ดของคุณสามารถเขียนได้แตกต่างกันเล็กน้อย:

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            myCallback(response);
        }
    });

    return result;
}

function myCallback(response) {
    // Does something.
}

5
ไม่มีอะไรแบบอะซิงโครนัสเกี่ยวกับการเรียกกลับหรือ JavaScript
Aluan Haddad

19

แทนที่จะขว้างรหัสที่คุณมี 2 แนวคิดที่สำคัญในการทำความเข้าใจวิธีการที่ JS จัดการกับการเรียกกลับและ asynchronicity (นั่นคือแม้แต่คำ?)

Event Loop และ Model Concurrency

มีสามสิ่งที่คุณต้องระวัง คิว เหตุการณ์ห่วงและสแต็ค

ในแง่ง่ายกว้างวงเหตุการณ์เป็นเหมือนผู้จัดการโครงการมันกำลังฟังอย่างต่อเนื่องสำหรับฟังก์ชั่นใด ๆ ที่ต้องการเรียกใช้และสื่อสารระหว่างคิวและสแต็ค

while (queue.waitForMessage()) {
   queue.processNextMessage();
}

เมื่อได้รับข้อความให้เรียกใช้บางสิ่งมันจะเพิ่มลงในคิว คิวคือรายการสิ่งต่าง ๆ ที่กำลังรอการดำเนินการ (เช่นคำขอ AJAX ของคุณ) ลองนึกภาพเช่นนี้:

 1. call foo.com/api/bar using foobarFunc
 2. Go perform an infinite loop
 ... and so on

เมื่อหนึ่งในข้อความเหล่านี้กำลังดำเนินการจะปรากฏข้อความจากคิวและสร้างสแต็คสแต็กคือทุกสิ่งที่ JS ต้องการดำเนินการเพื่อดำเนินการคำสั่งในข้อความ ในตัวอย่างของเรามันถูกบอกให้โทรfoobarFunc

function foobarFunc (var) {
  console.log(anotherFunction(var));
}

ดังนั้นสิ่งที่ foobarFunc จำเป็นต้องดำเนินการ (ในกรณีของเราanotherFunction) จะถูกส่งไปยังสแต็ก ดำเนินการแล้วลืม - ห่วงเหตุการณ์จะย้ายไปยังสิ่งต่อไปในคิว (หรือฟังข้อความ)

สิ่งสำคัญที่นี่คือลำดับของการดำเนินการ นั่นคือ

เมื่อสิ่งที่จะทำงาน

เมื่อคุณโทรออกโดยใช้ AJAX ให้กับบุคคลภายนอกหรือเรียกใช้โค้ดอะซิงโครนัส (ตัวอย่างเช่น setTimeout) Javascript จะขึ้นอยู่กับการตอบกลับก่อนจึงจะสามารถดำเนินการต่อได้

คำถามใหญ่คือเมื่อไหร่จะได้รับการตอบสนอง คำตอบคือเราไม่รู้ - ดังนั้นวงเหตุการณ์กำลังรอให้ข้อความนั้นพูดว่า "เฮ้ฉันหน่อย" หาก JS รอข้อความนั้นพร้อม ๆ กันแอปของคุณจะค้างและมันจะดูด ดังนั้น JS ดำเนินการรายการต่อไปในคิวในขณะที่รอข้อความเพื่อเพิ่มกลับไปที่คิว

นั่นเป็นเหตุผลที่มีการทำงานไม่ตรงกันเราจะใช้สิ่งที่เรียกว่าการเรียกกลับ มันค่อนข้างเหมือนสัญญาเลยทีเดียว ในขณะที่ฉันสัญญาว่าจะส่งคืนบางสิ่งในบางจุด jQuery ใช้การเรียกกลับเฉพาะที่เรียกว่าdeffered.done deffered.failและdeffered.always(อื่น ๆ ) คุณสามารถเห็นพวกเขาทั้งหมดได้ที่นี่

ดังนั้นสิ่งที่คุณต้องทำคือส่งผ่านฟังก์ชั่นที่สัญญาว่าจะดำเนินการในบางจุดด้วยข้อมูลที่ส่งผ่านไป

เนื่องจากการโทรกลับไม่ได้ดำเนินการทันที แต่ในเวลาต่อมาสิ่งสำคัญคือต้องส่งการอ้างอิงไปยังฟังก์ชันที่ไม่ได้ดำเนินการ ดังนั้น

function foo(bla) {
  console.log(bla)
}

ดังนั้นเวลาส่วนใหญ่ (แต่ไม่เสมอไป) คุณจะfooไม่ผ่านfoo()

หวังว่าจะมีเหตุผล เมื่อคุณพบสิ่งต่าง ๆ เช่นนี้ซึ่งดูสับสน - ฉันขอแนะนำให้อ่านเอกสารอย่างเต็มที่เพื่อให้เข้าใจอย่างน้อยที่สุด มันจะทำให้คุณเป็นนักพัฒนาที่ดีขึ้นมาก


18

การใช้ ES2017 คุณควรใช้สิ่งนี้เป็นการประกาศฟังก์ชัน

async function foo() {
    var response = await $.ajax({url: '...'})
    return response;
}

และดำเนินการเช่นนี้

(async function() {
    try {
        var result = await foo()
        console.log(result)
    } catch (e) {}
})()

หรือไวยากรณ์สัญญา

foo().then(response => {
    console.log(response)

}).catch(error => {
    console.log(error)

})

ฟังก์ชั่นที่สองนั้นสามารถนำมาใช้ซ้ำได้หรือไม่?
Zum Dummi

คุณจะใช้ผลลัพธ์ได้อย่างไรถ้า oncolse มีการเรียก log ไม่ใช่ทุกอย่างที่ไปที่คอนโซล ณ จุดนั้นใช่ไหม
Ken Ingram
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.