“ callback hell” คืออะไรและ RX แก้อย่างไรและทำไม?


113

ใครช่วยให้คำจำกัดความที่ชัดเจนพร้อมกับตัวอย่างง่ายๆที่อธิบายว่า "callback hell" คืออะไรสำหรับคนที่ไม่รู้จัก JavaScript และ node.js

"ปัญหานรกเรียกกลับ" (ในการตั้งค่าแบบใด) เกิดขึ้นเมื่อใด

ทำไมมันถึงเกิดขึ้น?

"callback hell" เกี่ยวข้องกับการคำนวณแบบอะซิงโครนัสเสมอหรือไม่

หรือ "callback hell" สามารถเกิดขึ้นในแอปพลิเคชันเธรดเดียวได้หรือไม่?

ฉันเข้าเรียนหลักสูตร Reactive ที่ Coursera และ Erik Meijer กล่าวในการบรรยายครั้งหนึ่งของเขาว่า RX ช่วยแก้ปัญหา "นรกเรียกกลับ" ได้ ฉันถามว่า "นรกเรียกกลับ" ในฟอรัม Coursera คืออะไร แต่ฉันไม่ได้รับคำตอบที่ชัดเจน

หลังจากอธิบาย "callback hell" ในตัวอย่างง่ายๆคุณสามารถแสดงให้เห็นว่า RX แก้ปัญหา "callback hell" ในตัวอย่างง่ายๆนั้นได้อย่างไร

คำตอบ:


136

1) "callback hell" สำหรับคนที่ไม่รู้จัก javascript และ node.js คืออะไร?

คำถามอื่นนี้มีตัวอย่างของ Javascript callback hell: วิธีหลีกเลี่ยงการซ้อนฟังก์ชันอะซิงโครนัสใน Node.js เป็นเวลานาน

ปัญหาใน Javascript คือวิธีเดียวที่จะ "หยุด" การคำนวณและให้ "ส่วนที่เหลือ" ดำเนินการหลัง (แบบอะซิงโครนัส) คือการใส่ "ส่วนที่เหลือ" ไว้ในการเรียกกลับ

ตัวอย่างเช่นสมมติว่าฉันต้องการเรียกใช้โค้ดที่มีลักษณะดังนี้:

x = getData();
y = getMoreData(x);
z = getMoreData(y);
...

จะเกิดอะไรขึ้นถ้าตอนนี้ฉันต้องการทำให้ฟังก์ชัน getData เป็นแบบอะซิงโครนัสหมายความว่าฉันมีโอกาสเรียกใช้โค้ดอื่น ๆ ในขณะที่ฉันกำลังรอให้พวกเขาส่งคืนค่า ใน Javascript วิธีเดียวที่จะเขียนทุกอย่างที่สัมผัสกับการคำนวณ async โดยใช้สไตล์การส่งต่อแบบต่อเนื่อง :

getData(function(x){
    getMoreData(x, function(y){
        getMoreData(y, function(z){ 
            ...
        });
    });
});

ฉันไม่คิดว่าจะต้องโน้มน้าวใครว่าเวอร์ชันนี้น่าเกลียดกว่าเวอร์ชั่นก่อน :-)

2) "ปัญหานรกเรียกกลับ" (ในการตั้งค่าแบบใด) เกิดขึ้นเมื่อใด?

เมื่อคุณมีฟังก์ชันโทรกลับมากมายในโค้ดของคุณ! มันจะยากขึ้นที่จะทำงานร่วมกับพวกเขายิ่งคุณมีโค้ดของคุณมากขึ้นและมันจะแย่เป็นพิเศษเมื่อคุณต้องทำลูปบล็อคลองจับและอะไรทำนองนั้น

ตัวอย่างเช่นเท่าที่ฉันทราบใน JavaScript เป็นวิธีเดียวที่จะเรียกใช้ชุดของฟังก์ชันอะซิงโครนัสโดยที่ฟังก์ชันหนึ่งถูกเรียกใช้หลังจากการส่งคืนก่อนหน้านี้ใช้ฟังก์ชันแบบเรียกซ้ำ คุณไม่สามารถใช้ for loop

