สัญญาจาวาสคริปต์: บทช่วยสอนพร้อมตัวอย่าง

เผยแพร่แล้ว: 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 ที่ใช้ร็อกแอนด์โรลเป็นธีมของแอปพลิเคชันในหนังสือ ลงทะเบียนที่นี่เพื่อทราบเมื่อเปิดตัว