สัญญาจาวาสคริปต์: บทช่วยสอนพร้อมตัวอย่าง
เผยแพร่แล้ว: 2022-03-11คำสัญญาเป็นประเด็นร้อนในแวดวงการพัฒนา JavaScript และคุณควรทำความคุ้นเคยกับพวกเขาอย่างแน่นอน มันไม่ง่ายเลยที่จะพันหัวของคุณ อาจต้องใช้บทช่วยสอน ตัวอย่าง และแบบฝึกหัดจำนวนหนึ่งเพื่อทำความเข้าใจ
เป้าหมายของฉันในบทช่วยสอนนี้คือช่วยให้คุณเข้าใจ JavaScript Promises และกระตุ้นให้คุณฝึกฝนการใช้งานมากขึ้น ฉันจะอธิบายว่าสัญญาคืออะไร ปัญหาที่พวกเขาแก้ไข และวิธีการทำงาน แต่ละขั้นตอนที่อธิบายไว้ในบทความนี้จะมาพร้อมกับตัวอย่างโค้ด jsbin
เพื่อช่วยคุณในการทำงานร่วมกัน และเพื่อใช้เป็นฐานสำหรับการสำรวจต่อไป
สัญญาจาวาสคริปต์คืออะไร?
สัญญาเป็นวิธีที่สร้างมูลค่าในที่สุด ถือได้ว่าเป็นฟังก์ชันคู่แบบอะซิงโครนัสของฟังก์ชัน getter สาระสำคัญของมันสามารถอธิบายได้ดังนี้:
promise.then(function(value) { // Do something with the 'value' });
คำมั่นสัญญาสามารถแทนที่การใช้การเรียกกลับแบบอะซิงโครนัส และให้ประโยชน์มากกว่านั้นหลายประการ พวกเขาเริ่มมีพื้นฐานมากขึ้นเรื่อยๆ เนื่องจากมีไลบรารีและเฟรมเวิร์กมากขึ้นเรื่อยๆ ที่รวมเอาสิ่งเหล่านี้เป็นแนวทางหลักในการจัดการความไม่ตรงกัน Ember.js เป็นตัวอย่างที่ดีของเฟรมเวิร์กดังกล่าว
มีไลบรารีหลายแห่งที่ใช้ข้อกำหนด Promises/A+ เราจะเรียนรู้คำศัพท์พื้นฐานและทำงานผ่านตัวอย่าง JavaScript ที่สัญญาว่าจะแนะนำแนวคิดเบื้องหลังในทางปฏิบัติ ฉันจะใช้หนึ่งในไลบรารีการนำไปใช้ที่ได้รับความนิยม rsvp.js ในตัวอย่างโค้ด
เตรียมตัวให้พร้อม เราจะทอยลูกเต๋ากันมากมาย!
รับไลบรารี rsvp.js
คำสัญญา และด้วยเหตุนี้ rsvp.js สามารถใช้ได้ทั้งบนเซิร์ฟเวอร์และฝั่งไคลเอ็นต์ ในการติดตั้งสำหรับ nodejs ให้ไปที่โฟลเดอร์โครงการของคุณและพิมพ์:
npm install --save rsvp
หากคุณทำงาน front-end และใช้ bower ก็เป็นแค่
bower install -S rsvp
ห่างออกไป.
หากคุณต้องการเล่นเกมให้ถูกต้อง คุณสามารถรวมมันผ่านแท็กสคริปต์อย่างง่าย (และด้วย jsbin
คุณสามารถเพิ่มผ่านเมนูแบบเลื่อนลง "เพิ่มไลบรารี" ได้):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
สัญญามีคุณสมบัติอะไรบ้าง?
คำสัญญาอาจอยู่ในสถานะใดสถานะหนึ่งจากสามสถานะ: อยู่ระหว่างดำเนิน การ ดำเนินการ แล้ว หรือ ถูกปฏิเสธ เมื่อสร้างแล้ว สัญญาจะอยู่ในสถานะรอดำเนินการ จากที่นี่ มันสามารถไปที่สถานะเติมเต็มหรือถูกปฏิเสธ เราเรียกการเปลี่ยนแปลงนี้ว่าการ แก้ไขสัญญา สถานะที่แก้ไขแล้วของคำสัญญาคือสถานะสุดท้าย ดังนั้นเมื่อสัญญาสำเร็จหรือถูกปฏิเสธ คำสัญญาจะอยู่ที่นั่น
วิธีสร้างคำมั่นสัญญาใน rsvp.js คือผ่านสิ่งที่เรียกว่าคอนสตรัคเตอร์ที่เปิดเผย คอนสตรัคเตอร์ประเภทนี้รับพารามิเตอร์ฟังก์ชันเดียวและเรียกใช้ทันทีด้วยอาร์กิวเมนต์สองข้อ คือ fulfill
และ reject
ที่สามารถเปลี่ยนสัญญาเป็นสถานะที่ fulfilled
หรือ rejected
:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
รูปแบบสัญญา JavaScript นี้เรียกว่าคอนสตรัคเตอร์ที่เปิดเผยเนื่องจากอาร์กิวเมนต์ของฟังก์ชันเดียวเปิดเผยความสามารถของมันต่อฟังก์ชันคอนสตรัคเตอร์ แต่รับรองว่าผู้บริโภคของสัญญาไม่สามารถจัดการสถานะได้
ผู้บริโภคของสัญญาสามารถตอบสนองต่อการเปลี่ยนแปลงสถานะโดยเพิ่มตัวจัดการผ่านวิธีการ then
ต้องใช้ฟังก์ชันการเติมเต็มและตัวจัดการการปฏิเสธ ซึ่งทั้งสองอย่างนี้อาจขาดหายไปได้
promise.then(onFulfilled, onRejected);
ขึ้นอยู่กับผลลัพธ์ของกระบวนการแก้ไขของสัญญา ตัวจัดการ onFulfilled
หรือ onRejected
จะถูกเรียก แบบอะซิงโครนัส
มาดูตัวอย่างที่แสดงให้เห็นว่ามีการดำเนินการในลำดับใด:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
ตัวอย่างนี้พิมพ์ผลลัพธ์ที่คล้ายกับต่อไปนี้:
1 2 3 Oh, noes, threw a 4.
หรือถ้าเราโชคดีเราจะเห็น:
1 2 3 Yay, threw a 6.
บทช่วยสอนสัญญานี้แสดงให้เห็นสองสิ่ง
ประการแรก ตัวจัดการที่เราแนบมากับคำสัญญานั้นถูกเรียกจริงๆ หลังจากที่โค้ดอื่นๆ ทั้งหมดทำงานแบบอะซิงโครนัส
ประการที่สอง ตัวจัดการการเติมเต็มจะถูกเรียกก็ต่อเมื่อคำสัญญาถูกเติมเต็ม โดยมีมูลค่าที่ได้รับการแก้ไขด้วย (ในกรณีของเรา ผลลัพธ์ของการทอยลูกเต๋า) เช่นเดียวกันกับตัวจัดการการปฏิเสธ
สัญญาผูกมัดและไหลลงมา
ข้อกำหนดกำหนดว่าฟังก์ชัน then
(ตัวจัดการ) จะต้องส่งคืนสัญญาด้วย ซึ่งช่วยให้สามารถเชื่อมโยงสัญญาเข้าด้วยกัน ส่งผลให้โค้ดดูเกือบจะซิงโครนัส:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
ที่นี่ signupPayingUser
ส่งกลับคำสัญญา และแต่ละฟังก์ชันในสัญญาลูกโซ่จะถูกเรียกพร้อมค่าส่งคืนของตัวจัดการก่อนหน้าเมื่อเสร็จสิ้น เพื่อวัตถุประสงค์ในทางปฏิบัติทั้งหมด สิ่งนี้จะทำให้การเรียกเป็นอนุกรมโดยไม่ปิดกั้นเธรดการดำเนินการหลัก
หากต้องการดูว่าแต่ละคำสัญญาได้รับการแก้ไขด้วยมูลค่าที่ส่งคืนของรายการก่อนหน้าในห่วงโซ่อย่างไร เรากลับไปที่การทอยลูกเต๋า เราต้องการทอยลูกเต๋าสูงสุดสามครั้งหรือจนกว่าหกตัวแรกขึ้นมา jsbin:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
เมื่อคุณเรียกใช้โค้ดตัวอย่างสัญญานี้ คุณจะเห็นสิ่งนี้บนคอนโซล:
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
คำมั่นสัญญาที่ส่งกลับโดย tossASix
จะถูกปฏิเสธเมื่อการทอยไม่ใช่หก ดังนั้นตัวจัดการการปฏิเสธจึงถูกเรียกด้วยการทอยตามจริง logAndTossAgain
พิมพ์ผลลัพธ์บนคอนโซลอีกครั้งและส่งคืนสัญญาที่แสดงถึงการทอยลูกเต๋าอื่น ในทางกลับกัน การโยนนั้นก็ถูกปฏิเสธและออกจากระบบในครั้งต่อไปด้วย logAndTossAgain
อย่างไรก็ตาม บางครั้งคุณโชคดี* และคุณสามารถทอยหกได้:
Tossed a 4, need to try again. Yay, managed to toss a 6.
* คุณไม่จำเป็นต้อง ได้ รับโชคดี มีโอกาส ~ 42% ที่จะทอยอย่างน้อยหนึ่งหกถ้าคุณทอยลูกเต๋าสามลูก

ตัวอย่างนั้นยังสอนอะไรเราอีกหลายอย่าง ดูว่าไม่มีการทอยอีกต่อไปหลังจากการหมุนหกครั้งสำเร็จครั้งแรกหรือไม่? โปรดทราบว่าตัวจัดการ Fulfillment ทั้งหมด (อาร์กิวเมนต์แรกในการเรียก then
) ในเชนนั้นเป็น null
ยกเว้นอันสุดท้าย logSuccess
ข้อกำหนดกำหนดว่าหากตัวจัดการ (การปฏิบัติตามหรือการปฏิเสธ) ไม่ใช่ฟังก์ชัน สัญญาที่ส่งคืนจะต้องได้รับการแก้ไข (ดำเนินการหรือปฏิเสธ) ด้วยค่าเดียวกัน ในตัวอย่างคำสัญญาด้านบน ตัวจัดการการเติมเต็ม null
ไม่ใช่ฟังก์ชันและค่าของสัญญาถูกเติมเต็มด้วย 6 ดังนั้นคำสัญญาที่ส่งคืนโดยการเรียก then
(อันถัดไปในสายโซ่) ก็จะถูกเติมเต็มเช่นกัน โดยมีค่า 6 เป็นค่าของมัน
สิ่งนี้จะเกิดขึ้นซ้ำๆ จนกว่าจะมีตัวจัดการการเติมสินค้าจริง (ตัวจัดการที่เป็นฟังก์ชัน) ดังนั้นการเติมเต็มจะ ไหลลงมา จนกว่าจะได้รับการจัดการ ในกรณีของเรา กรณีนี้จะเกิดขึ้นที่ส่วนท้ายของห่วงโซ่โดยออกจากระบบคอนโซลอย่างสนุกสนาน
การจัดการข้อผิดพลาด
ข้อมูลจำเพาะ Promises/A+ ต้องการให้ถ้าสัญญาถูกปฏิเสธหรือมีข้อผิดพลาดเกิดขึ้นในตัวจัดการการปฏิเสธ ตัวจัดการการปฏิเสธควรได้รับการจัดการโดยตัวจัดการการปฏิเสธที่ "ปลายน้ำ" จากต้นทาง
การใช้เทคนิค Trickle down ด้านล่างนี้เป็นวิธีที่สะอาดในการจัดการข้อผิดพลาด:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
เนื่องจากตัวจัดการการปฏิเสธจะถูกเพิ่มเข้าไปที่ส่วนท้ายสุดของเชนเท่านั้น หากตัวจัดการการเติมเต็มใดๆ ในเชนถูกปฏิเสธหรือเกิดข้อผิดพลาด ตัวจัดการการปฏิเสธจะไหลลงมาจนกว่าจะชนเข้ากับ displayAndSendErrorReport
กลับไปที่ลูกเต๋าที่เรารักและดูว่าในการดำเนินการ สมมติว่าเราต้องการโยนลูกเต๋าแบบอะซิงโครนัสแล้วพิมพ์ผลลัพธ์ออกมา:
var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
เมื่อคุณเรียกใช้สิ่งนี้จะไม่มีอะไรเกิดขึ้น ไม่มีการพิมพ์บนคอนโซลและดูเหมือนว่าจะไม่มีข้อผิดพลาดเกิดขึ้น
ในความเป็นจริง มีข้อผิดพลาดเกิดขึ้น เราไม่เห็นมันเนื่องจากไม่มีตัวจัดการการปฏิเสธในห่วงโซ่ เนื่องจากโค้ดในตัวจัดการถูกเรียกใช้งานแบบอะซิงโครนัส ด้วยสแต็กใหม่ จึงไม่ได้ล็อกเอาต์ออกจากคอนโซลด้วยซ้ำ มาแก้ไขสิ่งนี้:
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
การเรียกใช้โค้ดด้านบนแสดงข้อผิดพลาดในขณะนี้:
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
เราลืมส่งคืนบางสิ่งจาก logAndTossAgain
และสัญญาที่สองได้รับการเติมเต็มด้วย undefined
ตัวจัดการการปฏิบัติตามข้อกำหนดถัดไประเบิดขึ้นเมื่อพยายามเรียก toUpperCase
ในเรื่องนั้น นั่นเป็นอีกสิ่งสำคัญที่ต้องจำไว้: ส่งคืนบางสิ่งจากผู้จัดการเสมอ หรือเตรียมพร้อมสำหรับตัวจัดการที่ตามมาเพื่อไม่ให้ผ่านไป
สร้างสูงขึ้น
ตอนนี้เราได้เห็นพื้นฐานของสัญญา JavaScript ในโค้ดตัวอย่างของบทช่วยสอนนี้แล้ว ประโยชน์ที่ยอดเยี่ยมของการใช้สิ่งเหล่านี้คือสามารถประกอบขึ้นด้วยวิธีง่ายๆ เพื่อสร้างคำสัญญา "ประสม" กับพฤติกรรมที่เราต้องการ ไลบรารี rsvp.js
มีไลบรารี่จำนวนหนึ่ง และคุณสามารถสร้างไลบรารีของคุณเองได้โดยใช้พื้นฐานและระดับที่สูงกว่าเหล่านี้
สำหรับตัวอย่างสุดท้ายที่ซับซ้อนที่สุด เราเดินทางไปยังโลกแห่งการเล่นตามบทบาท AD&D และทอยลูกเต๋าเพื่อรับคะแนนตัวละคร คะแนนดังกล่าวได้มาจากการทอยลูกเต๋าสามลูกสำหรับแต่ละทักษะของตัวละคร
ให้ฉันวางโค้ดที่นี่ก่อนแล้วอธิบายว่ามีอะไรใหม่:
function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
เราคุ้นเคยกับ toss
จากตัวอย่างโค้ดที่แล้ว มันสร้างคำมั่นสัญญาที่จะเติมเต็มด้วยผลของการโยนลูกเต๋า ฉันใช้ RSVP.resolve
วิธีที่สะดวกในการสร้างสัญญาดังกล่าวโดยมีพิธีการน้อยกว่า (ดู [1] ในรหัสด้านบน)
ใน threeDice
ฉันสร้างคำมั่นสัญญา 3 ข้อซึ่งแต่ละคำเป็นตัวแทนของการทอยลูกเต๋า และในที่สุดก็รวมเข้ากับ RSVP.all
RSVP.all
รับอาร์เรย์ของสัญญาและได้รับการแก้ไขด้วยอาร์เรย์ของค่าที่แก้ไขแล้ว ค่าหนึ่งสำหรับแต่ละสัญญาที่เป็นส่วนประกอบ ในขณะที่ยังคงรักษาลำดับไว้ นั่นหมายความว่าเราได้ผลลัพธ์จากการโยน results
(ดู [2] ในโค้ดด้านบน) และเราส่งคืนสัญญาที่เติมเต็มด้วยผลรวม (ดู [3] ในโค้ดด้านบน)
การแก้ไขสัญญาที่ได้จะบันทึกจำนวนทั้งหมด:
"Rolled 11 with three dice"
ใช้คำมั่นสัญญาในการแก้ปัญหาที่แท้จริง
คำสัญญาของ JavaScript ใช้เพื่อแก้ปัญหาในแอปพลิเคชันที่มีความซับซ้อนมากกว่า การโยนลูกเต๋าแบบอะซิงโครนัสสำหรับไม่มีเหตุผลที่ ดี
หากคุณแทนที่การทอยลูกเต๋าสามลูกด้วยการส่งคำขอ ajax สามรายการเพื่อแยกปลายทางและดำเนินการต่อเมื่อทั้งหมดกลับมาสำเร็จ (หรือหากมีสิ่งใดล้มเหลว) แสดงว่าคุณมีการใช้งานคำมั่นสัญญาและ RSVP.all
ที่มีประโยชน์อยู่แล้ว
สัญญาเมื่อใช้อย่างถูกต้อง จะสร้างโค้ดที่อ่านง่ายซึ่งให้เหตุผลได้ง่ายกว่า และทำให้ดีบักได้ง่ายกว่าการเรียกกลับ ไม่จำเป็นต้องจัดทำข้อตกลงเกี่ยวกับ ตัวอย่างเช่น การจัดการข้อผิดพลาด เนื่องจากเป็นส่วนหนึ่งของข้อกำหนดแล้ว
เราแทบจะไม่ได้ขีดข่วนพื้นผิวของสิ่งที่สัญญาสามารถทำได้ในบทช่วยสอน JavaScript นี้ ไลบรารี Promise มีวิธีการมากมายและตัวสร้างระดับต่ำที่พร้อมให้คุณใช้งาน เชี่ยวชาญสิ่งเหล่านี้ และท้องฟ้าคือขีดจำกัดในสิ่งที่คุณสามารถทำได้กับพวกมัน
เกี่ยวกับผู้เขียน
Balint Erdi เป็นนักเล่นตามบทบาทที่ยอดเยี่ยมและเป็นแฟนตัวยงของ AD&D เมื่อนานมาแล้ว และเป็นคำสัญญาที่ยอดเยี่ยมและเป็นแฟนตัวยงของ Ember.js ในตอนนี้ สิ่งที่คงที่คือความหลงใหลในเพลงร็อกแอนด์โรลของเขา นั่นเป็นเหตุผลที่เขาตัดสินใจเขียนหนังสือบน Ember.js ที่ใช้ร็อกแอนด์โรลเป็นธีมของแอปพลิเคชันในหนังสือ ลงทะเบียนที่นี่เพื่อทราบเมื่อเปิดตัว