// we would like to write the following
for(var i=0; i<10; i++){
    doSomething(i);
}
blah();

แต่เราอาจต้องเขียน:

function loop(i, onDone){
    if(i >= 10){
        onDone()
    }else{
        doSomething(i, function(){
            loop(i+1, onDone);
        });
     }
}
loop(0, function(){
    blah();
});

//ugh!

จำนวนคำถามที่เราได้รับที่นี่ใน StackOverflow ที่ถามว่าจะทำสิ่งนี้ได้อย่างไรเป็นเครื่องพิสูจน์ว่ามันสับสนแค่ไหน :)

3) ทำไมจึงเกิดขึ้น?

เกิดขึ้นเนื่องจากใน JavaScript วิธีเดียวที่จะหน่วงเวลาการคำนวณเพื่อให้ทำงานหลังจากการเรียกคืนแบบอะซิงโครนัสคือการใส่รหัสล่าช้าไว้ในฟังก์ชันเรียกกลับ คุณไม่สามารถหน่วงเวลาโค้ดที่เขียนในรูปแบบซิงโครนัสแบบเดิมได้ดังนั้นคุณจึงต้องใช้การโทรกลับแบบซ้อนกันทุกที่

4) หรือ "callback hell" สามารถเกิดขึ้นในแอปพลิเคชันเธรดเดียวได้หรือไม่?

การเขียนโปรแกรมแบบอะซิงโครนัสเกี่ยวข้องกับการทำงานพร้อมกันในขณะที่เธรดเดี่ยวเกี่ยวข้องกับการขนานกัน ทั้งสองแนวคิดไม่ใช่สิ่งเดียวกัน

คุณยังสามารถมีโค้ดพร้อมกันในบริบทเธรดเดียว ในความเป็นจริง JavaScript ราชินีแห่งนรกเรียกกลับเป็นเธรดเดียว

อะไรคือความแตกต่างระหว่างภาวะพร้อมกันและความขนาน?

5) คุณช่วยแสดงให้ดูได้ไหมว่า RX แก้ปัญหา "callback hell" ในตัวอย่างง่ายๆนั้นได้อย่างไร

ฉันไม่รู้อะไรเกี่ยวกับ RX เป็นพิเศษ แต่โดยปกติแล้วปัญหานี้จะได้รับการแก้ไขโดยการเพิ่มการรองรับเนทีฟสำหรับการคำนวณแบบอะซิงโครนัสในภาษาโปรแกรม การใช้งานอาจแตกต่างกันไปและรวมถึง: async, generators, coroutines และ callcc

ใน Python เราสามารถนำตัวอย่างลูปก่อนหน้านั้นไปใช้กับบางสิ่งตามบรรทัดของ:

def myLoop():
    for i in range(10):
        doSomething(i)
        yield

myGen = myLoop()

นี่ไม่ใช่โค้ดแบบเต็ม แต่แนวคิดก็คือ "yield" จะหยุดการวนซ้ำของเราชั่วคราวจนกว่าจะมีคนเรียก myGen.next () สิ่งสำคัญคือเรายังสามารถเขียนโค้ดโดยใช้ for loop ได้โดยไม่จำเป็นต้องเปิดตรรกะ "inside out" เหมือนที่เราต้องทำในloopฟังก์ชันวนซ้ำนั้น


ดังนั้นการเรียกกลับนรกสามารถเกิดขึ้นได้ในการตั้งค่า async เท่านั้น? ถ้ารหัสของฉันเป็นแบบซิงโครนัสอย่างสมบูรณ์ (เช่นไม่มีการทำงานพร้อมกัน) จะเกิด "นรกเรียกกลับ" ไม่ได้ถ้าฉันเข้าใจคำตอบของคุณถูกต้องใช่ไหม
jhegedus

