Không hứa hẹn chỉ là cuộc gọi lại?


430

Tôi đã phát triển JavaScript được một vài năm và tôi không hiểu gì về những lời hứa.

Có vẻ như tất cả những gì tôi làm là thay đổi:

api(function(result){
    api2(function(result2){
        api3(function(result3){
             // do work
        });
    });
});

Mà tôi có thể sử dụng một thư viện như async cho dù sao, với một cái gì đó như:

api().then(function(result){
     api2().then(function(result2){
          api3().then(function(result3){
               // do work
          });
     });
});

Đó là nhiều mã hơn và ít đọc hơn. Tôi đã không đạt được bất cứ điều gì ở đây, nó cũng không đột nhiên 'phẳng' một cách kỳ diệu. Chưa kể phải chuyển đổi mọi thứ thành lời hứa.

Vì vậy, những gì ồn ào về những lời hứa ở đây?


11
Về chủ đề : có một bài viết thực sự nhiều thông tin về Lời hứa trên Html5Rocks: html5rocks.com/en/tutorials/es6/promises
ComFalet 20/03/2016

2
Fyi câu trả lời bạn chấp nhận là cùng một danh sách cũ về những lợi ích tầm thường hoàn toàn không phải là điểm hứa hẹn và thậm chí còn không thuyết phục tôi sử dụng lời hứa: /. Điều thuyết phục tôi sử dụng lời hứa là khía cạnh DSL như được mô tả trong câu trả lời của Oscar
Esailija

@Esailija tốt, leet của bạn đã thuyết phục tôi. Tôi đã chấp nhận câu trả lời khác mặc dù tôi nghĩ rằng một trong những điểm của Bergi cũng làm tăng một số điểm thực sự tốt (và khác biệt).
Benjamin Gruenbaum

@Esailija "Điều thuyết phục tôi sử dụng lời hứa là khía cạnh DSL như được mô tả trong câu trả lời của Oscar" << "DSL" là gì? và "khía cạnh DSL" mà bạn đang đề cập đến là gì?
monsto

1
@monsto: DSL: Ngôn ngữ cụ thể miền, một ngôn ngữ được thiết kế có chủ đích được sử dụng trong một tập hợp con cụ thể của hệ thống (ví dụ: SQL hoặc ORM để nói chuyện với cơ sở dữ liệu, biểu thức chính quy để tìm mẫu, v.v.). Trong ngữ cảnh này, "DSL" là API của Promise, nếu bạn cấu trúc mã theo cách mà Oscar đã làm, gần giống như đường cú pháp bổ sung JavaScript để giải quyết bối cảnh cụ thể của các hoạt động không đồng bộ. Hứa hẹn tạo ra một số thành ngữ biến chúng thành gần như một ngôn ngữ được thiết kế để cho phép lập trình viên dễ dàng nắm bắt dòng chảy tinh thần có phần khó nắm bắt của loại cấu trúc này.
Michael Ekoka

Câu trả lời:


631

Lời hứa không phải là cuộc gọi lại. Một lời hứa đại diện cho kết quả trong tương lai của một hoạt động không đồng bộ . Tất nhiên, viết chúng theo cách bạn làm, bạn sẽ nhận được rất ít lợi ích. Nhưng nếu bạn viết chúng theo cách chúng được sử dụng, bạn có thể viết mã không đồng bộ theo cách giống với mã đồng bộ và dễ thực hiện hơn nhiều:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
});

Chắc chắn, không ít mã hơn, nhưng dễ đọc hơn nhiều.

Nhưng đây không phải là kết thúc. Hãy khám phá những lợi ích thực sự: Điều gì sẽ xảy ra nếu bạn muốn kiểm tra bất kỳ lỗi nào trong bất kỳ bước nào? Sẽ là địa ngục khi làm điều đó với các cuộc gọi lại, nhưng với những lời hứa, là một miếng bánh:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
});

Khá giống như một try { ... } catchkhối.

Thậm chí còn tốt hơn:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
}).then(function() {
     //do something whether there was an error or not
     //like hiding an spinner if you were performing an AJAX request.
});

Và thậm chí tốt hơn: Nếu những 3 cuộc gọi đến api, api2, api3có thể chạy cùng một lúc (ví dụ như nếu họ gọi AJAX) nhưng bạn cần phải đợi cho ba? Nếu không có lời hứa, bạn sẽ phải tạo ra một số loại truy cập. Với lời hứa, sử dụng ký hiệu ES6, là một miếng bánh khác và khá gọn gàng:

