
ก่อนที่จะศึกษาเนื้อหาของบทความนี้ เราต้องเข้าใจแนวคิดของอะซิงโครนัสก่อน สิ่งแรกที่ต้องเน้นคือ มีความแตกต่างที่สำคัญระหว่างอะซิงโครนัสและขนาน
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 ก็เป็นทางออกที่ดีที่สุด