Callback hell มีส่วนเกี่ยวข้องกับความน่ารำคาญในการเขียนโค้ดโดยใช้รูปแบบการส่งต่อแบบต่อเนื่อง ในทางทฤษฎีแล้วคุณยังคงสามารถเขียนฟังก์ชันทั้งหมดของคุณใหม่โดยใช้สไตล์ CPS ได้แม้ในโปรแกรมทั่วไป (บทความวิกิพีเดียมีตัวอย่าง) แต่ด้วยเหตุผลที่ดีคนส่วนใหญ่ไม่ทำเช่นนั้น โดยปกติเราจะใช้รูปแบบการส่งผ่านความต่อเนื่องหากเราถูกบังคับเท่านั้นซึ่งเป็นกรณีของการเขียนโปรแกรม Javascript async
hugomg

btw ฉัน googled สำหรับส่วนขยายที่ตอบสนองและฉันได้รับความประทับใจว่าพวกเขาคล้ายกับไลบรารี Promise มากกว่าและไม่ใช่ส่วนขยายภาษาที่แนะนำไวยากรณ์ async คำสัญญาจะช่วยจัดการกับการซ้อนการเรียกกลับและด้วยการจัดการข้อยกเว้น แต่ไม่เรียบร้อยเท่าส่วนขยายไวยากรณ์ สำหรับลูปยังคงสร้างความรำคาญให้กับโค้ดและคุณยังต้องแปลโค้ดจากสไตล์ซิงโครนัสเป็นรูปแบบสัญญา
hugomg

1
ฉันควรชี้แจงว่า RX โดยทั่วไปทำงานได้ดีกว่าอย่างไร RX เป็นประกาศ คุณสามารถประกาศว่าโปรแกรมจะตอบสนองต่อเหตุการณ์ที่เกิดขึ้นในภายหลังได้อย่างไรโดยไม่ส่งผลกระทบต่อตรรกะของโปรแกรมอื่น ๆ สิ่งนี้ช่วยให้คุณสามารถแยกรหัสลูปหลักออกจากรหัสการจัดการเหตุการณ์ คุณสามารถจัดการกับรายละเอียดต่างๆเช่นการจัดลำดับเหตุการณ์แบบ async ที่เป็นฝันร้ายเมื่อใช้ตัวแปรสถานะ ฉันพบว่า RX เป็นการใช้งานที่สะอาดที่สุดในการดำเนินการร้องขอเครือข่ายใหม่หลังจากที่เครือข่ายตอบกลับ 3 ครั้งกลับมาหรือเพื่อจัดการข้อผิดพลาดทั้งห่วงโซ่หากไม่มีการส่งคืน จากนั้นจะสามารถรีเซ็ตตัวเองและรอ 3 เหตุการณ์เดียวกัน
colintheshots

อีกหนึ่งความคิดเห็นที่เกี่ยวข้อง: RX เป็น monad ต่อเนื่องซึ่งเกี่ยวข้องกับ CPS ถ้าฉันจำไม่ผิดนี่อาจอธิบายได้ว่า / ทำไม RX ถึงดีสำหรับปัญหาการโทรกลับ / นรก
jhegedus

30

เพียงแค่ตอบคำถาม: คุณช่วยแสดงให้เห็นได้ไหมว่า RX แก้ปัญหา "callback hell" ในตัวอย่างง่ายๆนั้นได้อย่างไร

flatMapเป็นความมหัศจรรย์ เราสามารถเขียนโค้ดต่อไปนี้ใน Rx สำหรับตัวอย่างของ @ hugomg:

def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
         .flatMap(y -> Observable[Z])
         .map(z -> ...)...

มันก็เหมือนกับการที่คุณเขียนรหัส FP บางจังหวะ Schedulerแต่ที่จริงแล้วคุณสามารถทำให้พวกเขาไม่ตรงกันโดย


26

