มีความแตกต่างพื้นฐานระหว่างการโทรกลับและสัญญาหรือไม่


94

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

jQueryรหัสทั่วไปบางอย่างออกแบบด้วยวิธีนี้:

$.get('userDetails', {'name': 'joe'}, function(data) {
    $('#userAge').text(data.age);
});

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

ดังนั้นวิธีที่สองคือใช้สัญญา คำมั่นสัญญาเป็นวัตถุที่แสดงถึงค่าที่อาจยังไม่มี คุณสามารถตั้งค่า callback บนมันซึ่งจะถูกเรียกเมื่อค่าพร้อมที่จะอ่าน

ความแตกต่างระหว่างคำสัญญาและวิธีการเรียกกลับแบบเดิมคือวิธีการแบบอะซิงก์ตอนนี้ส่งคืนออบเจ็กต์คำสัญญาซึ่งลูกค้าตั้งค่าการเรียกกลับ ตัวอย่างเช่นรหัสที่คล้ายกันโดยใช้สัญญาใน AngularJS:

$http.get('userDetails', {'name': 'joe'})
    .then(function(response) {
        $('#userAge').text(response.age);
    });

ดังนั้นคำถามของฉันคือจริง ๆ แล้วมีความแตกต่างจริง ๆ ความแตกต่างนั้นดูเหมือนจะเป็นการสร้างประโยคอย่างหมดจด

มีเหตุผลใดที่ลึกซึ้งยิ่งขึ้นในการใช้เทคนิคหนึ่งเหนืออีกเทคนิคหนึ่ง?


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


5
@gnat: ด้วยคุณภาพสัมพัทธ์ของสองคำถาม / คำตอบการลงคะแนนซ้ำควรเป็นวิธีอื่นรอบ IMHO
Bart van Ingen Schenau

คำตอบ:


110

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

เหตุผลลึก ๆ ว่าทำไมสัญญามักจะดีกว่าที่พวกเขากำลังมากขึ้นcomposeableซึ่งประมาณหมายความว่าการรวมหลายสัญญา "เพิ่งทำงาน" ในขณะที่รวมเรียกกลับหลายมักจะไม่ ตัวอย่างเช่นมันไม่สำคัญที่จะกำหนดสัญญาให้กับตัวแปรและแนบตัวจัดการเพิ่มเติมไปไว้ในภายหลังหรือแม้กระทั่งแนบตัวจัดการกับกลุ่มสัญญาจำนวนมากที่ได้รับการดำเนินการหลังจากแก้ไขสัญญาทั้งหมดแล้วเท่านั้น ในขณะที่คุณสามารถเลียนแบบสิ่งเหล่านี้ด้วยการเรียกกลับต้องใช้รหัสมากขึ้นเป็นเรื่องยากมากที่จะทำอย่างถูกต้องและผลลัพธ์สุดท้ายมักจะบำรุงรักษาได้น้อยกว่ามาก

หนึ่งในวิธีที่ใหญ่ที่สุด (และย่อย) เพื่อให้ได้รับความสามารถในการแต่งเพลงของพวกเขาคือการจัดการค่าตอบแทนและการยกเว้นที่ไม่ได้ตรวจสอบอย่างสม่ำเสมอ ด้วยการเรียกกลับวิธีการจัดการข้อยกเว้นอาจขึ้นอยู่กับการเรียกกลับที่ซ้อนกันหลาย ๆ ครั้งและฟังก์ชันใดที่การเรียกกลับมีการลอง / จับในการนำไปใช้ สัญญากับคุณรู้ว่าข้อยกเว้นซึ่งหนีออกมาทำงานกลับหนึ่งจะถูกจับและส่งผ่านไปยังจัดการข้อผิดพลาดที่คุณให้กับหรือ.error().catch()

สำหรับตัวอย่างที่คุณได้รับจากการติดต่อกลับครั้งเดียวเมื่อเทียบกับคำสัญญาเดียวมันเป็นความจริงที่ไม่มีความแตกต่างอย่างมีนัยสำคัญ มันคือเมื่อคุณมีการโทรกลับเป็น zillion กับ zillion สัญญาว่ารหัสตามสัญญามีแนวโน้มที่จะดูดีกว่ามาก


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

