คำนำ
ครั้งแรกที่ฉันติดต่อกับสัญญาคือเมื่อ Microsoft เปิดตัวระบบปฏิบัติการ Windows 8 ในปี 2012 และศึกษาโดยใช้ HTML5 เพื่อเขียนแอปพลิเคชันรถไฟใต้ดินด้วยทัศนคติที่อยากรู้อยากเห็น ในเวลานั้นอินเทอร์เฟซแบบอะซิงโครนัสในห้องสมุด WINJS ที่ให้มาพร้อมกับ HTML5 ล้วนอยู่ในรูปแบบสัญญาซึ่งเป็นเพียงหนังสือแห่งสวรรค์สำหรับฉันที่เพิ่งจบการศึกษาจาก JavaScript ในเวลานั้น สิ่งที่ฉันคิดในเวลานั้นคือ Microsoft กำลังซ่อมแซมอีกครั้ง
โดยไม่คาดคิดภายในปี 2558 สัญญาถูกเขียนลงในมาตรฐาน ES6 นอกจากนี้การสำรวจแสดงให้เห็นว่าโปรแกรมเมอร์ JS ใช้สิ่งนี้ค่อนข้างสูง
กระแทกแดกดันในฐานะ Microsoft ซึ่งใช้สัญญาอย่างกว้างขวางในส่วนต่อประสานการพัฒนาแอพพลิเคชั่นเมโทรเร็วเท่าปี 2012 เบราว์เซอร์ของตัวเองคือยังไม่สนับสนุนสัญญาจนกว่าจะเสียชีวิตในปี 2558 ดูเหมือนว่า Microsoft ไม่มีเทคโนโลยีนี้ - -
เมื่อมองย้อนกลับไปตอนนี้สิ่งที่ลำบากที่สุดเกี่ยวกับการเห็นคำสัญญาในเวลานั้นคือผู้เริ่มต้นดูเหลือเชื่อและยังเป็นคุณสมบัติที่ได้รับการยกย่องอย่างกว้างขวางที่สุดโดยโปรแกรมเมอร์ JS: จากนั้นฟังก์ชั่นโซ่โทร
จากนั้นฟังก์ชั่นการเรียกสายโซ่คือการเรียกกระบวนการแบบอะซิงโครนัสหลายกระบวนการตามลำดับ บทความนี้เริ่มต้นจากจุดนี้และการศึกษาและเรียนรู้คุณสมบัติของสัญญา
สัญญาที่แก้ไขแล้ว
พิจารณาสถานการณ์ต่อไปนี้หลังจากฟังก์ชั่นล่าช้า 2 วินาทีให้พิมพ์บรรทัดของบันทึกแล้วล่าช้า 3 วินาทีจากนั้นล่าช้า 4 วินาทีพิมพ์บรรทัดของบันทึก นี่เป็นสิ่งที่ง่ายมากในภาษาการเขียนโปรแกรมอื่น ๆ แต่มันยากกว่าที่จะเข้าสู่ JS และรหัสอาจจะถูกเขียนดังนี้:
var myfunc = function () {settimeout (function () {console.log ("log1"); settimeout (function () {console.log ("log2"); settimeout (ฟังก์ชัน () {console.log ("log3");}, 4000);}, 3000);เนื่องจากโครงสร้างการโทรกลับหลายชั้นซ้อนกันโครงสร้างพีระมิดทั่วไปจะเกิดขึ้นที่นี่ หากตรรกะทางธุรกิจมีความซับซ้อนมากขึ้นมันจะกลายเป็นนรกที่น่ากลัว
หากคุณมีการรับรู้ที่ดีขึ้นและรู้วิธีแยกฟังก์ชั่นง่าย ๆ รหัสจะมีลักษณะเช่นนี้:
var func1 = function () {settimeout (func2, 2000);}; var func2 = function () {console.log ("log1"); settimeout (func3, 3000);}; var func3 = function () {console.log ("log2"); settimeout (func4, 4000);}; var func4 = function () {console.log ("log3");};มันดูดีขึ้นเล็กน้อย แต่มันก็รู้สึกแปลก ๆ อยู่เสมอ - - อันที่จริงแล้วระดับ JS ของฉันมี จำกัด ดังนั้นฉันจึงไม่สามารถพูดได้ว่าทำไมฉันไม่สามารถเขียนได้ดี หากคุณรู้ว่าทำไมสิ่งนี้ถึงไม่ดีและดังนั้นคุณจึงคิดค้นสัญญาโปรดแจ้งให้เราทราบ
ตอนนี้กลับมาที่จุดและพูดคุยเกี่ยวกับสิ่งที่สัญญา
คำอธิบายของสัญญา
โปรดให้ฉันอ้างคำอธิบายของ MDN เกี่ยวกับสัญญาที่นี่:
วัตถุสัญญาใช้สำหรับการคำนวณรอการตัดบัญชีและการคำนวณแบบอะซิงโครนัส วัตถุสัญญาแสดงถึงการดำเนินการที่ยังไม่เสร็จสมบูรณ์ แต่คาดว่าจะแล้วเสร็จในอนาคต
วัตถุสัญญาเป็นพร็อกซีสำหรับค่าส่งคืนซึ่งอาจไม่เป็นที่รู้จักเมื่อสร้างวัตถุสัญญา ช่วยให้คุณระบุวิธีการจัดการเพื่อความสำเร็จหรือความล้มเหลวของการดำเนินการแบบอะซิงโครนัส สิ่งนี้ช่วยให้วิธีการแบบอะซิงโครนัสสามารถส่งคืนค่าเช่นวิธีการซิงโครนัส: วิธีการแบบอะซิงโครนัสส่งคืนวัตถุสัญญาที่มีค่าส่งคืนเดิมแทนค่าผลตอบแทนเดิม
วัตถุสัญญามีสถานะดังต่อไปนี้:
•รอดำเนินการ: สถานะเริ่มต้น, ไม่ได้เติมเต็มหรือปฏิเสธ
•เติมเต็ม: การดำเนินการที่ประสบความสำเร็จ
•ปฏิเสธ: การดำเนินการล้มเหลว
วัตถุสัญญาที่มีสถานะที่ค้างอยู่สามารถแปลงเป็นสถานะที่เติมเต็มด้วยค่าความสำเร็จหรือสถานะที่ถูกปฏิเสธด้วยข้อความความล้มเหลว เมื่อการเปลี่ยนแปลงของรัฐวิธีการที่ผูกพันกับสัญญาจากนั้น (ฟังก์ชั่นที่จับ) จะถูกเรียก (เมื่อเชื่อมโยงวิธีการหากวัตถุสัญญาอยู่ในสถานะที่ปฏิบัติตามหรือปฏิเสธแล้ววิธีการที่สอดคล้องกันจะถูกเรียกทันทีดังนั้นจึงไม่มีเงื่อนไขการแข่งขันระหว่างการดำเนินการแบบอะซิงโครนัสและวิธีการผูกมัด)
สำหรับคำอธิบายเพิ่มเติมและตัวอย่างของสัญญาโปรดดูที่รายการสัญญาของ MDN หรือรายการสัญญาของ MSDN
พยายามแก้ปัญหาของเราด้วยคำสัญญา
จากความเข้าใจข้างต้นเกี่ยวกับคำสัญญาเรารู้ว่าเราสามารถใช้มันเพื่อแก้ปัญหาที่รหัสที่อยู่เบื้องหลังการเรียกกลับหลายชั้นที่ซ้อนกันนั้นโง่และยากที่จะรักษา ลิงก์ทั้งสองที่ระบุไว้ข้างต้นมีความชัดเจนมากเกี่ยวกับไวยากรณ์และพารามิเตอร์ของสัญญา ฉันจะไม่ทำซ้ำที่นี่เพียงอัปโหลดรหัส
ก่อนอื่นลองใช้กรณีที่ค่อนข้างง่ายซึ่งจะดำเนินการล่าช้าและโทรกลับหนึ่งครั้ง:
สัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มต้น settimeout"); settimeout (res, 2000);}) จากนั้น (ฟังก์ชั่น () {console.log (date.now () + "การโทรกลับ");});ดูเหมือนว่าไม่มีความแตกต่างจากตัวอย่างใน MSDN และผลการดำเนินการมีดังนี้:
$ node promistest.js1450194136374 เริ่มต้น Settimeout1450194138391 การกลับหมดเวลาโทรกลับ
ดังนั้นหากเราต้องการทำล่าช้าอีกครั้งฉันสามารถเขียนสิ่งนี้:
สัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มต้น settimeout 1"); settimeout (res, 2000);}) จากนั้น (ฟังก์ชั่น () {console.log (วันที่ () + "การโทรกลับ"); 3000);}) จากนั้น (ฟังก์ชัน () {console.log (date.now () + "หมดเวลา 2 โทรกลับ");})});ดูเหมือนว่าจะทำงานได้อย่างถูกต้องเช่นกัน:
$ node promistest.js1450194338710 เริ่มต้น Settimeout 11450194340720 การหมดเวลา 1 โทรกลับ 1450194340720 เริ่มต้น Settimeout 21450194343722 หมดเวลา 2 โทรกลับ
แต่รหัสดูโง่และน่ารักใช่มั้ย มันเป็นการสร้างปิรามิดอีกครั้ง สิ่งนี้ขัดกับจุดประสงค์ในการแนะนำสัญญา
แล้วปัญหาคืออะไร? ท่าทางที่ถูกต้องคืออะไร?
คำตอบนั้นถูกซ่อนไว้ในค่าการส่งคืนของฟังก์ชันแล้วฟังก์ชันการเรียกกลับ (หรือ oncompleted) ของฟังก์ชันนั้น
ก่อนอื่นฟังก์ชั่นนั้นจะส่งคืนตัวแปรสัญญาใหม่และคุณสามารถเรียกฟังก์ชันของตัวแปรสัญญาใหม่นี้อีกครั้งเช่นนี้:
สัญญาใหม่ (... ). จากนั้น (... )
ฟังก์ชั่นที่ส่งคืนโดยฟังก์ชั่นนั้นขึ้นอยู่กับค่าการส่งคืนของการโทรกลับอย่างสมบูรณ์
ในความเป็นจริง onfulfilled สามารถส่งคืนตัวแปรปกติหรือตัวแปรสัญญาอื่น
หาก ONFULFILLD ส่งคืนค่าปกติฟังก์ชั่นจะส่งคืนตัวแปรสัญญาเริ่มต้น การดำเนินการฟังก์ชั่นของสัญญานี้จะทำให้สัญญาเป็นไปตามทันทีและฟังก์ชั่น onfulfilled จะถูกดำเนินการและพารามิเตอร์รายการ onfulfilled คือค่าคืนของ onfulfilled ก่อนหน้า
หาก onfulfilled ส่งคืนตัวแปรสัญญาตัวแปรสัญญานั้นจะถูกใช้เป็นค่าส่งคืนของฟังก์ชันนั้น
เอกสารเกี่ยวกับ MDN และ MSDN ไม่มีคำอธิบายเชิงบวกที่ชัดเจนของการตั้งค่าชุดนี้สำหรับฟังก์ชั่นนั้นและฟังก์ชั่น onfulfilled สำหรับเอกสาร ES6 อย่างเป็นทางการ ECMASCRIPT 2015 (รุ่นที่ 6, ECMA-262) - - ฉันไม่เข้าใจระดับของฉันจริงๆ หากผู้เชี่ยวชาญใด ๆ สามารถอธิบายคำอธิบายของค่าส่งคืนทั้งสองในเอกสารอย่างเป็นทางการโปรดฝากข้อความไว้เพื่อขอคำแนะนำ! - -
ดังนั้นข้างต้นคือการเล่นฟรีของฉันและองค์กรภาษาก็ยากที่จะอธิบาย คุณจะเข้าใจหลังจากอ่านรหัส
ก่อนอื่นกรณีของตัวแปรปกติที่ส่งคืน:
สัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มต้น Settimeout 1"); settimeout (res, 2000);}) จากนั้น (ฟังก์ชั่น () {console.log (date.now () + "การโทรกลับ (คืน" arg);});ผลการดำเนินการรหัสด้านบนคือ:
$ node promistest.js1450277122125 เริ่มต้น Settimeout 11450277124129 การหมดเวลา 1 การโทรกลับ 1450277124129 ล่าสุดผลตอบแทน onfulfilled 1024
มันน่าสนใจเล็กน้อย แต่นั่นไม่ใช่กุญแจ กุญแจสำคัญคือฟังก์ชั่น onfulfilled ส่งคืนตัวแปรสัญญาซึ่งทำให้สะดวกสำหรับเราที่จะเรียกกระบวนการอะซิงโครนัสหลายครั้งต่อเนื่อง ตัวอย่างเช่นเราสามารถพยายามดำเนินการล่าช้าสองครั้งติดต่อกัน:
สัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มต้น settimeout 1"); settimeout (res, 2000);}) จากนั้น (ฟังก์ชั่น () {console.log (วันที่ () 3000);});}). จากนั้น (ฟังก์ชั่น () {console.log (date.now () + "หมดเวลา 2 โทรกลับ");});ผลการดำเนินการมีดังนี้:
$ node promistest.js1450277510275 เริ่มต้น Settimeout 11450277512276 การหมดเวลา 1 การโทรกลับ 1450277512276 เริ่มต้น Settimeout 21450277515327 หมดเวลา 2 โทรกลับ
หากคุณคิดว่านี่ไม่มีอะไรดีมันไม่ใช่ปัญหาที่จะทำอีกสองสามครั้ง:
สัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มต้น settimeout 1"); settimeout (res, 2000);}) จากนั้น (ฟังก์ชั่น () {console.log (วันที่ () 3000);});}) จากนั้น (ฟังก์ชั่น () {console.log (date.now () + "หมดเวลา 2 โทรกลับ"); ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (วันที่. now () + "settimeout 3"); "การหมดเวลา 3 โทรกลับ"); การส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มต้น Settimeout 4"); settimeout (res, 5000);});});})$ node promistest.js1450277902714 เริ่มต้น Settimeout 11450277904722 TIMEOUT 1 โทรกลับ 1450277904724 เริ่มต้น SettimeOut 2145027777725 TIMEOUT 2 โทรกลับ Settimeout 41450277916744 หมดเวลา 4 โทรกลับ
จะเห็นได้ว่าฟังก์ชั่นการโทรกลับล่าช้าหลายรายการถูกจัดเรียงอย่างเป็นระเบียบและไม่มีโครงสร้างคล้ายพีระมิดที่ได้รับความนิยม แม้ว่ารหัสเรียกกระบวนการแบบอะซิงโครนัส แต่ดูเหมือนว่าพวกเขาทั้งหมดประกอบด้วยกระบวนการซิงโครนัส นี่คือผลประโยชน์ที่สัญญานำมาให้เรา
หากคุณมีนิสัยที่ดีในการกลั่นรหัส verbose เป็นฟังก์ชั่นแยกต่างหากมันจะสวยงามยิ่งขึ้น:
ฟังก์ชั่น timeout1 () {ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มหมดเวลา 1"); settimeout (res, 2000);});} function timeout2 () {ส่งคืนสัญญาใหม่ (res, rej) {console.log (date.now () TimeOut3 () {ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มต้นหมดเวลา 2"); settimeout (res, 3000);});} timeout3 () {คืนสัญญาใหม่ (res, rej) {console.log (วันที่ TimeOut4 () {ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {console.log (date.now () + "เริ่มหมดเวลา 4"); settimeout (res, 5000);});} timeout1 (). จากนั้น -$ node promistest.js1450278983342 เริ่มต้นหมดเวลา 11450278985343 เริ่มต้นเวลา 24145027898351 เริ่มต้นเวลา 31450278992356 เริ่มต้นเวลา 4145027897370 Timout4
ต่อไปเราสามารถศึกษาปัญหาของการผ่านในพารามิเตอร์ที่เข้ามาของฟังก์ชั่น onfulfilled
เรารู้อยู่แล้วว่าหากฟังก์ชั่น onfulfilled ก่อนหน้านี้ส่งคืนค่าปกติค่านี้จะเป็นพารามิเตอร์รายการของฟังก์ชั่น onfulfilled; จากนั้นหาก OnFulfilled ก่อนหน้านี้ส่งคืนตัวแปรสัญญาพารามิเตอร์รายการของ onfulfilled มาจากไหน?
คำตอบคือพารามิเตอร์รายการของฟังก์ชั่น onfulfilled นี้คือค่าที่ส่งผ่านเมื่อฟังก์ชันการแก้ไขถูกเรียกในสัญญาก่อนหน้านี้
ฉันไม่ยอมรับการกระโดดสักพักใช่มั้ย มาทำดี
ก่อนอื่นฟังก์ชั่นสัญญาคืออะไรแก้ไข? ใช้คำสั่งจาก Zou Zou เหนือ MDN
แก้ไขวัตถุสัญญาที่มีมูลค่าความสำเร็จ หากค่ามีความต่อเนื่อง (สามารถทำได้เช่นด้วยวิธีนี้) วัตถุสัญญาที่ส่งคืนจะ "ติดตาม" ค่า
ในระยะสั้นนี่คือการโทรกลับเมื่อการโทรแบบอะซิงโครนัสสำเร็จ
ลองมาดูกันว่าการโทรกลับเป็นอย่างไรในอินเทอร์เฟซแบบอะซิงโครนัสปกติ ใช้ fs.readfile (ไฟล์ [, ตัวเลือก], callback) บน nodejs เช่น ตัวอย่างการโทรทั่วไปมีดังนี้
fs.readfile ('/etc/passwd', ฟังก์ชั่น (err, data) {ถ้า (err) throw err; console.log (data);});เนื่องจากสำหรับฟังก์ชั่น fs.readfile ไม่ว่าจะสำเร็จหรือล้มเหลวมันจะเรียกฟังก์ชั่นการเรียกกลับการโทรกลับดังนั้นการโทรกลับนี้จึงยอมรับพารามิเตอร์สองพารามิเตอร์ ได้แก่ คำอธิบายข้อยกเว้นเกี่ยวกับความล้มเหลว ERR และข้อมูลผลการส่งคืนบนความสำเร็จ
ดังนั้นหากเราใช้สัญญาว่าจะสร้างตัวอย่างการอ่านไฟล์นี้เราจะเขียนได้อย่างไร?
ขั้นแรกให้ห่อหุ้มฟังก์ชั่น fs.readfile:
ฟังก์ชั่น readfile (ชื่อไฟล์) {ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (แก้ไข, ปฏิเสธ) {fs.readfile (ชื่อไฟล์, ฟังก์ชัน (err, data) {ถ้า (err) {ปฏิเสธ (err);} else {resolve (data);}});});});};ประการที่สองคือการโทร:
ReadFile ('thefile.txt') จากนั้น (ฟังก์ชั่น (ข้อมูล) {console.log (data);}, ฟังก์ชั่น (err) {โยน err;});ลองนึกภาพว่าเนื้อหาของไฟล์มักจะถูกวางไว้ในอินเทอร์เฟซการโทรแบบซิงโครนัสของการอ่านไฟล์ในภาษาอื่น ๆ ? ค่าคืนฟังก์ชันถูกต้องหรือไม่? คำตอบคือโสมทางเข้าของการแก้ไขนี้คืออะไร? มันเป็นค่าส่งคืนเมื่อการโทรแบบอะซิงโครนัสสำเร็จ
ด้วยแนวคิดนี้มันไม่ยากที่จะเข้าใจ "พารามิเตอร์อินพุตของฟังก์ชั่น onfulfilled คือค่าที่ส่งผ่านเมื่อเรียกฟังก์ชั่นการแก้ไขในสัญญาก่อนหน้านี้" เพราะงานที่สมบูรณ์คือการประมวลผลผลลัพธ์หลังจากการโทรแบบอะซิงโครนัสก่อนหน้านี้สำเร็จ
อนิจจาในที่สุดก็ยืดออก - -
สรุป
โปรดอนุญาตให้ฉันใช้รหัสหนึ่งเพื่อสรุปประเด็นสำคัญที่อธิบายไว้ในบทความนี้:
ฟังก์ชั่น callp1 () {console.log (date.now () + "เริ่ม callp1"); ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {settimeout (res, 2000);});} function callp2 () {console.log (date.now () + "เริ่ม callp2"); ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {settimeout (function () {res ({arg1: 4, arg2: "arg2 value"});}, 3000);});} ฟังก์ชั่น callp3 (arg) {console.log (date.now ()) ส่งคืนสัญญาใหม่ (ฟังก์ชั่น (res, rej) {settimeout (function () {res ("callp3");}, arg * 1000);});} callp1 (). จากนั้น (ฟังก์ชั่น () {console.log (date.now () + "callp1 return"); value = " + json.stringify (ret)); return callp3 (ret.arg1);}) จากนั้น (ฟังก์ชั่น (ret) {console.log (date.now () +" callp3 return ด้วย ret value = " + ret);}) $ node promistest.js1450191479575 เริ่ม callp11450191481597 callp1 return1450191481599 เริ่ม callp21450191484605 callp2 ส่งคืนด้วยค่า ret = {"arg1": 4, "arg2": 41450191488610 callp3 return ด้วยค่า ret = callp3ประสบการณ์การเรียนรู้ที่เรียบง่ายข้างต้นของการใช้สัญญาเพื่อแก้ปัญหาการโทรแบบอะซิงโครนัสหลายชั้นคือเนื้อหาทั้งหมดที่ฉันแบ่งปันกับคุณ ฉันหวังว่าคุณจะให้ข้อมูลอ้างอิงและฉันหวังว่าคุณจะสนับสนุน wulin.com มากขึ้น