เพื่อตอบคำถามว่า Rx แก้ปัญหาการเรียกกลับได้อย่างไร :

ก่อนอื่นให้อธิบายนรกเรียกกลับอีกครั้ง

ลองนึกภาพว่าเราต้องทำ http เพื่อให้ได้ทรัพยากรสามอย่างคือคนดาวเคราะห์และกาแล็กซี่ วัตถุประสงค์ของเราคือการค้นหาดาราจักรที่บุคคลนั้นอาศัยอยู่อันดับแรกเราต้องได้บุคคลจากนั้นดาวเคราะห์จากนั้นจึงกาแล็กซี่ นั่นคือการเรียกกลับสามครั้งสำหรับการดำเนินการแบบอะซิงโครนัสสามครั้ง

getPerson(person => { 
   getPlanet(person, (planet) => {
       getGalaxy(planet, (galaxy) => {
           console.log(galaxy);
       });
   });
});

การโทรกลับแต่ละครั้งจะซ้อนกัน การโทรกลับภายในแต่ละครั้งขึ้นอยู่กับผู้ปกครอง นำไปสู่รูปแบบ "ปิรามิดของการลงโทษ" ของนี้นรกโทรกลับ รหัสดูเหมือนเครื่องหมาย>

ในการแก้ปัญหานี้ใน RxJ คุณสามารถทำสิ่งนี้ได้:

getPerson()
  .map(person => getPlanet(person))
  .map(planet => getGalaxy(planet))
  .mergeAll()
  .subscribe(galaxy => console.log(galaxy));

ด้วยตัวดำเนินการmergeMapAKA flatMapคุณสามารถทำให้กระชับมากขึ้น:

getPerson()
  .mergeMap(person => getPlanet(person))
  .mergeMap(planet => getGalaxy(planet))
  .subscribe(galaxy => console.log(galaxy));

อย่างที่คุณเห็นรหัสจะแบนและมีการเรียกวิธีการแบบโซ่เดียว เราไม่มี "ปิรามิดแห่งการลงโทษ"

ดังนั้นจึงหลีกเลี่ยงการเรียกกลับนรก

ในกรณีที่คุณสงสัยคำสัญญาเป็นอีกวิธีหนึ่งในการหลีกเลี่ยงการเรียกกลับนรก แต่คำสัญญานั้นกระตือรือร้นไม่ขี้เกียจเหมือนคนสังเกตและ (โดยทั่วไปพูด) คุณไม่สามารถยกเลิกได้ง่ายๆ


ฉันไม่ใช่นักพัฒนา JS แต่เป็นคำอธิบายง่ายๆ
Omar Beshary

15

Callback hell คือรหัสใด ๆ ที่การใช้ฟังก์ชัน callbacks ในรหัส async คลุมเครือหรือยากต่อการติดตาม โดยทั่วไปเมื่อมีการกำหนดทิศทางมากกว่าหนึ่งระดับโค้ดที่ใช้การเรียกกลับอาจทำตามได้ยากขึ้น refactor ยากขึ้นและทดสอบได้ยากขึ้น กลิ่นรหัสคือการเยื้องหลายระดับเนื่องจากการส่งผ่านตัวอักษรฟังก์ชันหลายชั้น

สิ่งนี้มักเกิดขึ้นเมื่อพฤติกรรมมีการพึ่งพากล่าวคือเมื่อ A ต้องเกิดขึ้นก่อน B ต้องเกิดขึ้นก่อน C จากนั้นคุณจะได้รับรหัสดังนี้:

