Có thực sự có một sự khác biệt cơ bản giữa cuộc gọi lại và Lời hứa?


94

Khi thực hiện lập trình không đồng bộ đơn luồng, có hai kỹ thuật chính mà tôi quen thuộc. Một phổ biến nhất là sử dụng cuộc gọi lại. Điều đó có nghĩa là chuyển đến hàm hoạt động không đồng bộ một hàm gọi lại làm tham số. Khi hoạt động không đồng bộ sẽ kết thúc, cuộc gọi lại sẽ được gọi.

Một số jQuerymã điển hình được thiết kế theo cách này:

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

Tuy nhiên, loại mã này có thể trở nên lộn xộn và được lồng ghép cao khi chúng ta muốn thực hiện các cuộc gọi không đồng bộ bổ sung lần lượt khi lần trước kết thúc.

Vì vậy, một cách tiếp cận thứ hai là sử dụng Lời hứa. Promise là một đối tượng đại diện cho một giá trị có thể chưa tồn tại. Bạn có thể đặt các cuộc gọi lại trên nó, nó sẽ được gọi khi giá trị sẵn sàng để đọc.

Sự khác biệt giữa Promise và cách tiếp cận gọi lại truyền thống, là các phương thức async hiện trả về đồng bộ các đối tượng Promise mà máy khách đặt lại cho cuộc gọi lại. Ví dụ: mã tương tự sử dụng Promise trong AngularJS:

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

Vì vậy, câu hỏi của tôi là: có thực sự có một sự khác biệt? Sự khác biệt dường như hoàn toàn là cú pháp.

Có bất kỳ lý do sâu sắc hơn để sử dụng một kỹ thuật khác?


8
Có: gọi lại chỉ là chức năng hạng nhất. Hứa hẹn là các đơn vị cung cấp một cơ chế có thể kết hợp để vận hành chuỗi trên các giá trị và tình cờ sử dụng các hàm bậc cao hơn với các cuộc gọi lại để cung cấp giao diện thuận tiện.
amon


5
@gnat: Với chất lượng tương đối của hai câu hỏi / câu trả lời, phiếu bầu trùng lặp sẽ là cách khác xung quanh IMHO.
Bart van Ingen Schenau

Câu trả lời:


110

Công bằng mà nói lời hứa chỉ là cú pháp đường. Tất cả mọi thứ bạn có thể làm với lời hứa bạn có thể làm với các cuộc gọi lại. Trong thực tế, hầu hết các triển khai hứa hẹn cung cấp các cách chuyển đổi giữa hai bất cứ khi nào bạn muốn.

Lý do sâu tại sao lời hứa thường tốt hơn là họ có nhiều composeable , mà có nghĩa là khoảng cách kết hợp nhiều lời hứa "chỉ hoạt động", trong khi cách kết hợp nhiều callbacks thường thì không. Chẳng hạn, việc gán lời hứa cho một biến và đính kèm các trình xử lý bổ sung cho nó sau này, hoặc thậm chí đính kèm một trình xử lý vào một nhóm lớn các lời hứa chỉ được thực hiện sau khi tất cả các lời hứa được giải quyết. Mặc dù bạn có thể sắp xếp mô phỏng những thứ này bằng các cuộc gọi lại, nhưng nó đòi hỏi nhiều mã hơn, rất khó để thực hiện chính xác và kết quả cuối cùng thường ít được duy trì hơn nhiều.

Một trong những cách lớn nhất (và tinh vi nhất) hứa hẹn đạt được khả năng kết hợp của chúng là bằng cách xử lý thống nhất các giá trị trả về và các ngoại lệ chưa được lưu. Với các cuộc gọi lại, làm thế nào một ngoại lệ được xử lý có thể phụ thuộc hoàn toàn vào việc trong số nhiều cuộc gọi lại lồng nhau đã ném nó và chức năng nào thực hiện cuộc gọi lại có một thử / bắt trong quá trình thực hiện. Với lời hứa, bạn biết rằng một ngoại lệ thoát khỏi một hàm gọi lại sẽ bị bắt và chuyển đến bộ xử lý lỗi mà bạn cung cấp .error()hoặc .catch().

Đối với ví dụ bạn đã đưa ra một cuộc gọi lại so với một lời hứa duy nhất, thì đúng là không có sự khác biệt đáng kể nào. Đó là khi bạn có một cuộc gọi lại hàng tỷ so với một tỷ lời hứa rằng mã dựa trên lời hứa có xu hướng trông đẹp hơn nhiều.


Đây là một nỗ lực đối với một số mã giả định được viết bằng lời hứa và sau đó với các cuộc gọi lại chỉ đủ phức tạp để cung cấp cho bạn một số ý tưởng về những gì tôi đang nói.

Với những lời hứa:

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);
}

Với cuộc gọi lại:

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);
                }
            });
        });
    });
}

Có thể có một số cách thông minh để giảm trùng lặp mã trong phiên bản gọi lại ngay cả khi không có lời hứa, nhưng tất cả những cách tôi có thể nghĩ đến để thực hiện một cái gì đó rất giống như lời hứa.


1
Một lợi thế lớn khác của các lời hứa là chúng có thể tiếp tục "đường hóa" với async / await hoặc coroutine trả lại các giá trị đã hứa cho các yieldlời hứa ed. Ưu điểm ở đây là bạn có khả năng kết hợp trong các cấu trúc luồng điều khiển riêng, có thể thay đổi số lượng hoạt động không đồng bộ mà chúng thực hiện. Tôi sẽ thêm một phiên bản cho thấy điều này.
acjay

9
Sự khác biệt cơ bản giữa các cuộc gọi lại và lời hứa là sự đảo ngược của kiểm soát. Với các cuộc gọi lại, API của bạn phải chấp nhận một cuộc gọi lại , nhưng với Lời hứa, API của bạn phải cung cấp một lời hứa . Đây là sự khác biệt chính và nó có ý nghĩa rộng đối với thiết kế API.
cwharris

@ChristopherHarris không chắc chắn tôi đồng ý. có một then(callback)phương thức trên Promise chấp nhận cuộc gọi lại (thay vì phương thức API chấp nhận cuộc gọi lại này) không phải làm gì với IoC. Promise giới thiệu một mức độ gián tiếp hữu ích cho việc cấu thành, xử lý chuỗi và xử lý lỗi (lập trình theo định hướng đường sắt có hiệu lực), nhưng gọi lại vẫn không được thực hiện bởi máy khách, vì vậy không thực sự vắng mặt IoC.
Draingan.stepanovic

1
@ Draingan.stepanovic Bạn nói đúng, và tôi đã sử dụng thuật ngữ sai. Sự khác biệt là sự quyết đoán. Với một cuộc gọi lại, bạn phải biết những gì cần phải được thực hiện với kết quả. Với một lời hứa, bạn có thể quyết định sau.
cwharris
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.