กลับมาที่ปัญหาที่กล่าวถึงในบทนำ: การเรียกกลับ: เรามีลำดับของงานอะซิงโครนัสที่จะดำเนินการทีละงาน เช่น การโหลดสคริปต์ เราจะเขียนโค้ดให้ดีได้อย่างไร?
สัญญามีสูตรสองสามสูตรในการทำเช่นนั้น
ในบทนี้เราจะกล่าวถึงการผูกมัดสัญญา
ดูเหมือนว่านี้:
สัญญาใหม่ (ฟังก์ชั่น (แก้ไข, ปฏิเสธ) { setTimeout(() => แก้ไข (1), 1,000); - }).แล้ว(ฟังก์ชั่น(ผลลัพธ์) { // (**) การแจ้งเตือน (ผลลัพธ์); // 1 ส่งคืนผลลัพธ์ * 2; }).แล้ว(ฟังก์ชั่น(ผลลัพธ์) { // (***) การแจ้งเตือน (ผลลัพธ์); // 2 ส่งคืนผลลัพธ์ * 2; }).แล้ว(ฟังก์ชั่น(ผลลัพธ์) { การแจ้งเตือน (ผลลัพธ์); // 4 ส่งคืนผลลัพธ์ * 2; -
แนวคิดก็คือผลลัพธ์จะถูกส่งผ่านสายโซ่ของตัวจัดการ . .then
นี่คือการไหล:
สัญญาเริ่มต้นจะได้รับการแก้ไขใน 1 วินาที (*)
จากนั้นตัวจัดการ .then
จะถูกเรียก (**)
ซึ่งจะสร้างสัญญาใหม่ (แก้ไขด้วยค่า 2
)
ถัด then
(***)
จะได้ผลลัพธ์ของอันก่อนหน้า ประมวลผล (สองเท่า) และส่งผ่านไปยังตัวจัดการถัดไป
…และอื่นๆ
เมื่อผลลัพธ์ถูกส่งไปตามสายโซ่ของตัวจัดการ เราจะเห็นลำดับของการโทร alert
: 1
→ 2
→ 4
ทุกอย่างได้ผล เพราะทุกครั้งที่โทรไปที่ .then
จะส่งคืนสัญญาใหม่ เพื่อให้เราสามารถโทรไปยัง .then
ครั้งถัดไปได้
เมื่อตัวจัดการส่งคืนค่า มันจะกลายเป็นผลลัพธ์ของสัญญานั้น ดังนั้น . .then
ถัดไปจะถูกเรียกพร้อมกับมัน
ข้อผิดพลาดของมือใหม่แบบคลาสสิก: ในทางเทคนิคแล้ว เรายังสามารถเพิ่ม .then
จำนวนมากลงในสัญญาเดียวได้ นี่ไม่ใช่การผูกมัด
ตัวอย่างเช่น:
ให้สัญญา = สัญญาใหม่ (ฟังก์ชั่น (แก้ไข, ปฏิเสธ) { setTimeout(() => แก้ไข (1), 1,000); - สัญญาแล้ว (ฟังก์ชั่น (ผลลัพธ์) { การแจ้งเตือน (ผลลัพธ์); // 1 ส่งคืนผลลัพธ์ * 2; - สัญญาแล้ว (ฟังก์ชั่น (ผลลัพธ์) { การแจ้งเตือน (ผลลัพธ์); // 1 ส่งคืนผลลัพธ์ * 2; - สัญญาแล้ว (ฟังก์ชั่น (ผลลัพธ์) { การแจ้งเตือน (ผลลัพธ์); // 1 ส่งคืนผลลัพธ์ * 2; -
สิ่งที่เราทำที่นี่คือการเพิ่มตัวจัดการหลายรายในสัญญาเดียว พวกเขาไม่ส่งต่อผลลัพธ์ให้กัน แต่พวกเขาดำเนินการอย่างอิสระแทน
นี่คือภาพ (เปรียบเทียบกับการผูกมัดด้านบน):
ทั้งหมด .then
ในคำสัญญาเดียวกันจะได้รับผลลัพธ์เดียวกัน – ผลลัพธ์ของคำสัญญานั้น ดังนั้นในโค้ดด้านบน alert
ทั้งหมดจะแสดงเหมือนกัน: 1
ในทางปฏิบัติเราแทบจะไม่ต้องการตัวจัดการหลายตัวสำหรับสัญญาเดียว การผูกมัดใช้บ่อยกว่ามาก
ตัวจัดการที่ใช้ใน .then(handler)
อาจสร้างและส่งคืนสัญญา
ในกรณีนั้นตัวจัดการเพิ่มเติมจะรอจนกว่าจะชำระตัว และรับผลลัพธ์
ตัวอย่างเช่น:
สัญญาใหม่ (ฟังก์ชั่น (แก้ไข, ปฏิเสธ) { setTimeout(() => แก้ไข (1), 1,000); }).แล้ว(ฟังก์ชั่น(ผลลัพธ์) { การแจ้งเตือน (ผลลัพธ์); // 1 คืนสัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { // (*) setTimeout(() => แก้ไข (ผลลัพธ์ * 2), 1,000); - }).แล้ว(ฟังก์ชั่น(ผลลัพธ์) { // (**) การแจ้งเตือน (ผลลัพธ์); // 2 คืนสัญญาใหม่ ((แก้ไข, ปฏิเสธ) => { setTimeout(() => แก้ไข (ผลลัพธ์ * 2), 1,000); - }).แล้ว(ฟังก์ชั่น(ผลลัพธ์) { การแจ้งเตือน (ผลลัพธ์); // 4 -
ที่นี่อันแรก .then
จะแสดง 1
และส่งคืน new Promise(…)
ในบรรทัด (*)
หลังจากผ่านไปหนึ่งวินาทีก็จะแก้ไข และผลลัพธ์ (อาร์กิวเมนต์ของ resolve
ในที่นี้ก็คือ result * 2
) จะถูกส่งต่อไปยังตัวจัดการของวินาที . .then
ตัวจัดการนั้นอยู่ในบรรทัด (**)
มันแสดง 2
และทำสิ่งเดียวกัน
ดังนั้นเอาต์พุตจะเหมือนกับในตัวอย่างก่อนหน้า: 1 → 2 → 4 แต่ตอนนี้มีความล่าช้า 1 วินาทีระหว่างการโทร alert
การส่งคืนสัญญาทำให้เราสามารถสร้างห่วงโซ่ของการดำเนินการแบบอะซิงโครนัสได้
ลองใช้คุณสมบัตินี้กับ Promisified loadScript
ที่กำหนดไว้ในบทที่แล้ว เพื่อโหลดสคริปต์ทีละตัว ตามลำดับ:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(ฟังก์ชั่น(สคริปต์) { กลับ loadScript("https://javascript.info/article/promise-chaining/two.js"); - .then(ฟังก์ชั่น(สคริปต์) { กลับ loadScript("https://javascript.info/article/promise-chaining/three.js"); - .then(ฟังก์ชั่น(สคริปต์) { // ใช้ฟังก์ชันที่ประกาศไว้ในสคริปต์ // เพื่อแสดงว่าโหลดแล้วจริงๆ หนึ่ง(); สอง(); สาม(); -
รหัสนี้สามารถทำให้สั้นลงได้เล็กน้อยด้วยฟังก์ชันลูกศร:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/three.js")) .แล้ว(สคริปต์ => { // โหลดสคริปต์แล้ว เราสามารถใช้ฟังก์ชันที่ประกาศไว้ที่นั่นได้ หนึ่ง(); สอง(); สาม(); -
ที่นี่การเรียก loadScript
แต่ละครั้งจะส่งคืนสัญญา และ .then
ถัดไปจะทำงานเมื่อได้รับการแก้ไข จากนั้นจะเริ่มโหลดสคริปต์ถัดไป ดังนั้นสคริปต์จึงถูกโหลดทีละอัน
เราสามารถเพิ่มการดำเนินการแบบอะซิงโครนัสเพิ่มเติมให้กับเชนได้ โปรดทราบว่าโค้ดยังคงเป็น "คงที่" — โดยจะขยายลงมา ไม่ใช่อยู่ทางด้านขวา ไม่มีร่องรอยของ "ปิรามิดแห่งความหายนะ"
ในทางเทคนิคแล้ว เราสามารถเพิ่ม .then
ลงในแต่ละ loadScript
ได้โดยตรง เช่นนี้
loadScript("https://javascript.info/article/promise-chaining/one.js").แล้ว(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").แล้ว(script2 => { loadScript("https://javascript.info/article/promise-chaining/three.js").แล้ว(script3 => { // ฟังก์ชั่นนี้สามารถเข้าถึงตัวแปร script1, script2 และ script3 หนึ่ง(); สอง(); สาม(); - - -
รหัสนี้ทำเช่นเดียวกัน: โหลด 3 สคริปต์ตามลำดับ แต่มัน "เติบโตไปทางขวา" ดังนั้นเราจึงมีปัญหาเดียวกันกับการโทรกลับ
คนที่เริ่มใช้คำสัญญาบางครั้งไม่รู้เรื่องการผูกมัด จึงเขียนไว้แบบนี้ โดยทั่วไปแล้ว การผูกมัดเป็นที่ต้องการ
บางครั้งการเขียน .then
โดยตรงก็เป็นเรื่องปกติ เนื่องจากฟังก์ชันที่ซ้อนกันสามารถเข้าถึงขอบเขตภายนอกได้ ในตัวอย่างข้างต้น การเรียกกลับที่ซ้อนกันมากที่สุดสามารถเข้าถึงตัวแปรทั้งหมด script1
, script2
, script3
แต่นั่นเป็นข้อยกเว้นมากกว่ากฎ
จากนั้น
เพื่อให้แม่นยำยิ่งขึ้น ตัวจัดการอาจไม่ส่งคืนคำสัญญาอย่างแน่นอน แต่เป็นวัตถุที่เรียกว่า "แล้วได้" ซึ่งเป็นวัตถุที่กำหนดเองซึ่งมีวิธีการ . .then
มันจะได้รับการปฏิบัติเช่นเดียวกับคำสัญญา
แนวคิดก็คือไลบรารีของบุคคลที่สามอาจใช้วัตถุ "ที่เข้ากันได้กับสัญญา" ของตนเอง พวกเขาสามารถมีชุดวิธีการเพิ่มเติมได้ แต่ยังเข้ากันได้กับคำสัญญาดั้งเดิมด้วย เพราะพวกเขาใช้ . .then
นี่คือตัวอย่างของวัตถุแล้ว:
คลาสแล้วได้ { ตัวสร้าง (จำนวน) { this.num = จำนวน; - จากนั้น (แก้ไข, ปฏิเสธ) { แจ้งเตือน (แก้ไข); // function() { รหัสเนทีฟ } // แก้ไขด้วย this.num*2 หลังจากผ่านไป 1 วินาที setTimeout(() => แก้ไข (this.num * 2), 1,000); - - - สัญญาใหม่ (แก้ไข => แก้ไข (1)) .then(ผลลัพธ์ => { กลับใหม่ จากนั้น (ผลลัพธ์); - - .แล้ว(แจ้งเตือน); // แสดง 2 หลังจาก 1,000ms
JavaScript ตรวจสอบอ็อบเจ็กต์ที่ส่งคืนโดย .then
handler ในบรรทัด (*)
: หากมีเมธอดที่เรียกได้ชื่อ then
มันจะเรียกเมธอดนั้นที่ให้ฟังก์ชันเนทิฟ resolve
reject
เป็นอาร์กิวเมนต์ (คล้ายกับตัวดำเนินการ) และรอจนกระทั่งหนึ่งในนั้น เรียกว่า ในตัวอย่างข้างต้น จะมีการเรียก resolve(2)
หลังจากผ่านไป 1 วินาที (**)
จากนั้นผลลัพธ์ก็จะถูกส่งต่อไปตามสายโซ่
คุณลักษณะนี้ช่วยให้เราสามารถรวมออบเจ็กต์ที่กำหนดเองเข้ากับ Promise Chains โดยไม่ต้องสืบทอดจาก Promise
ในการเขียนโปรแกรมส่วนหน้า สัญญามักจะใช้สำหรับคำขอเครือข่าย ลองดูตัวอย่างเพิ่มเติมของเรื่องนั้น
เราจะใช้วิธีการดึงข้อมูลเพื่อโหลดข้อมูลเกี่ยวกับผู้ใช้จากเซิร์ฟเวอร์ระยะไกล มีพารามิเตอร์ทางเลือกมากมายครอบคลุมอยู่ในบทที่แยกจากกัน แต่ไวยากรณ์พื้นฐานค่อนข้างง่าย:
ให้สัญญา = ดึงข้อมูล (url);
สิ่งนี้จะส่งคำขอเครือข่ายไปยัง url
และส่งคืนสัญญา สัญญาจะแก้ไขด้วยออบเจ็กต์ response
เมื่อเซิร์ฟเวอร์ระยะไกลตอบกลับด้วยส่วนหัว แต่ ก่อนที่จะดาวน์โหลดการตอบกลับแบบเต็ม
หากต้องการอ่านคำตอบแบบเต็ม เราควรเรียกใช้เมธอด response.text()
: โดยจะส่งคืนสัญญาที่จะแก้ไขเมื่อมีการดาวน์โหลดข้อความแบบเต็มจากเซิร์ฟเวอร์ระยะไกล โดยให้ข้อความนั้นเป็นผลลัพธ์
รหัสด้านล่างส่งคำขอไปยัง user.json
และโหลดข้อความจากเซิร์ฟเวอร์:
ดึงข้อมูล ('https://javascript.info/article/promise-chaining/user.json') // .จากนั้นด้านล่างจะทำงานเมื่อเซิร์ฟเวอร์ระยะไกลตอบสนอง .then(ฟังก์ชั่น(ตอบสนอง) { // response.text() ส่งคืนสัญญาใหม่ที่แก้ไขด้วยข้อความตอบกลับแบบเต็ม // เมื่อมันโหลด ส่งคืนการตอบกลับข้อความ (); - .then(ฟังก์ชั่น(ข้อความ) { // ...และนี่คือเนื้อหาของไฟล์ระยะไกล การแจ้งเตือน (ข้อความ); // {"ชื่อ": "iliakan", "isAdmin": true} -
ออบเจ็กต์ response
ที่ส่งคืนจาก fetch
ยังมีเมธอด response.json()
ที่อ่านข้อมูลระยะไกลและแยกวิเคราะห์เป็น JSON ในกรณีของเราสะดวกกว่า ดังนั้นเรามาเปลี่ยนมาใช้กันดีกว่า
นอกจากนี้เรายังใช้ฟังก์ชันลูกศรเพื่อความกระชับ:
// เช่นเดียวกับข้างต้น แต่ response.json() แยกวิเคราะห์เนื้อหาระยะไกลเป็น JSON ดึงข้อมูล ('https://javascript.info/article/promise-chaining/user.json') .then(response => response.json()) .then(user => alert(user.name)); // อิเลียคาน ได้รับชื่อผู้ใช้แล้ว
ตอนนี้เรามาทำอะไรบางอย่างกับผู้ใช้ที่โหลดแล้ว
ตัวอย่างเช่น เราสามารถส่งคำขอไปยัง GitHub อีกครั้ง โหลดโปรไฟล์ผู้ใช้ และแสดงอวตาร:
// ส่งคำขอสำหรับ user.json ดึงข้อมูล ('https://javascript.info/article/promise-chaining/user.json') // โหลดเป็น json .then(response => response.json()) // ส่งคำขอไปยัง GitHub .then(user => fetch(`https://api.github.com/users/${user.name}`)) // โหลดการตอบสนองเป็น json .then(response => response.json()) // แสดงภาพประจำตัว (githubUser.avatar_url) เป็นเวลา 3 วินาที (อาจทำให้เคลื่อนไหวได้) .แล้ว(githubUser => { ให้ img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "สัญญา-อวตาร-ตัวอย่าง"; document.body.append(img); setTimeout(() => img.remove(), 3000); - -
รหัสใช้งานได้ ดูความคิดเห็นเกี่ยวกับรายละเอียด อย่างไรก็ตาม อาจมีปัญหาเกิดขึ้น ซึ่งเป็นข้อผิดพลาดทั่วไปสำหรับผู้ที่เริ่มใช้คำสัญญา
ดูที่บรรทัด (*)
: เราจะทำอย่างไร หลังจาก อวตารแสดงเสร็จแล้วและถูกลบออก? เช่น เราต้องการแสดงแบบฟอร์มสำหรับแก้ไขผู้ใช้รายนั้นหรืออย่างอื่น ณ ตอนนี้ไม่มีทางแล้ว
เพื่อให้ห่วงโซ่ขยายได้ เราต้องคืนคำสัญญาที่จะได้รับการแก้ไขเมื่ออวตารแสดงเสร็จสิ้น
แบบนี้:
ดึงข้อมูล ('https://javascript.info/article/promise-chaining/user.json') .then(response => response.json()) .then(user => fetch(`https://api.github.com/users/${user.name}`)) .then(response => response.json()) .then(githubUser => new Promise(function(แก้ไข, ปฏิเสธ) { // (*) ให้ img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "สัญญา-อวตาร-ตัวอย่าง"; document.body.append(img); setTimeout(() => { img.remove(); แก้ไข (githubUser); - }, 3000); - // ทริกเกอร์หลังจาก 3 วินาที .then(githubUser => alert(`แสดง ${githubUser.name}` เสร็จแล้ว));
นั่นคือตัวจัดการ .then
ในบรรทัด (*)
ส่งคืน new Promise
ซึ่งจะถูกตัดสินหลังจากการเรียก resolve(githubUser)
ใน setTimeout
(**)
เท่านั้น ถัดไป .then
ในห่วงโซ่จะรอสิ่งนั้น
ตามแนวทางปฏิบัติที่ดี การดำเนินการแบบอะซิงโครนัสควรให้ผลตามสัญญาเสมอ นั่นทำให้สามารถวางแผนการดำเนินการหลังจากนั้นได้ แม้ว่าเราไม่ได้วางแผนที่จะขยายห่วงโซ่ตอนนี้ เราอาจต้องการมันในภายหลัง
สุดท้ายนี้ เราสามารถแบ่งโค้ดออกเป็นฟังก์ชันที่นำมาใช้ซ้ำได้:
ฟังก์ชั่น loadJson (url) { ส่งคืนการดึงข้อมูล (URL) .แล้ว(ตอบกลับ => response.json()); - ฟังก์ชั่น loadGithubUser (ชื่อ) { ส่งคืน loadJson(`https://api.github.com/users/${name}`); - ฟังก์ชั่น showAvatar (githubUser) { คืนสัญญาใหม่ (ฟังก์ชั่น (แก้ไข, ปฏิเสธ) { ให้ img = document.createElement('img'); img.src = githubUser.avatar_url; img.className = "สัญญา-อวตาร-ตัวอย่าง"; document.body.append(img); setTimeout(() => { img.remove(); แก้ไข (githubUser); }, 3000); - - // ใช้มัน: loadJson ('https://javascript.info/article/promise-chaining/user.json') .then(user => loadGithubUser(user.name)) .แล้ว(showAvatar) .then(githubUser => alert(`แสดง ${githubUser.name}` เสร็จแล้ว)); -
หากตัวจัดการ .then
(หรือ catch/finally
ไม่สำคัญ) ส่งคืนสัญญา ส่วนที่เหลือของห่วงโซ่จะรอจนกระทั่งตกลง เมื่อเป็นเช่นนั้น ผลลัพธ์ (หรือข้อผิดพลาด) จะถูกส่งต่อไป
นี่คือภาพเต็ม:
ส่วนของโค้ดเหล่านี้เท่ากันหรือไม่ กล่าวอีกนัยหนึ่ง พวกเขาทำงานในลักษณะเดียวกันในทุกสถานการณ์สำหรับฟังก์ชันตัวจัดการใดๆ หรือไม่?
สัญญาแล้ว(f1).จับ(f2);
เมื่อเทียบกับ:
สัญญาแล้ว (f1, f2);
คำตอบสั้นๆ คือ: ไม่ พวกมันไม่เท่ากัน :
ข้อแตกต่างคือหากเกิดข้อผิดพลาดใน f1
ข้อผิดพลาดนั้นจะถูกจัดการโดย .catch
ที่นี่:
สัญญา .แล้ว(f1) .จับ(f2);
…แต่ไม่ใช่ที่นี่:
สัญญา .แล้ว(f1, f2);
นั่นเป็นเพราะว่าข้อผิดพลาดถูกส่งผ่านไปยังเชน และในส่วนโค้ดที่สองนั้นไม่มีเชนที่ต่ำกว่า f1
กล่าวอีกนัยหนึ่ง . .then
ส่งผลลัพธ์/ข้อผิดพลาดไปยัง .then/catch
ถัดไป ดังนั้นในตัวอย่างแรก มี catch
ด้านล่าง และตัวอย่างที่สองไม่มี ดังนั้นจึงไม่สามารถจัดการข้อผิดพลาดได้