a({
    parameter : someParameter,
    callback : function() {
        b({
             parameter : someOtherParameter,
             callback : function({
                 c(yetAnotherParameter)
        })
    }
});

หากคุณมีการอ้างอิงพฤติกรรมจำนวนมากในโค้ดของคุณเช่นนี้อาจทำให้เกิดปัญหาได้อย่างรวดเร็ว โดยเฉพาะอย่างยิ่งถ้ามันสาขา ...

a({
    parameter : someParameter,
    callback : function(status) {
        if (status == states.SUCCESS) {
          b(function(status) {
              if (status == states.SUCCESS) {
                 c(function(status){
                     if (status == states.SUCCESS) {
                         // Not an exaggeration. I have seen
                         // code that looks like this regularly.
                     }
                 });
              }
          });
        } elseif (status == states.PENDING {
          ...
        }
    }
});

สิ่งนี้จะไม่ทำ เราจะทำให้โค้ดอะซิงโครนัสดำเนินการตามลำดับที่กำหนดได้อย่างไรโดยไม่ต้องผ่านการเรียกกลับทั้งหมดเหล่านี้

RX ย่อมาจาก 'ส่วนขยายปฏิกิริยา' ฉันไม่ได้ใช้มัน แต่ Googling แนะนำว่าเป็นกรอบงานตามเหตุการณ์ซึ่งสมเหตุสมผล เหตุการณ์ที่เกิดขึ้นเป็นรูปแบบทั่วไปที่จะทำให้รหัสดำเนินการในการสั่งซื้อโดยไม่ต้องสร้างการมีเพศสัมพันธ์ที่เปราะบาง คุณสามารถทำให้ C ฟังเหตุการณ์ 'bFinished' ซึ่งจะเกิดขึ้นหลังจาก B เรียกว่าฟัง 'aFinished' เท่านั้น จากนั้นคุณสามารถเพิ่มขั้นตอนพิเศษหรือขยายพฤติกรรมประเภทนี้ได้อย่างง่ายดายและสามารถทดสอบได้อย่างง่ายดายว่าโค้ดของคุณดำเนินการตามลำดับโดยการถ่ายทอดเหตุการณ์ในกรณีทดสอบของคุณเท่านั้น


1

การเรียกกลับนรกหมายความว่าคุณอยู่ในการโทรกลับภายในการโทรกลับอื่นและจะไปที่การโทรที่ n จนกว่าความต้องการของคุณจะไม่สมบูรณ์

มาทำความเข้าใจกับตัวอย่างการโทร ajax ปลอมโดยใช้ set timeout API สมมติว่าเรามีสูตร API เราต้องดาวน์โหลดสูตรอาหารทั้งหมด

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
            }, 1500);
        }
        getRecipe();
    </script>
</body>

ในตัวอย่างข้างต้นหลังจาก 1.5 วินาทีเมื่อตัวจับเวลาหมดอายุภายในรหัสการโทรกลับจะดำเนินการกล่าวอีกนัยหนึ่งผ่านการโทร ajax ปลอมของเราสูตรทั้งหมดจะดาวน์โหลดจากเซิร์ฟเวอร์ ตอนนี้เราต้องดาวน์โหลดข้อมูลสูตรเฉพาะ

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

ในการดาวน์โหลดข้อมูลสูตรเฉพาะเราได้เขียนโค้ดไว้ในการโทรกลับครั้งแรกและส่งรหัสสูตร

สมมติว่าเราต้องดาวน์โหลดสูตรอาหารทั้งหมดของผู้เผยแพร่สูตรเดียวกันซึ่ง id คือ 7638

<body>
    <script>
        function getRecipe(){
            setTimeout(()=>{
                const recipeId = [83938, 73838, 7638];
                console.log(recipeId);
                setTimeout(id=>{
                    const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                    console.log(`${id}: ${recipe.title}`);
                    setTimeout(publisher=>{
                        const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
                        console.log(recipe2);
                    }, 1500, recipe.publisher);
                }, 1500, recipeId[2])
            }, 1500);
        }
        getRecipe();
    </script>
</body>

เพื่อเติมเต็มความต้องการของเราซึ่งก็คือการดาวน์โหลดสูตรอาหารทั้งหมดของชื่อผู้จัดพิมพ์ suru เราได้เขียนโค้ดไว้ในการโทรกลับครั้งที่สองของเรา เห็นได้ชัดว่าเราเขียนห่วงโซ่การเรียกกลับซึ่งเรียกว่านรกเรียกกลับ

