เราใช้วิธีการเบราว์เซอร์ตามตัวอย่างที่นี่
เพื่อสาธิตการใช้การเรียกกลับ คำสัญญา และแนวคิดเชิงนามธรรมอื่นๆ เราจะใช้วิธีการบางอย่างของเบราว์เซอร์: โดยเฉพาะการโหลดสคริปต์และดำเนินการจัดการเอกสารอย่างง่าย
หากคุณไม่คุ้นเคยกับวิธีการเหล่านี้ และการใช้งานในตัวอย่างทำให้เกิดความสับสน คุณอาจต้องการอ่านบทบางส่วนจากส่วนถัดไปของบทช่วยสอน
แม้ว่าเราจะพยายามทำให้สิ่งต่าง ๆ ชัดเจนอยู่แล้ว จะไม่มีสิ่งใดที่ซับซ้อนเกี่ยวกับเบราว์เซอร์มากนัก
ฟังก์ชันหลายอย่างมีให้โดยสภาพแวดล้อมโฮสต์ JavaScript ซึ่งช่วยให้คุณสามารถกำหนดเวลาการดำเนินการแบบอะซิ งโครนัสได้ กล่าวอีกนัยหนึ่ง การกระทำที่เราเริ่มต้นตอนนี้ แต่จะเสร็จสิ้นในภายหลัง
ตัวอย่างเช่น ฟังก์ชันหนึ่งคือฟังก์ชัน setTimeout
มีตัวอย่างอื่นๆ ของการดำเนินการแบบอะซิงโครนัสในโลกแห่งความเป็นจริง เช่น การโหลดสคริปต์และโมดูล (เราจะกล่าวถึงในบทต่อๆ ไป)
ดูฟังก์ชัน loadScript(src)
ที่โหลดสคริปต์ด้วย src
ที่กำหนด:
ฟังก์ชั่น loadScript (src) { // สร้างแท็ก <script> และผนวกเข้ากับเพจ // สิ่งนี้ทำให้สคริปต์ที่มี src ที่กำหนดเริ่มโหลดและรันเมื่อเสร็จสมบูรณ์ ให้ script = document.createElement('script'); script.src = src; document.head.append(สคริปต์); -
โดยแทรกแท็ก <script src="…">
ใหม่ที่สร้างขึ้นแบบไดนามิกลงในเอกสารด้วย src
ที่กำหนด เบราว์เซอร์จะเริ่มโหลดโดยอัตโนมัติและดำเนินการเมื่อเสร็จสมบูรณ์
เราสามารถใช้ฟังก์ชันนี้ได้ดังนี้:
// โหลดและรันสคริปต์ตามเส้นทางที่กำหนด loadScript('/my/script.js');
สคริปต์ถูกดำเนินการ "แบบอะซิงโครนัส" เนื่องจากเริ่มโหลดในขณะนี้ แต่จะรันในภายหลังเมื่อฟังก์ชันเสร็จสิ้นแล้ว
หากมีโค้ดใดๆ ด้านล่าง loadScript(…)
จะไม่รอจนกว่าการโหลดสคริปต์จะเสร็จสิ้น
loadScript('/my/script.js'); // โค้ดด้านล่าง loadScript // ไม่รอให้สคริปต์โหลดเสร็จ -
สมมติว่าเราจำเป็นต้องใช้สคริปต์ใหม่ทันทีที่โหลด มันประกาศฟังก์ชั่นใหม่และเราต้องการเรียกใช้มัน
แต่ถ้าเราทำเช่นนั้นทันทีหลังจากการเรียก loadScript(…)
มันจะไม่ทำงาน:
loadScript('/my/script.js'); // สคริปต์มี "function newFunction() {…}" ฟังก์ชั่นใหม่(); // ไม่มีฟังก์ชันดังกล่าว!
แน่นอนว่าเบราว์เซอร์อาจไม่มีเวลาโหลดสคริปต์ ณ ขณะนี้ ฟังก์ชัน loadScript
ยังไม่มีวิธีการติดตามความสมบูรณ์ของการโหลด สคริปต์จะโหลดและรันในที่สุดเท่านั้นเอง แต่เราต้องการทราบว่าเมื่อไรจะเกิดขึ้น เพื่อใช้ฟังก์ชันและตัวแปรใหม่จากสคริปต์นั้น
มาเพิ่มฟังก์ชัน callback
เป็นอาร์กิวเมนต์ที่สองให้กับ loadScript
ที่ควรดำเนินการเมื่อสคริปต์โหลด:
ฟังก์ชั่น loadScript (src, โทรกลับ) { ให้ script = document.createElement('script'); script.src = src; script.onload = () => โทรกลับ (สคริปต์); document.head.append(สคริปต์); -
เหตุการณ์ onload
อธิบายไว้ในบทความ การโหลดทรัพยากร: onload และ onerror โดยพื้นฐานแล้วจะเรียกใช้ฟังก์ชันหลังจากโหลดและเรียกใช้สคริปต์แล้ว
ตอนนี้หากเราต้องการเรียกใช้ฟังก์ชันใหม่จากสคริปต์ เราควรเขียนสิ่งนั้นในการเรียกกลับ:
loadScript('/my/script.js', ฟังก์ชั่น() { // โทรกลับจะทำงานหลังจากโหลดสคริปต์แล้ว ฟังก์ชั่นใหม่(); // ตอนนี้มันใช้งานได้แล้ว - -
นั่นคือแนวคิด: อาร์กิวเมนต์ที่สองคือฟังก์ชัน (โดยปกติจะไม่ระบุชื่อ) ที่ทำงานเมื่อการกระทำเสร็จสิ้น
นี่คือตัวอย่างที่รันได้พร้อมสคริปต์จริง:
ฟังก์ชั่น loadScript (src, โทรกลับ) { ให้ script = document.createElement('script'); script.src = src; script.onload = () => โทรกลับ (สคริปต์); document.head.append(สคริปต์); - loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', สคริปต์ => { alert(`เจ๋งมาก, โหลดสคริปต์ ${script.src} แล้ว`); เตือน( _ ); // _ เป็นฟังก์ชันที่ประกาศไว้ในสคริปต์ที่โหลด -
นั่นเรียกว่าสไตล์การเขียนโปรแกรมแบบอะซิงโครนัสแบบ "เรียกกลับ" ฟังก์ชันที่ทำบางอย่างแบบอะซิงโครนัสควรจัดให้มีอาร์กิวเมนต์ callback
โดยที่เรากำหนดให้ฟังก์ชันทำงานหลังจากที่ฟังก์ชันเสร็จสมบูรณ์
ที่นี่เราทำใน loadScript
แต่แน่นอนว่ามันเป็นแนวทางทั่วไป
เราจะโหลดสคริปต์สองตัวตามลำดับได้อย่างไร: สคริปต์แรก และสคริปต์ที่สองหลังจากนั้น
วิธีแก้ปัญหาตามธรรมชาติคือใส่การเรียก loadScript
ครั้งที่สองไว้ในการโทรกลับ เช่นนี้
loadScript('/my/script.js', ฟังก์ชั่น (สคริปต์) { alert(`เอาล่ะ ${script.src} โหลดแล้ว มาโหลดกันใหม่ดีกว่า''); loadScript('/my/script2.js', ฟังก์ชั่น (สคริปต์) { alert(`เยี่ยมครับ โหลดสคริปต์ที่สองแล้ว`); - -
หลังจากที่ loadScript
ภายนอกเสร็จสมบูรณ์แล้ว การเรียกกลับจะเริ่มต้นการทำงานภายใน
จะเป็นอย่างไรถ้าเราต้องการอีกหนึ่งสคริปต์…?
loadScript('/my/script.js', ฟังก์ชั่น (สคริปต์) { loadScript('/my/script2.js', ฟังก์ชั่น (สคริปต์) { loadScript('/my/script3.js', ฟังก์ชั่น (สคริปต์) { // ...ดำเนินการต่อหลังจากโหลดสคริปต์ทั้งหมดแล้ว - - -
ดังนั้นทุกการกระทำใหม่จึงอยู่ในการโทรกลับ เป็นเรื่องปกติสำหรับการกระทำบางอย่าง แต่ไม่เป็นผลดีสำหรับหลาย ๆ คน ดังนั้นเราจะเห็นรูปแบบอื่น ๆ เร็วๆ นี้
ในตัวอย่างข้างต้น เราไม่ได้พิจารณาถึงข้อผิดพลาด จะเกิดอะไรขึ้นหากการโหลดสคริปต์ล้มเหลว การโทรกลับของเราควรจะสามารถตอบสนองต่อสิ่งนั้นได้
นี่คือ loadScript
เวอร์ชันปรับปรุงที่ติดตามข้อผิดพลาดในการโหลด:
ฟังก์ชั่น loadScript (src, โทรกลับ) { ให้ script = document.createElement('script'); script.src = src; script.onload = () => โทรกลับ (null, สคริปต์); script.onerror = () => โทรกลับ (ข้อผิดพลาดใหม่ (`ข้อผิดพลาดในการโหลดสคริปต์สำหรับ ${src}`)); document.head.append(สคริปต์); -
มันจะเรียก callback(null, script)
สำหรับการโหลดที่สำเร็จและ callback(error)
มิฉะนั้น
การใช้งาน:
loadScript('/my/script.js', ฟังก์ชั่น (ข้อผิดพลาด, สคริปต์) { ถ้า (ข้อผิดพลาด) { //จัดการข้อผิดพลาด } อื่น { // โหลดสคริปต์สำเร็จ - -
เป็นอีกครั้งที่สูตรที่เราใช้สำหรับ loadScript
นั้นค่อนข้างธรรมดา เรียกว่ารูปแบบ "การโทรกลับข้อผิดพลาดก่อน"
อนุสัญญาคือ:
อาร์กิวเมนต์แรกของ callback
ถูกสงวนไว้สำหรับข้อผิดพลาดหากเกิดขึ้น จากนั้น callback(err)
จะถูกเรียก
อาร์กิวเมนต์ที่สอง (และอาร์กิวเมนต์ถัดไปหากจำเป็น) มีไว้สำหรับผลลัพธ์ที่ประสบความสำเร็จ จากนั้นจะมีการเรียก callback(null, result1, result2…)
ดังนั้นจึงใช้ฟังก์ชัน callback
เดี่ยวทั้งในการรายงานข้อผิดพลาดและส่งกลับผลลัพธ์
เมื่อมองแวบแรก ดูเหมือนว่าแนวทางการเข้ารหัสแบบอะซิงโครนัสจะเป็นไปได้ และก็เป็นเช่นนั้นจริงๆ สำหรับการโทรแบบซ้อนหนึ่งหรือสองครั้งก็ดูดี
แต่สำหรับการดำเนินการแบบอะซิงโครนัสหลายครั้งที่ตามมา เราจะมีโค้ดดังนี้:
loadScript('1.js', ฟังก์ชั่น (ข้อผิดพลาด, สคริปต์) { ถ้า (ข้อผิดพลาด) { handleError(ข้อผิดพลาด); } อื่น { - loadScript('2.js', ฟังก์ชั่น (ข้อผิดพลาด, สคริปต์) { ถ้า (ข้อผิดพลาด) { handleError(ข้อผิดพลาด); } อื่น { - loadScript('3.js', ฟังก์ชั่น (ข้อผิดพลาด, สคริปต์) { ถ้า (ข้อผิดพลาด) { handleError(ข้อผิดพลาด); } อื่น { // ...ดำเนินการต่อหลังจากโหลดสคริปต์ทั้งหมดแล้ว (*) - - - - - -
ในโค้ดด้านบน:
เราโหลด 1.js
แล้วหากไม่มีข้อผิดพลาด...
เราโหลด 2.js
แล้วหากไม่มีข้อผิดพลาด...
เราโหลด 3.js
แล้วหากไม่มีข้อผิดพลาด ให้ทำอย่างอื่น (*)
เมื่อการโทรซ้อนกันมากขึ้น โค้ดก็จะลึกขึ้นและยากขึ้นในการจัดการ โดยเฉพาะอย่างยิ่งถ้าเรามีโค้ดจริงแทนที่จะเป็น ...
ซึ่งอาจรวมถึงการวนซ้ำ คำสั่งแบบมีเงื่อนไข และอื่นๆ
บางครั้งเรียกว่า "นรกเรียกกลับ" หรือ "ปิรามิดแห่งความหายนะ"
“ปิรามิด” ของการเรียกแบบซ้อนจะขยายไปทางขวาทุกครั้งที่มีการดำเนินการแบบอะซิงโครนัส ในไม่ช้ามันก็ลุกลามจนควบคุมไม่ได้
ดังนั้นวิธีการเข้ารหัสแบบนี้จึงไม่ค่อยดีนัก
เราสามารถพยายามบรรเทาปัญหาได้โดยทำให้ทุกการกระทำเป็นฟังก์ชันแบบสแตนด์อโลน เช่นนี้
loadScript('1.js', ขั้นตอนที่ 1); ฟังก์ชั่น step1 (ข้อผิดพลาดสคริปต์) { ถ้า (ข้อผิดพลาด) { handleError(ข้อผิดพลาด); } อื่น { - loadScript('2.js', ขั้นตอนที่ 2); - - ฟังก์ชั่น step2 (ข้อผิดพลาดสคริปต์) { ถ้า (ข้อผิดพลาด) { handleError(ข้อผิดพลาด); } อื่น { - loadScript('3.js', ขั้นตอนที่ 3); - - ฟังก์ชั่น step3 (ข้อผิดพลาดสคริปต์) { ถ้า (ข้อผิดพลาด) { handleError(ข้อผิดพลาด); } อื่น { // ...ดำเนินการต่อหลังจากโหลดสคริปต์ทั้งหมดแล้ว (*) - -
ดู? มันทำสิ่งเดียวกัน และตอนนี้ไม่มีการซ้อนลึกเนื่องจากเราทำให้ทุกการกระทำเป็นฟังก์ชันระดับบนสุดที่แยกจากกัน
ใช้งานได้ แต่โค้ดดูเหมือนสเปรดชีตที่ขาดออกจากกัน อ่านยาก และคุณอาจสังเกตเห็นว่าเราต้องมองข้ามส่วนต่างๆ ในขณะที่อ่าน นั่นไม่สะดวก โดยเฉพาะอย่างยิ่งหากผู้อ่านไม่คุ้นเคยกับโค้ดและไม่รู้ว่าจะข้ามไปตรงไหน
นอกจากนี้ ฟังก์ชันชื่อ step*
เป็นแบบใช้ครั้งเดียวทั้งหมด ซึ่งสร้างขึ้นเพื่อหลีกเลี่ยง "ปิรามิดแห่งความหายนะ" เท่านั้น จะไม่มีใครนำสิ่งเหล่านั้นกลับมาใช้ใหม่นอกห่วงโซ่การดำเนินการ จึงมีเนมสเปซเกะกะเล็กน้อยที่นี่
เราอยากได้สิ่งที่ดีกว่านี้
โชคดีที่มีวิธีอื่นในการหลีกเลี่ยงปิรามิดเช่นนี้ วิธีที่ดีที่สุดวิธีหนึ่งคือการใช้ “คำสัญญา” ซึ่งจะอธิบายในบทถัดไป