Promise.all([api(), api2(), api3()]).then(function(result) {
    //do work. result is an array contains the values of the three fulfilled promises.
}).catch(function(error) {
    //handle the error. At least one of the promises rejected.
});

Hy vọng bạn nhìn thấy Lời hứa trong một ánh sáng mới bây giờ.


124
Họ thực sự không nên đặt tên nó là "Lời hứa". "Tương lai" tốt hơn ít nhất 100 lần.
Pacerier

12
@Pacerier vì Tương lai không bị jQuery làm hỏng?
Esailija

5
Mẫu thay thế (tùy thuộc vào những gì mong muốn: api (). Sau đó (api2) .then (api3) .then (doWork); Nghĩa là, nếu các hàm api2 / api3 lấy đầu vào từ bước cuối cùng và tự trả về những lời hứa mới chỉ có thể bị xiềng xích mà không cần gói thêm. Đó là, họ sáng tác.
Dtipson

1
Điều gì nếu có hoạt động không đồng bộ trong api2api3? cuối cùng .thenchỉ được gọi khi các hoạt động không đồng bộ đó được hoàn thành?
NiCk Newman

8
Tại sao bạn gắn thẻ tôi? Tôi chỉ sửa lỗi ngữ pháp một chút. Tôi không phải là chuyên gia về JS. :)
Scott Arciszewski

169

Có, Lời hứa là cuộc gọi lại không đồng bộ. Họ không thể làm bất cứ điều gì mà cuộc gọi lại không thể làm được và bạn phải đối mặt với những vấn đề tương tự với sự không đồng bộ như với cuộc gọi lại đơn giản.

Tuy nhiên, lời hứa là nhiều hơn chỉ là callbacks. Chúng là một sự trừu tượng hóa rất mạnh mẽ, cho phép mã chức năng sạch hơn và tốt hơn, với bản tóm tắt ít bị lỗi hơn.

Vậy ý chính là gì?

Hứa hẹn là các đối tượng đại diện cho kết quả của một tính toán (không đồng bộ). Họ giải quyết kết quả đó chỉ một lần. Có một vài điều điều này có nghĩa là:

Hứa hẹn thực hiện một mô hình quan sát viên:

  • Bạn không cần phải biết các cuộc gọi lại sẽ sử dụng giá trị trước khi tác vụ hoàn thành.
  • Thay vì mong đợi các cuộc gọi lại làm đối số cho các chức năng của bạn, bạn có thể dễ dàng trở returnthành một đối tượng Promise
  • Lời hứa sẽ lưu trữ các giá trị, và bạn có thể minh bạch thêm một callback bất cứ khi nào bạn muốn. Nó sẽ được gọi khi kết quả có sẵn. "Tính minh bạch" ngụ ý rằng khi bạn có một lời hứa và thêm một cuộc gọi lại cho nó, điều đó sẽ không tạo ra sự khác biệt cho mã của bạn cho dù kết quả đã đến chưa - API và các hợp đồng giống nhau, đơn giản hóa rất nhiều bộ nhớ đệm / ghi nhớ.
  • Bạn có thể thêm nhiều cuộc gọi lại dễ dàng

Lời hứa có thể kết nối ( đơn âm , nếu bạn muốn ):

  • Nếu bạn cần chuyển đổi giá trị mà một lời hứa thể hiện, bạn ánh xạ một hàm biến đổi theo lời hứa và nhận lại một lời hứa mới thể hiện kết quả được chuyển đổi. Bạn không thể đồng bộ hóa giá trị để sử dụng nó bằng cách nào đó, nhưng bạn có thể dễ dàng nâng chuyển đổi trong bối cảnh hứa hẹn. Không có cuộc gọi lại nồi hơi.
  • Nếu bạn muốn xâu chuỗi hai tác vụ không đồng bộ, bạn có thể sử dụng .then()phương thức. Nó sẽ nhận một cuộc gọi lại để được gọi với kết quả đầu tiên và trả lại một lời hứa cho kết quả của lời hứa mà cuộc gọi lại trả về.

Nghe có vẻ phức tạp? Thời gian cho một ví dụ mã.

var p1 = api1(); // returning a promise
var p3 = p1.then(function(api1Result) {
    var p2 = api2(); // returning a promise
    return p2; // The result of p2 …
}); // … becomes the result of p3

// So it does not make a difference whether you write
api1().then(function(api1Result) {
    return api2().then(console.log)
})
// or the flattened version
api1().then(function(api1Result) {
    return api2();
}).then(console.log)