หากคุณต้องการหลีกเลี่ยงการโทรกลับคุณสามารถใช้ Promise ซึ่งเป็นคุณสมบัติ js es6 โดยแต่ละคำสัญญาจะเรียกกลับซึ่งเรียกเมื่อคำสัญญาเต็มไปหมด สัญญาว่าจะโทรกลับมีสองตัวเลือกไม่ว่าจะได้รับการแก้ไขหรือปฏิเสธ สมมติเรียก API ของคุณจะประสบความสำเร็จคุณสามารถเรียกแก้ไขและส่งผ่านข้อมูลผ่านการแก้ปัญหาคุณจะได้รับข้อมูลนี้โดยใช้แล้ว () แต่ถ้า API ของคุณล้มเหลวคุณสามารถใช้ปฏิเสธได้ให้ใช้catchเพื่อตรวจจับข้อผิดพลาด อย่าลืมสัญญามักจะใช้แล้วสำหรับการแก้ปัญหาและจับสำหรับการปฏิเสธ

มาแก้ปัญหาการโทรกลับก่อนหน้านี้โดยใช้คำสัญญา

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        getIds.then(IDs=>{
            console.log(IDs);
        }).catch(error=>{
            console.log(error);
        });
    </script>
</body>

ดาวน์โหลดสูตรเฉพาะ:

<body>
    <script>
        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }
        getIds.then(IDs=>{
            console.log(IDs);
            return getRecipe(IDs[2]);
        }).
        then(recipe =>{
            console.log(recipe);
        })
        .catch(error=>{
            console.log(error);
        });
    </script>
</body>

ตอนนี้เราสามารถเขียนอีกวิธีหนึ่งที่เรียกว่าallRecipeOfAPublisherเช่น getRecipe ซึ่งจะคืนสัญญาและเราสามารถเขียนอีกครั้งแล้ว () เพื่อรับคำสัญญาแก้ไขสำหรับ allRecipeOfAPublisher ฉันหวังว่า ณ จุดนี้คุณสามารถทำได้ด้วยตัวเอง

ดังนั้นเราจึงได้เรียนรู้วิธีสร้างและใช้คำสัญญาตอนนี้มาทำให้การบริโภคสัญญาง่ายขึ้นโดยใช้ async / await ซึ่งเปิดตัวใน es8

<body>
    <script>

        const getIds = new Promise((resolve, reject)=>{
            setTimeout(()=>{
                const downloadSuccessfull = true;
                const recipeId = [83938, 73838, 7638];
                if(downloadSuccessfull){
                    resolve(recipeId);
                }else{
                    reject('download failed 404');
                }
            }, 1500);
        });

        const getRecipe = recID => {
            return new Promise((resolve, reject)=>{
                setTimeout(id => {
                    const downloadSuccessfull = true;
                    if (downloadSuccessfull){
                        const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
                        resolve(`${id}: ${recipe.title}`);
                    }else{
                        reject(`${id}: recipe download failed 404`);
                    }

                }, 1500, recID)
            })
        }

        async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

        getRecipesAw();
    </script>
</body>

ในตัวอย่างข้างต้นเราใช้ฟังก์ชัน async เพราะมันจะทำงานในพื้นหลังภายในฟังก์ชัน async เราใช้คำหลักรอก่อนที่แต่ละวิธีจะส่งคืนหรือเป็นคำสัญญาเพราะต้องรอตำแหน่งนั้นจนกว่าสัญญานั้นจะสำเร็จกล่าวอีกนัยหนึ่งใน รหัสร้องจนกว่า getIds จะแก้ไขหรือปฏิเสธโปรแกรมจะหยุดการเรียกใช้รหัสดังต่อไปนี้เมื่อ ID ส่งคืนจากนั้นเราเรียกอีกครั้งว่า getRecipe () ฟังก์ชั่นด้วย id และรอโดยใช้คำสำคัญรอจนกว่าข้อมูลจะถูกส่งคืน นี่คือวิธีที่เราฟื้นจากนรกเรียกกลับได้ในที่สุด

  async function getRecipesAw(){
            const IDs = await getIds;
            console.log(IDs);
            const recipe = await getRecipe(IDs[2]);
            console.log(recipe);
        }

