ก่อนที่จะศึกษาเนื้อหาของบทความนี้ เราต้องเข้าใจแนวคิดของอะซิงโครนัสก่อน สิ่งแรกที่ต้องเน้นคือ มีความแตกต่างที่สำคัญระหว่างอะซิงโครนัสและขนาน
CPU
ความเท่าเทียมหมายถึงการประมวลผลแบบขนาน ซึ่ง CPU
ว่าคำสั่งหลายคำสั่งจะถูกดำเนินการในเวลาเดียวกันCPU
จะพักงานปัจจุบันไว้ชั่วคราว ประมวลผลงานถัดไปก่อน จากนั้นจึงกลับไปยังงานก่อนหน้าเพื่อดำเนินการต่อไปหลังจากได้รับการแจ้งเตือนการเรียกกลับของงานก่อนหน้า กระบวนการทั้งหมดไม่จำเป็นต้องมี หัวข้อที่สองบางทีมันอาจจะง่ายกว่าที่จะอธิบายความเท่าเทียม การซิงโครไนซ์ และความอะซิงโครนัสในรูปแบบของรูปภาพ สมมติว่ามีสองงาน A และ B ที่จำเป็นต้องได้รับการประมวลผล วิธีการประมวลผลแบบขนาน ซิงโครนัส และอะซิงโครนัสจะใช้วิธีการดำเนินการตามที่แสดงใน รูปต่อไปนี้:
JavaScript
ให้ฟังก์ชันแบบอะซิงโครนัสมากมายแก่เรา ฟังก์ชันเหล่านี้ช่วยให้เราสามารถดำเนินงานแบบอะซิงโครนัสได้อย่างสะดวก กล่าวคือ เราเริ่มดำเนินการงาน (ฟังก์ชัน) ทันที แต่งานจะเสร็จสิ้นในภายหลัง และเวลาที่กำหนดให้เสร็จสิ้น ไม่แน่ใจ.
ตัวอย่างเช่น ฟังก์ชัน setTimeout
เป็นฟังก์ชันอะซิงโครนัสทั่วไป นอกจากนี้ fs.readFile
และ fs.writeFile
ยังเป็นฟังก์ชันอะซิงโครนัสอีกด้วย
เราสามารถกำหนดกรณีงานแบบอะซิงโครนัสได้ด้วยตัวเอง เช่น การปรับแต่งฟังก์ชันการคัดลอกไฟล์ copyFile(from,to)
:
const fs = need('fs') ฟังก์ชั่น copyFile (จาก, ถึง) { 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',(ผิดพลาด,ข้อมูล)=>{ - })
หากไม่ได้สร้างไฟล์ ./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: The Immortal Sect นั้นดีไม่สิ้นสุด แต่มันขาดผู้ชายคนหนึ่งไป การอ่านไฟล์ B: หากคุณต้องการเข้าร่วม Immortal Sect คุณต้องมีลิงก์
http://t.csdn.cn/H1faI
คุณสามารถอ่านไฟล์ B ได้ทันทีหลังจากอ่านไฟล์ A ผ่านการโทรกลับ
จะเป็นอย่างไรหากเราต้องการอ่านไฟล์ C ต่อจากไฟล์ B? สิ่งนี้ต้องมีการซ้อนการโทรกลับอย่างต่อเนื่อง:
fs.readFile('./A.txt', (err, data) => {//First callback if (err) { console.log (ผิดพลาด. ข้อความ) กลับ - console.log('อ่านไฟล์ A: ' + data.toString()) fs.readFile('./B.txt', (ผิดพลาด, ข้อมูล) => {//โทรกลับครั้งที่สองถ้า (ผิดพลาด) { 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
ก็เป็นทางออกที่ดีที่สุด