Làm phẳng không đến một cách kỳ diệu, nhưng bạn có thể dễ dàng làm điều đó. Đối với ví dụ được lồng rất nhiều của bạn, tương đương (gần) sẽ là

api1().then(api2).then(api3).then(/* do-work-callback */);

Nếu việc xem mã của các phương thức này giúp hiểu, thì đây là một lời hứa cơ bản nhất trong một vài dòng .

Những gì ồn ào về những lời hứa?

Sự trừu tượng hóa Promise cho phép khả năng kết hợp tốt hơn nhiều chức năng. Ví dụ, bên cạnh thenchuỗi, allhàm tạo ra một lời hứa cho kết quả kết hợp của nhiều lời hứa chờ song song.

Lời hứa cuối cùng nhưng không kém phần quan trọng đi kèm với việc xử lý lỗi tích hợp. Kết quả của việc tính toán có thể là một trong hai lời hứa được thực hiện với một giá trị hoặc nó bị từ chối với một lý do. Tất cả các hàm thành phần tự động xử lý việc này và truyền các lỗi trong chuỗi hứa hẹn, do đó bạn không cần quan tâm đến nó một cách rõ ràng ở mọi nơi - ngược lại với việc thực hiện gọi lại đơn giản. Cuối cùng, bạn có thể thêm một cuộc gọi lại lỗi chuyên dụng cho tất cả các trường hợp ngoại lệ xảy ra.

Chưa kể phải chuyển đổi mọi thứ thành lời hứa.

Điều đó thực sự khá tầm thường với các thư viện hứa hẹn tốt, hãy xem Làm cách nào để chuyển đổi API gọi lại hiện tại thành các lời hứa?


chào Bergi, bạn có điều gì thú vị để thêm vào câu hỏi SO này không? stackoverflow.com/questions/22724883/ hy
Sebastien Lorber

1
@Sebastien: Tôi không biết nhiều về Scala (chưa) và tôi chỉ có thể lặp lại những gì Benjamin nói :-)
Bergi

3
Chỉ cần một lưu ý nhỏ: bạn không thể sử dụng .then(console.log), vì console.log phụ thuộc vào bối cảnh giao diện điều khiển. Bằng cách này, nó sẽ gây ra một lỗi gọi bất hợp pháp. Sử dụng console.log.bind(console)hoặc x => console.log(x)để ràng buộc bối cảnh.
Tamas Hegedus

3
@hege_hegedus: Có những môi trường mà consolecác phương thức đã bị ràng buộc. Và tất nhiên, tôi chỉ nói rằng cả hai tổ đều có hành vi giống hệt nhau, không phải bất kỳ cái nào trong số chúng cũng hoạt động :-P
Bergi

1
Điều đó thật tuyệt. Đây là những gì tôi cần: ít mã hơn và giải thích nhiều hơn. Cảm ơn bạn.
Adam Patterson

21

Ngoài các câu trả lời đã được thiết lập, với các chức năng mũi tên ES6, Promise biến từ một ngôi sao lùn nhỏ màu xanh sáng ngời thẳng thành một người khổng lồ đỏ. Đó là sắp sụp đổ thành một siêu tân tinh:

api().then(result => api2()).then(result2 => api3()).then(result3 => console.log(result3))

Như oligofren đã chỉ ra, không có đối số giữa các cuộc gọi api, bạn hoàn toàn không cần các hàm bao bọc ẩn danh:

api().then(api2).then(api3).then(r3 => console.log(r3))

Và cuối cùng, nếu bạn muốn đạt đến cấp độ lỗ đen siêu lớn, Lời hứa có thể được chờ đợi:

async function callApis() {
    let api1Result = await api();
    let api2Result = await api2(api1Result);
    let api3Result = await api3(api2Result);

    return api3Result;
}

9
"với các chức năng mũi tên ES6 Hứa biến từ một ngôi sao nhỏ màu xanh sáng khiêm tốn thành một người khổng lồ đỏ. Điều đó sắp sụp đổ thành siêu tân tinh" Dịch: Kết hợp các chức năng mũi tên ES6 với Promising là tuyệt vời :)
user344977

3
Điều đó làm cho Promise nghe giống như một thảm họa vũ trụ, mà tôi không nghĩ là ý định của bạn.
Michael McGinnis

Nếu bạn không sử dụng các đối số trong các apiXphương thức, bạn cũng có thể bỏ qua các hàm mũi tên hoàn toàn : api().then(api2).then(api3).then(r3 => console.log(r3)).
oligofren