ในการใช้ await เราจะต้องมีฟังก์ชัน async เราสามารถคืนสัญญาได้ดังนั้นใช้เพื่อแก้ไขสัญญาและ cath สำหรับการปฏิเสธสัญญา

จากตัวอย่างด้านบน:

 async function getRecipesAw(){
            const IDs = await getIds;
            const recipe = await getRecipe(IDs[2]);
            return recipe;
        }

        getRecipesAw().then(result=>{
            console.log(result);
        }).catch(error=>{
            console.log(error);
        });

0

วิธีหนึ่งที่สามารถหลีกเลี่ยง Callback hell คือการใช้ FRP ซึ่งเป็น "รุ่นปรับปรุง" ของ RX

ฉันเริ่มใช้ FRP เมื่อเร็ว ๆ นี้เพราะฉันพบว่ามีการใช้งานที่ดีที่เรียกว่าSodium( http://sodium.nz/ )

รหัสทั่วไปมีลักษณะดังนี้ (Scala.js):

def render: Unit => VdomElement = { _ =>
  <.div(
    <.hr,
    <.h2("Note Selector"),
    <.hr,
    <.br,
    noteSelectorTable.comp(),
    NoteCreatorWidget().createNewNoteButton.comp(),
    NoteEditorWidget(selectedNote.updates()).comp(),
    <.hr,
    <.br
  )
}

selectedNote.updates()คือสิ่งStreamที่เริ่มทำงานหากselectedNode(ซึ่งเป็นCell) มีการเปลี่ยนแปลงNodeEditorWidgetจากนั้นจะอัปเดตตามนั้น

ดังนั้นขึ้นอยู่กับเนื้อหาของการselectedNode Cellแก้ไขในปัจจุบันNoteจะเปลี่ยนไป

รหัสนี้หลีกเลี่ยง Callback-s โดยสิ้นเชิงเกือบ Cacllback-s ถูกผลักไปที่ "ชั้นนอก" / "พื้นผิว" ของแอปซึ่งตรรกะการจัดการสถานะจะเชื่อมต่อกับโลกภายนอก ไม่จำเป็นต้องมีการเรียกกลับเพื่อเผยแพร่ข้อมูลภายในตรรกะการจัดการสถานะภายใน (ซึ่งใช้เครื่องสถานะ)

ซอร์สโค้ดฉบับเต็มอยู่ที่นี่

ข้อมูลโค้ดด้านบน corrosponds ตัวอย่างง่ายๆต่อไปนี้สร้าง / แสดง / อัปเดต:

ใส่คำอธิบายภาพที่นี่

รหัสนี้ยังส่งการอัปเดตไปยังเซิร์ฟเวอร์ดังนั้นการเปลี่ยนแปลงเอนทิตีที่อัปเดตจะถูกบันทึกลงในเซิร์ฟเวอร์โดยอัตโนมัติ

การจัดการเหตุการณ์ทั้งหมดได้รับการดูแลโดยใช้Streams and Cells นี่คือแนวคิด FRP จำเป็นต้องมีการโทรกลับเฉพาะเมื่อตรรกะ FRP เชื่อมต่อกับโลกภายนอกเช่นการป้อนข้อมูลของผู้ใช้การแก้ไขข้อความการกดปุ่มการโทรกลับ AJAX

โฟลว์ข้อมูลได้รับการอธิบายอย่างชัดเจนในลักษณะที่เปิดเผยโดยใช้ FRP (ดำเนินการโดยไลบรารี Sodium) ดังนั้นจึงไม่จำเป็นต้องใช้ตรรกะการจัดการเหตุการณ์ / การเรียกกลับเพื่ออธิบายโฟลว์ข้อมูล

FRP (ซึ่งเป็น RX เวอร์ชันที่ "เข้มงวดกว่า") เป็นวิธีอธิบายกราฟการไหลของข้อมูลซึ่งสามารถมีโหนดที่มีสถานะ สถานะทริกเกอร์เหตุการณ์เปลี่ยนแปลงในสถานะที่มีโหนด (เรียกว่าCells)

โซเดียมเป็นไลบรารี FRP ลำดับที่สูงกว่าซึ่งหมายความว่าการใช้flatMap/ switchดั้งเดิมสามารถจัดเรียงกราฟการไหลของข้อมูลที่รันไทม์ได้

ฉันขอแนะนำให้ดูในหนังสือ Sodiumซึ่งจะอธิบายรายละเอียดว่า FRP กำจัด Callback ทั้งหมดที่ไม่จำเป็นสำหรับการอธิบายตรรกะกระแสข้อมูลที่เกี่ยวข้องกับการอัปเดตสถานะแอปพลิเคชันเพื่อตอบสนองต่อสิ่งเร้าภายนอกบางอย่างได้อย่างไร

การใช้ FRP จำเป็นต้องเก็บเฉพาะ Callbacks เหล่านั้นไว้ซึ่งอธิบายการโต้ตอบกับโลกภายนอก กล่าวอีกนัยหนึ่งกระแสข้อมูลจะอธิบายในลักษณะการทำงาน / การประกาศเมื่อใช้เฟรมเวิร์ก FRP (เช่น Sodium) หรือเมื่อใช้เฟรมเวิร์ก "FRP like" (เช่น RX)

โซเดียมยังสามารถใช้ได้กับ Javascript / typescript


-3

หากคุณไม่มีความรู้เกี่ยวกับการโทรกลับและการโทรกลับนรกก็ไม่มีปัญหาสิ่งที่สำคัญที่สุดคือการโทรกลับและเรียกนรกกลับตัวอย่างเช่นการโทรกลับนรกก็เหมือนกับเราสามารถจัดเก็บคลาสไว้ในคลาสได้อย่างที่คุณเคยได้ยิน เกี่ยวกับสิ่งที่ซ้อนอยู่ในภาษา C, C ++ การวางตัวหมายความว่าคลาสภายในคลาสอื่น


คำตอบจะมีประโยชน์มากขึ้นหากมีข้อมูลโค้ดเพื่อแสดงว่าอะไรคือ 'Callback hell' และข้อมูลโค้ดเดียวกันกับ Rx หลังจากลบ 'callback hell'
rafa

-4

ใช้ jazz.js https://github.com/Javanile/Jazz.js

มันง่ายขึ้นเช่นนี้:

    // รันภารกิจตามลำดับที่ถูกล่ามโซ่
    jj.script ([
        // งานแรก
        function (ถัดไป) {
            // ในตอนท้ายของกระบวนการนี้ 'ถัดไป' ชี้ไปที่งานที่สองและเรียกใช้ 
            callAsyncProcess1 (ถัดไป);
        },
      // ภารกิจที่สอง
      function (ถัดไป) {
        // ในตอนท้ายของกระบวนการนี้ 'ถัดไป' ชี้ไปที่งานที่สามแล้วเรียกใช้ 
        callAsyncProcess2 (ถัดไป);
      },
      // งานที่สาม
      function (ถัดไป) {
        // ในตอนท้ายของกระบวนการนี้ 'ถัดไป' ชี้ไปที่ (ถ้ามี) 
        callAsyncProcess3 (ถัดไป);
      },
    ]);


พิจารณาขนาดกะทัดรัดเป็นพิเศษเช่นนี้github.com/Javanile/Jazz.js/wiki/Script-showcase
cicciodarkast
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.