ด้วยสัญญา:

createViewFilePage(fileDescriptor) {
    getCurrentUser().then(function(user) {
        return isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id);
    }).then(function(isAuthorized) {
        if(!isAuthorized) {
            throw new Error('User not authorized to view this resource.'); // gets handled by the catch() at the end
        }
        return Promise.all([
            loadUserFile(fileDescriptor.id),
            getFileDownloadCount(fileDescriptor.id),
            getCommentsOnFile(fileDescriptor.id),
        ]);
    }).then(function(fileData) {
        var fileContents = fileData[0];
        var fileDownloads = fileData[1];
        var fileComments = fileData[2];
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }).catch(showAndLogErrorMessage);
}

ด้วยการโทรกลับ:

createViewFilePage(fileDescriptor) {
    setupWidgets(fileContents, fileDownloads, fileComments) {
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }

    getCurrentUser(function(error, user) {
        if(error) { showAndLogErrorMessage(error); return; }
        isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id, function(error, isAuthorized) {
            if(error) { showAndLogErrorMessage(error); return; }
            if(!isAuthorized) {
                throw new Error('User not authorized to view this resource.'); // gets silently ignored, maybe?
            }

            var fileContents, fileDownloads, fileComments;
            loadUserFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileContents = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getFileDownloadCount(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileDownloads = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getCommentsOnFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileComments = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
        });
    });
}

อาจมีวิธีที่ชาญฉลาดในการลดความซ้ำซ้อนของรหัสในเวอร์ชันที่เรียกกลับมาโดยไม่ต้องสัญญา แต่ทุกอย่างที่ฉันคิดได้ว่าจะนำไปใช้ในการดำเนินการตามที่สัญญาไว้


1
ข้อได้เปรียบที่สำคัญอีกประการหนึ่งของสัญญาคือพวกเขามีหน้าที่รับผิดชอบในการ "เปลี่ยนเป็นน้ำตาล" ด้วย async / await หรือ coroutine ที่ส่งคืนค่าที่สัญญาไว้สำหรับyieldสัญญา ed ข้อได้เปรียบที่นี่คือคุณจะได้รับความสามารถในการผสมในโครงสร้างโฟลว์การควบคุมเนทิฟซึ่งอาจแตกต่างกันไปตามจำนวนการดำเนินการ async ฉันจะเพิ่มเวอร์ชันที่แสดงสิ่งนี้
acjay

9
ความแตกต่างพื้นฐานระหว่างการโทรกลับและสัญญาคือการผกผันของการควบคุม ด้วยการเรียกกลับ API ของคุณจะต้องยอมรับการเรียกกลับแต่มีสัญญา API ของคุณจะต้องให้สัญญา นี่คือความแตกต่างหลักและมีความหมายกว้างสำหรับการออกแบบ API
cwharris

@ChristopherHarris ไม่แน่ใจว่าฉันเห็นด้วย มีthen(callback)วิธีการใน Promise ที่รับการเรียกกลับ (แทนที่จะเป็นวิธีการที่ API รับการเรียกกลับนี้) ไม่ต้องทำอะไรกับ IoC สัญญาแนะนำการอ้อมในระดับหนึ่งที่มีประโยชน์สำหรับการจัดองค์ประกอบการโยงและการจัดการข้อผิดพลาด (การเขียนโปรแกรมเชิงรถไฟมีผล) แต่การเรียกกลับยังคงไม่ถูกดำเนินการโดยลูกค้าดังนั้นจึงไม่ขาด IoC จริงๆ
dragan.stepanovic

1
@ dragan.stepanovic คุณพูดถูกและฉันใช้คำศัพท์ที่ผิด ความแตกต่างคือทางอ้อม ด้วยการโทรกลับคุณต้องทราบว่าต้องทำอะไรกับผลลัพธ์ ด้วยสัญญาคุณสามารถตัดสินใจได้ในภายหลัง
cwharris
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.