@MichaelMcGinnis - Tác động có lợi của Lời hứa đối với một địa ngục gọi lại buồn tẻ giống như một siêu tân tinh nổ tung trong một góc tối của không gian.
John Weisz

Tôi biết bạn có nghĩa là nó về mặt thi pháp, nhưng những lời hứa khá xa với "siêu tân tinh". Vi phạm luật đơn nguyên hoặc thiếu hỗ trợ cho các trường hợp sử dụng đáng tin cậy hơn như hủy bỏ hoặc trả lại nhiều giá trị xuất hiện trong tâm trí.
Dmitri Zaitsev

15

Ngoài các câu trả lời tuyệt vời ở trên, có thể thêm 2 điểm nữa:

1. Sự khác biệt về ngữ nghĩa:

Lời hứa có thể đã được giải quyết khi tạo. Điều này có nghĩa là họ đảm bảo các điều kiện hơn là các sự kiện . Nếu chúng đã được giải quyết, hàm được giải quyết được truyền cho nó vẫn được gọi.

Ngược lại, cuộc gọi lại xử lý các sự kiện. Vì vậy, nếu sự kiện bạn quan tâm đã xảy ra trước khi cuộc gọi lại được đăng ký, cuộc gọi lại sẽ không được gọi.

2. Đảo ngược kiểm soát

Cuộc gọi lại liên quan đến đảo ngược kiểm soát. Khi bạn đăng ký chức năng gọi lại với bất kỳ API nào, bộ thực thi Javascript sẽ lưu trữ chức năng gọi lại và gọi nó từ vòng lặp sự kiện một khi nó đã sẵn sàng để chạy.

Tham khảo vòng lặp Sự kiện Javascript để được giải thích.

Với Lời hứa , kiểm soát nằm trong chương trình gọi. Phương thức .then () có thể được gọi bất cứ lúc nào nếu chúng ta lưu trữ đối tượng lời hứa.


1
Tôi không biết tại sao nhưng đây có vẻ là một câu trả lời tốt hơn.
radiantshaw

13

Ngoài các câu trả lời khác, cú pháp ES2015 kết hợp hoàn hảo với các lời hứa, giảm mã nồi hơi hơn nữa:

// Sequentially:
api1()
  .then(r1 => api2(r1))
  .then(r2 => api3(r2))
  .then(r3 => {
      // Done
  });

// Parallel:
Promise.all([
    api1(),
    api2(),
    api3()
]).then(([r1, r2, r3]) => {
    // Done
});

5

Lời hứa không phải là cuộc gọi lại, cả hai đều là thành ngữ lập trình tạo điều kiện thuận lợi cho lập trình async. Sử dụng một kiểu lập trình không đồng bộ / chờ đợi bằng cách sử dụng coroutines hoặc trình tạo trả lại lời hứa có thể được coi là thành ngữ thứ 3 như vậy. Một so sánh các thành ngữ này trên các ngôn ngữ lập trình khác nhau (bao gồm Javascript) có tại đây: https://github.com/KjellSchubert/promise-future-task


4

Không hoàn toàn không.

Gọi lại đơn giản là các hàm Trong JavaScript sẽ được gọi và sau đó được thực thi sau khi thực hiện một chức năng khác đã kết thúc. Vậy nó xảy ra như thế nào?

Trên thực tế, trong JavaScript, các hàm tự nó được coi là các đối tượng và do đó là tất cả các đối tượng khác, thậm chí các hàm có thể được gửi dưới dạng đối số cho các hàm khác . Trường hợp sử dụng phổ biến và chung nhất mà người ta có thể nghĩ đến là hàm setTimeout () trong JavaScript.

Hứa hẹn không là gì ngoài cách tiếp cận ngẫu hứng hơn nhiều trong việc xử lý và cấu trúc mã không đồng bộ so với thực hiện tương tự với các cuộc gọi lại.

Promise nhận được hai Callbacks trong hàm constructor: giải quyết và từ chối. Các cuộc gọi lại bên trong hứa hẹn cung cấp cho chúng tôi quyền kiểm soát chi tiết đối với việc xử lý lỗi và các trường hợp thành công. Cuộc gọi lại giải quyết được sử dụng khi việc thực hiện lời hứa được thực hiện thành công và cuộc gọi lại từ chối được sử dụng để xử lý các trường hợp lỗi.


2

Không có lời hứa nào chỉ là trình bao bọc cho các cuộc gọi lại

ví dụ Bạn có thể sử dụng lời hứa gốc javascript với nút js

