ก่อนที่จะศึกษาเนื้อหาของบทความนี้ เราต้องเข้าใจแนวคิดของอะซิงโครนัสก่อน สิ่งแรกที่ต้องเน้นคือ มี ความแตกต่างที่สำคัญระหว่าง อะซิงโครนัส และ ขนาน
โดย CPU
ความเท่าเทียมหมายถึงการประมวลผลแบบขนาน ซึ่ง CPU
ว่าคำสั่งหลายคำสั่งจะถูกดำเนินการในเวลาเดียวกัน
โดยทั่วไปการซิงโครไนซ์หมายถึงการดำเนินการตามลำดับที่กำหนดไว้ล่วงหน้าเท่านั้น เมื่องานก่อนหน้าเสร็จสิ้น งานถัดไปจะถูกดำเนินการเท่านั้น
แบบอะซิงโครนัสซึ่งสอดคล้องกับการซิงโครไนซ์หมายความว่า CPU
จะพักงานปัจจุบันไว้ชั่วคราว ประมวลผลงานถัดไปก่อน จากนั้นจึงกลับไปยังงานก่อนหน้าเพื่อดำเนินการต่อไปหลังจากได้รับการแจ้งเตือนการเรียกกลับของงานก่อนหน้า กระบวนการทั้งหมดไม่จำเป็นต้องมี หัวข้อที่สอง
บางทีมันอาจจะง่ายกว่าที่จะอธิบายความเท่าเทียม การซิงโครไนซ์ และความอะซิงโครนัสในรูปแบบของรูปภาพ สมมติว่ามีสองงาน A และ B ที่จำเป็นต้องได้รับการประมวลผล วิธีการประมวลผลแบบขนาน ซิงโครนัส และอะซิงโครนัสจะใช้วิธีการดำเนินการตามที่แสดงใน รูปต่อไปนี้:
JavaScript
ให้ฟังก์ชันแบบอะซิงโครนัสมากมายแก่เรา ฟังก์ชันเหล่านี้ช่วยให้เราสามารถดำเนินงานแบบอะซิงโครนัสได้อย่างสะดวก กล่าวคือ เราเริ่มดำเนินการงาน (ฟังก์ชัน) ทันที แต่งานจะเสร็จสิ้นในภายหลัง และเวลาที่กำหนดให้เสร็จสิ้น ไม่แน่ใจ.
ตัวอย่างเช่น ฟังก์ชัน setTimeout
เป็นฟังก์ชันอะซิงโครนัสทั่วไป นอกจากนี้ fs.readFile
และ fs.writeFile
ยังเป็นฟังก์ชันอะซิงโครนัสอีกด้วย
เราสามารถกำหนดกรณีงานแบบอะซิงโครนัสได้ด้วยตัวเอง เช่น การปรับแต่งฟังก์ชันการคัดลอกไฟล์ copyFile(from,to)
:
const fs = need('fs')function copyFile(from, to) { fs.readFile(จาก, (ผิดพลาด, ข้อมูล) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - fs.writeFile (ถึง, ข้อมูล, (ผิดพลาด) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log('คัดลอกเสร็จแล้ว') - })}
ฟังก์ชัน copyFile
จะอ่านข้อมูลไฟล์จากพารามิเตอร์ from
ก่อน จากนั้นจึงเขียนข้อมูลลงในไฟล์ที่พารามิเตอร์ชี้ไป to
เราสามารถเรียก copyFile
ได้ดังนี้
copyFile('./from.txt','./to.txt')//Copy ไฟล์
ถ้ามีโค้ดอื่นตามหลัง copyFile(...)
ในเวลานี้โปรแกรมจะไม่ wait การดำเนินการของ copyFile
สิ้นสุดลง แต่จะดำเนินการลงด้านล่างโดยตรง โปรแกรมไม่สนใจเมื่องานคัดลอกไฟล์สิ้นสุดลง
copyFile('./from.txt','./to.txt')//โค้ดต่อไปนี้จะไม่รอให้โค้ดด้านบนนี้สิ้นสุด...
ณ จุดนี้ ทุกอย่างดูเหมือนจะเป็นปกติ แต่ถ้า จะเกิดอะไรขึ้นหากคุณเข้าถึงเนื้อหาของไฟล์ ./to.txt
โดยตรงหลังจากฟังก์ชัน copyFile(...)
สิ่งนี้จะไม่อ่านเนื้อหาที่คัดลอก เช่นนี้:
copyFile('./from.txt','./to.txt')fs.readFile('./to.txt',(err,data)= >{ ...})
หากไม่ได้สร้างไฟล์ ./to.txt
ก่อนที่จะรันโปรแกรม คุณจะได้รับข้อผิดพลาดต่อไปนี้:
PS E:CodeNodedemos 3-callback> node .index.js ที่เสร็จเรียบร้อย คัดลอกเสร็จแล้ว PS E:CodeNodedemos 3-callback> โหนด .index.js ข้อผิดพลาด: ENOENT: ไม่มีไฟล์หรือไดเร็กทอรีดังกล่าว ให้เปิด 'E:CodeNodedemos 3-callbackto.txt'คัดลอกเสร็จแล้ว
แม้ว่าจะมี ./to.txt
อยู่ แต่เนื้อหาที่คัดลอกก็ไม่สามารถอ่านได้
สาเหตุของปรากฏการณ์นี้คือ: copyFile(...)
ถูกดำเนินการแบบอะซิงโครนัส หลังจากที่โปรแกรมรันฟังก์ชัน copyFile(...)
แล้ว โปรแกรมจะไม่รอให้การคัดลอกเสร็จสมบูรณ์ แต่จะดำเนินการลงด้านล่างโดยตรง ทำให้เกิดไฟล์ ปรากฏขึ้น . ./to.txt
ไม่มีข้อผิดพลาด หรือเนื้อหาไฟล์เป็นข้อผิดพลาดว่างเปล่า (หากไฟล์ถูกสร้างขึ้นล่วงหน้า)
ไม่สามารถกำหนดเวลาสิ้นสุดการดำเนินการเฉพาะของฟังก์ชันการเรียกกลับได้ ตัวอย่างเช่น เวลาสิ้นสุดการดำเนินการของฟังก์ชัน readFile(from,to)
มักจะขึ้นอยู่กับขนาดของไฟล์ from
.
ดังนั้น คำถามคือ เราจะค้นหาจุดสิ้นสุดของการดำเนิน copyFile
และอ่านเนื้อหาของไฟล์ to
ได้อย่างไร
สิ่งนี้จำเป็นต้องใช้ฟังก์ชันการโทรกลับ เราสามารถแก้ไขฟังก์ชัน copyFile
ได้ดังนี้:
function copyFile(from, to, callback) { fs.readFile(จาก, (ผิดพลาด, ข้อมูล) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - fs.writeFile (ถึง, ข้อมูล, (ผิดพลาด) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log('คัดลอกเสร็จแล้ว') callback()//ฟังก์ชัน Callback จะถูกเรียกเมื่อการคัดลอกเสร็จสิ้น}) })}
ด้วยวิธีนี้ หากเราจำเป็นต้องดำเนินการบางอย่างทันทีหลังจากการคัดลอกไฟล์เสร็จสิ้น เราสามารถเขียนการดำเนินการเหล่านี้ลงในฟังก์ชันการโทรกลับได้:
function copyFile(from, to, callback) { fs.readFile(จาก, (ผิดพลาด, ข้อมูล) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - fs.writeFile (ถึง, ข้อมูล, (ผิดพลาด) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log('คัดลอกเสร็จแล้ว') callback()//ฟังก์ชัน Callback จะถูกเรียกเมื่อการคัดลอกเสร็จสิ้น}) })}copyFile('./from.txt', './to.txt', ฟังก์ชัน () { //ส่งผ่านฟังก์ชันโทรกลับ อ่านเนื้อหาของไฟล์ "to.txt" และเอาต์พุต fs.readFile('./to.txt', (err, data) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log(data.toString()) })})
หากคุณได้เตรียมไฟล์ ./from.txt
โค้ดข้างต้นสามารถเรียกใช้ได้โดยตรง:
PS E:CodeNodedemos 3-callback> node .index.js คัดลอกเสร็จแล้ว เข้าร่วมชุมชน "Xianzong" และปลูกฝังความเป็นอมตะกับฉัน ที่อยู่ชุมชน: http://t.csdn.cn/EKf1h
วิธีการเขียนโปรแกรมนี้เรียกว่ารูปแบบการเขียนโปรแกรมแบบอะซิงโครนัส "แบบเรียกกลับ" ใช้ในการโทรหลังจากงานสิ้นสุด
ลักษณะนี้เป็นเรื่องปกติในการเขียน JavaScript
ตัวอย่างเช่น ฟังก์ชันการอ่านไฟล์ fs.readFile
และ fs.writeFile
ล้วนเป็นฟังก์ชันแบบอะซิงโครนัส
การโทรกลับสามารถจัดการเรื่องที่ตามมาได้อย่างถูกต้องหลังจากงานอะซิงโครนัสเสร็จสิ้น หากเราจำเป็นต้องดำเนินการแบบอะซิงโครนัสหลายครั้ง เราจำเป็นต้องซ้อนฟังก์ชันการโทรกลับ
สถานการณ์กรณี:
การใช้โค้ดสำหรับการอ่านไฟล์ A และไฟล์ B ตามลำดับ:
fs.readFile('./A.txt', (err, data) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log('อ่านไฟล์ A: ' + data.toString()) fs.readFile('./B.txt', (ผิดพลาด, ข้อมูล) => { ถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log("อ่านไฟล์ B: " + data.toString()) })})
ผลการดำเนินการ:
PS E:CodeNodedemos 3-callback> node .index.js การอ่านไฟล์ A: Immortal Sect นั้นดีไม่สิ้นสุด แต่ขาดใครบางคนไป การอ่านไฟล์ B: หากคุณต้องการเข้าร่วม Immortal Sect คุณต้องมีลิงก์ http://t.csdn.cn/H1faI
คุณสามารถอ่านผ่านการโทรกลับได้ ไฟล์ หลังจาก A ไฟล์ B จะถูกอ่านทันที
จะเป็นอย่างไรหากเราต้องการอ่านไฟล์ C ต่อจากไฟล์ B? สิ่งนี้ต้องมีการซ้อนการโทรกลับอย่างต่อเนื่อง:
fs.readFile('./A.txt', (err, data) => {//First callback if (err) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log('อ่านไฟล์ A: ' + data.toString()) fs.readFile('./B.txt', (ผิดพลาด, data) => {//โทรกลับครั้งที่สองถ้า (ผิดพลาด) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log("อ่านไฟล์ B: " + data.toString()) fs.readFile('./C.txt',(err,data)=>{//การติดต่อกลับครั้งที่สาม... - })})
กล่าวอีกนัยหนึ่ง หากเราต้องการดำเนินการแบบอะซิงโครนัสหลายครั้งตามลำดับ เราจำเป็นต้องมีการโทรกลับแบบซ้อนหลายระดับ ซึ่งจะมีผลเมื่อจำนวนระดับน้อย แต่เมื่อมีเวลาซ้อนมากเกินไป ปัญหาบางอย่างจะเกิดขึ้น เกิดขึ้น.
แบบแผนการเรียกกลับ
ที่จริงแล้ว รูปแบบของฟังก์ชันการเรียกกลับใน fs.readFile
ไม่ใช่ข้อยกเว้น แต่เป็นแบบแผนทั่วไปใน JavaScript
เราจะปรับแต่งฟังก์ชันการโทรกลับจำนวนมากในอนาคต และเราจำเป็นต้องปฏิบัติตามข้อตกลงนี้และสร้างนิสัยการเขียนโค้ดที่ดี
หลักการคือ:
callback
ถูกสงวนไว้สำหรับข้อผิดพลาด เมื่อเกิดข้อผิดพลาด callback(err)
จะถูกเรียกcallback(null, result1, result2,...)
จะถูกเรียกตามรูปแบบข้างต้น ฟังก์ชันการเรียกกลับมีสองฟังก์ชัน: การจัดการข้อผิดพลาดและการรับผลลัพธ์ ตัวอย่างเช่น ฟังก์ชันการเรียกกลับของ fs.readFile('...',(err,data)=>{})
เป็นไปตามระเบียบนี้
หากเราไม่เจาะลึกลงไปอีก การประมวลผลวิธีแบบอะซิงโครนัสที่ยึดตามการโทรกลับดูเหมือนจะเป็นวิธีที่สมบูรณ์แบบในการจัดการกับมัน ปัญหาคือถ้าเรามีพฤติกรรมอะซิงโครนัสซ้ำแล้วซ้ำอีก โค้ดจะมีลักษณะดังนี้:
fs.readFile('./a.txt',(err,data)=>{ ถ้า(ผิดพลาด){ console.log (ผิดพลาด. ข้อความ) กลับ - //อ่านการดำเนินการผลลัพธ์ fs.readFile('./b.txt',(err,data)=>{ ถ้า(ผิดพลาด){ console.log (ผิดพลาด. ข้อความ) กลับ - //อ่านการดำเนินการผลลัพธ์ fs.readFile('./c.txt',(err,data)=>{ ถ้า(ผิดพลาด){ console.log (ผิดพลาด. ข้อความ) กลับ - //อ่านการดำเนินการผลลัพธ์ fs.readFile('./d.txt',(err,data)=>{ ถ้า(ผิดพลาด){ console.log (ผิดพลาด. ข้อความ) กลับ - - - - })})
เนื้อหาการดำเนินการของโค้ดข้างต้นคือ:
เมื่อจำนวนการเรียกเพิ่มขึ้น ระดับการซ้อนโค้ดจะลึกขึ้นเรื่อยๆ รวมถึงคำสั่งแบบมีเงื่อนไขมากขึ้นเรื่อยๆ ส่งผลให้โค้ดสับสนที่มีการเยื้องไปทางขวาอยู่ตลอดเวลา ทำให้อ่านและอ่านได้ยาก บำรุงรักษา.
เราเรียกปรากฏการณ์นี้ว่าการเติบโตอย่างต่อเนื่องไปทางขวา (เยื้องไปทางขวา) " callback hell " หรือ " ปิรามิดแห่งความหายนะ "!
fs.readFile('a.txt',(ผิดพลาด,ข้อมูล)=>{ fs.readFile('b.txt',(ผิดพลาด,ข้อมูล)=>{ fs.readFile('c.txt',(ผิดพลาด,ข้อมูล)=>{ fs.readFile('d.txt',(ผิดพลาด,ข้อมูล)=>{ fs.readFile('e.txt',(ผิดพลาด,ข้อมูล)=>{ fs.readFile('f.txt',(ผิดพลาด,ข้อมูล)=>{ fs.readFile('g.txt',(ผิดพลาด,ข้อมูล)=>{ fs.readFile('h.txt',(ผิดพลาด,ข้อมูล)=>{ - - ประตูสู่นรก ===> - - - - - - - })})
แม้ว่าโค้ดด้านบนจะดูค่อนข้างปกติ แต่ก็เป็นเพียงสถานการณ์ในอุดมคติ โดยปกติแล้วจะมีคำสั่งแบบมีเงื่อนไขจำนวนมาก การดำเนินการประมวลผลข้อมูล และโค้ดอื่นๆ ในตรรกะทางธุรกิจ ซึ่งขัดขวางลำดับที่สวยงามในปัจจุบันและทำให้ การเปลี่ยนแปลงรหัสทำได้ยาก
โชคดีที่ JavaScript
มีวิธีแก้ปัญหามากมายให้เรา และ Promise
ก็เป็นทางออกที่ดีที่สุด