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