my cloud 9 code link : https://ide.c9.io/adx2803/native-promises-in-node

/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
    request.get(url, function (error, response, body) {
    if (!error && response.statusCode == 200) {
        resolve(body);
    }
    else {
        reject(error);
    }
    })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
//get the post with post id 100
promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
var obj = JSON.parse(result);
return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
})
.catch(function (e) {
    console.log(e);
})
.then(function (result) {
    res.end(result);
}
)

})


var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("Example app listening at http://%s:%s", host, port)

})


//run webservice on browser : http://localhost:8081/listAlbums

0

Lời hứa JavaScript thực sự sử dụng các hàm gọi lại để xác định những việc cần làm sau khi Lời hứa đã được giải quyết hoặc bị từ chối, do đó cả hai đều không khác nhau về cơ bản. Ý tưởng chính đằng sau Promise là thực hiện các cuộc gọi lại - đặc biệt là các cuộc gọi lại lồng nhau, nơi bạn muốn thực hiện một loại hành động, nhưng nó sẽ dễ đọc hơn.


0

Hứa hẹn tổng quan:

Trong JS, chúng ta có thể bao bọc các hoạt động không đồng bộ (ví dụ như các cuộc gọi cơ sở dữ liệu, các cuộc gọi AJAX) trong các lời hứa. Thông thường chúng tôi muốn chạy một số logic bổ sung trên dữ liệu được truy xuất. JS hứa hẹn có các hàm xử lý xử lý kết quả của các hoạt động không đồng bộ. Các hàm xử lý thậm chí có thể có các hoạt động không đồng bộ khác trong chúng, có thể dựa vào giá trị của các hoạt động không đồng bộ trước đó.

Một lời hứa luôn có 3 trạng thái sau:

  1. đang chờ xử lý: trạng thái bắt đầu của mọi lời hứa, không được thực hiện cũng không bị từ chối.
  2. hoàn thành: Các hoạt động hoàn thành thành công.
  3. bị từ chối: Hoạt động thất bại.

Một lời hứa đang chờ xử lý có thể được giải quyết / thực hiện đầy đủ hoặc từ chối với một giá trị. Sau đó, các phương thức xử lý sau lấy các cuộc gọi lại làm đối số được gọi:

  1. Promise.prototype.then() : Khi lời hứa được giải quyết, đối số gọi lại của hàm này sẽ được gọi.
  2. Promise.prototype.catch() : Khi lời hứa bị từ chối, đối số gọi lại của hàm này sẽ được gọi.

Mặc dù các kỹ năng phương pháp trên có được các đối số gọi lại nhưng chúng vượt trội hơn nhiều so với việc chỉ sử dụng các cuộc gọi lại ở đây là một ví dụ sẽ làm rõ rất nhiều:

Thí dụ

function createProm(resolveVal, rejectVal) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                console.log("Resolved");
                resolve(resolveVal);
            } else {
                console.log("Rejected");
                reject(rejectVal);
            }
        }, 1000);
    });
}

createProm(1, 2)
    .then((resVal) => {
        console.log(resVal);
        return resVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
        return resVal + 2;
    })
    .catch((rejectVal) => {
        console.log(rejectVal);
        return rejectVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
    })
    .finally(() => {
        console.log("Promise done");
    });

  • Hàm createdProm tạo ra một lời hứa được giải quyết hoặc từ chối dựa trên Nr ngẫu nhiên sau 1 giây
  • Nếu lời hứa được giải quyết, thenphương thức đầu tiên được gọi và giá trị được giải quyết được chuyển vào dưới dạng đối số của cuộc gọi lại
  • Nếu lời hứa bị từ chối, catchphương thức đầu tiên được gọi và giá trị bị từ chối được truyền vào dưới dạng đối số
  • Các phương thức catchthentrả về hứa hẹn đó là lý do tại sao chúng ta có thể xâu chuỗi chúng. Chúng bao bọc mọi giá trị được trả về Promise.resolvevà bất kỳ giá trị ném nào (sử dụng throwtừ khóa) trong Promise.reject. Vì vậy, bất kỳ giá trị nào được trả về đều được chuyển thành một lời hứa và trên lời hứa này, chúng ta lại có thể gọi hàm xử lý.
  • Chuỗi hứa hẹn cho chúng ta kiểm soát tốt hơn và tổng quan tốt hơn so với các cuộc gọi lại lồng nhau. Ví dụ, catchphương thức xử lý tất cả các lỗi đã xảy ra trước khi catchxử lý.
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.