Bukankah janji hanya panggilan balik?

430

Saya telah mengembangkan JavaScript selama beberapa tahun dan saya tidak mengerti apa-apa tentang janji.

Sepertinya yang saya lakukan hanyalah perubahan:

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

Yang mana saya bisa menggunakan pustaka seperti async , dengan sesuatu seperti:

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

Kode yang lebih banyak dan lebih sedikit dibaca. Saya tidak mendapatkan apa-apa di sini, tidak secara mendadak 'datar' juga. Belum lagi harus mengubah hal-hal menjadi janji.

Jadi, apa masalah besar tentang janji di sini?

Benjamin Gruenbaum
sumber
11
Pada topik : ada artikel yang sangat informatif tentang Janji pada Html5Rocks: html5rocks.com/en/tutorials/es6/promises
ComFreek
2
Fyi jawaban yang Anda terima adalah daftar tua yang sama dari manfaat sepele yang sama sekali bukan titik janji dan bahkan tidak meyakinkan saya untuk menggunakan janji: /. Apa yang meyakinkan saya untuk menggunakan janji-janji adalah aspek DSL seperti yang dijelaskan dalam jawaban Oscar
Esailija
@ Eailija baik-baik saja, leet Anda berbicara meyakinkan saya. Saya telah menerima jawaban yang lain walaupun saya pikir jawaban Bergi menimbulkan beberapa poin yang sangat bagus (dan berbeda) juga.
Benjamin Gruenbaum
@Esailija "Apa yang meyakinkan saya untuk menggunakan janji adalah aspek DSL seperti yang dijelaskan dalam jawaban Oscar" << Apa itu "DSL"? dan apa "aspek DSL" yang Anda maksud?
monsto
1
@monsto: DSL: Domain Specific Language, bahasa yang sengaja dirancang untuk digunakan dalam subset tertentu dari suatu sistem (misalnya SQL atau ORM untuk berbicara dengan database, regex untuk menemukan pola, dll). Dalam konteks ini "DSL" adalah API Promise yang, jika Anda menyusun kode Anda seperti yang dilakukan Oscar, hampir seperti gula sintaksis yang melengkapi JavaScript untuk mengatasi konteks khusus operasi async. Janji membuat beberapa idiom yang mengubahnya menjadi hampir bahasa yang dirancang untuk memungkinkan programmer untuk lebih mudah memahami aliran mental yang agak sulit dipahami dari jenis struktur ini.
Michael Ekoka

Jawaban:

631

Janji bukan panggilan balik. Sebuah janji mewakili hasil operasi asinkron di masa depan . Tentu saja, menuliskannya seperti yang Anda lakukan, Anda mendapat sedikit manfaat. Tetapi jika Anda menuliskannya dengan cara yang seharusnya digunakan, Anda dapat menulis kode asinkron dengan cara yang menyerupai kode sinkron dan jauh lebih mudah diikuti:

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

Tentu saja, tidak jauh lebih sedikit kode, tetapi jauh lebih mudah dibaca.

Tapi ini bukan akhirnya. Mari kita temukan manfaat sebenarnya: Bagaimana jika Anda ingin memeriksa kesalahan dalam salah satu langkah? Akan sangat sulit untuk melakukannya dengan panggilan balik, tetapi dengan janji, adalah sepotong kue:

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

Sama seperti try { ... } catchbalok.

Bahkan lebih baik:

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

Dan bahkan lebih baik: Bagaimana jika mereka 3 panggilan ke api, api2, api3bisa dijalankan secara bersamaan (misalnya jika mereka panggilan AJAX) tetapi Anda harus menunggu untuk tiga? Tanpa janji, Anda harus membuat semacam penghitung. Dengan janji-janji, menggunakan notasi ES6, adalah sepotong kue dan cukup rapi:

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

Semoga Anda melihat Janji dalam cahaya baru sekarang.

Oscar Paz
sumber
124
Mereka seharusnya tidak menamainya sebagai "Janji". "Masa depan" setidaknya 100x lebih baik.
Pacerier
12
@Pacerier karena Future tidak dinodai oleh jQuery?
Esailija
5
Pola alternatif (tergantung pada apa yang diinginkan: api (). Lalu (api2) .then (api3) .then (doWork); Yaitu, jika fungsi api2 / api3 mengambil input dari langkah terakhir, dan mengembalikan janji baru sendiri, mereka hanya bisa dirantai tanpa pembungkus ekstra. Artinya, mereka menulis
Dtips
1
Bagaimana jika ada async yang beroperasi di api2dan api3? apakah yang terakhir .thenhanya akan dipanggil setelah operasi async selesai?
NiCk Newman
8
Mengapa Anda memberi tag saya? Saya baru saja memperbaiki tata bahasa sedikit. Saya bukan ahli JS. :)
Scott Arciszewski
169

Ya, Janji adalah panggilan balik yang tidak sinkron. Mereka tidak dapat melakukan apa pun yang tidak bisa dilakukan panggilan balik, dan Anda menghadapi masalah yang sama dengan asinkron seperti halnya dengan panggilan balik biasa.

Namun, Janji lebih dari sekadar panggilan balik. Mereka adalah abstraksi yang sangat perkasa, memungkinkan kode fungsional lebih bersih dan lebih baik dengan boilerplate yang lebih rentan kesalahan.

Jadi apa ide utamanya?

Janji adalah objek yang mewakili hasil perhitungan tunggal (tidak sinkron). Mereka menyelesaikan hasil itu hanya sekali. Ada beberapa hal yang artinya:

Janji menerapkan pola pengamat:

  • Anda tidak perlu tahu panggilan balik yang akan menggunakan nilai sebelum tugas selesai.
  • Alih-alih mengharapkan panggilan balik sebagai argumen untuk fungsi Anda, Anda dapat dengan mudah return menjadi objek Janji
  • Janji itu akan menyimpan nilai, dan Anda bisa transparan menambahkan panggilan balik kapan pun Anda mau. Ini akan dipanggil ketika hasilnya tersedia. "Transparansi" menyiratkan bahwa ketika Anda memiliki janji dan menambahkan panggilan balik ke dalamnya, itu tidak membuat perbedaan pada kode Anda apakah hasilnya telah tiba - API dan kontraknya sama, menyederhanakan banyak caching / memoisasi.
  • Anda dapat menambahkan beberapa panggilan balik dengan mudah

Janji-janji tidak dapat ditagih ( monadik , jika Anda mau ):

  • Jika Anda perlu mengubah nilai yang diwakili oleh janji, Anda memetakan fungsi transformasi atas janji dan mendapatkan kembali janji baru yang mewakili hasil yang diubah. Anda tidak dapat secara sinkron mendapatkan nilai untuk menggunakannya, tetapi Anda bisa dengan mudah mengangkat transformasi dalam konteks janji. Tidak ada panggilan balik boilerplate.
  • Jika Anda ingin membuat dua tugas asinkron, Anda dapat menggunakan .then()metode ini. Dibutuhkan panggilan balik untuk dipanggil dengan hasil pertama, dan mengembalikan janji untuk hasil janji bahwa panggilan balik kembali.

Kedengarannya rumit? Waktu untuk contoh kode.

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)

Meratakan tidak datang secara ajaib, tetapi Anda dapat dengan mudah melakukannya. Untuk contoh Anda yang sangat bersarang, yang setara (dekat) akan menjadi

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

Jika melihat kode dari metode ini membantu memahami, inilah janji paling mendasar dalam beberapa baris .

Apa masalah besar tentang janji?

Abstraksi Janji memungkinkan kompabilitas fungsi yang jauh lebih baik. Sebagai contoh, di sebelah thenuntuk chaining, allfungsi menciptakan janji untuk hasil gabungan dari janji-janji paralel ganda.

Last but not least Janji datang dengan penanganan kesalahan terintegrasi. Hasil perhitungannya bisa berupa apakah janji itu dipenuhi dengan nilai, atau ditolak dengan alasan. Semua fungsi komposisi menangani ini secara otomatis dan menyebarkan kesalahan dalam rantai janji, sehingga Anda tidak perlu mempedulikannya secara eksplisit di mana-mana - berbeda dengan implementasi panggilan balik biasa. Pada akhirnya, Anda dapat menambahkan callback kesalahan khusus untuk semua pengecualian yang terjadi.

Belum lagi harus mengubah hal-hal menjadi janji.

Itu cukup sepele sebenarnya dengan perpustakaan janji yang baik, lihat Bagaimana cara mengonversi API panggilan balik yang ada ke janji?

Bergi
sumber
hai Bergi, apakah Anda memiliki sesuatu yang menarik untuk ditambahkan ke pertanyaan SO ini? stackoverflow.com/questions/22724883/…
Sebastien Lorber
1
@ Sebastien: Saya belum tahu banyak tentang Scala, dan saya hanya bisa mengulangi apa yang dikatakan Benjamin :-)
Bergi
3
Hanya sedikit komentar: Anda tidak dapat menggunakan .then(console.log), karena console.log tergantung pada konteks konsol. Dengan cara ini akan menyebabkan kesalahan doa ilegal. Gunakan console.log.bind(console)atau x => console.log(x)untuk mengikat konteks.
Tamas Hegedus
3
@hege_hegedus: Ada lingkungan di mana consolemetode sudah terikat. Dan tentu saja, saya hanya mengatakan bahwa kedua sarang memiliki perilaku yang persis sama, tidak ada di antara mereka yang bekerja:
P
1
Tadi sangat menyenangkan. Inilah yang saya butuhkan: lebih sedikit kode dan lebih banyak interpretasi. Terima kasih.
Adam Patterson
21

Selain jawaban yang sudah ada, dengan fungsi panah ES6, Janji berubah dari kerdil biru kecil yang bersinar lurus menjadi raksasa merah. Itu akan runtuh menjadi supernova:

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

Seperti yang ditunjukkan oligofren , tanpa argumen antara panggilan api, Anda tidak memerlukan fungsi pembungkus anonim sama sekali:

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

Dan akhirnya, jika Anda ingin mencapai level lubang hitam supermasif, Janji bisa ditunggu:

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

    return api3Result;
}
John Weisz
sumber
9
"dengan ES6 panah fungsi Janji berbalik dari bintang biru kecil sederhana bersinar langsung ke raksasa merah Itu akan runtuh ke dalam supernova." Terjemahan: Menggabungkan ES6 panah fungsi dengan Janji mengagumkan :)
user3344977
3
Itu membuat Janji terdengar seperti bencana kosmik, yang menurutku bukan niatmu.
Michael McGinnis
Jika Anda tidak menggunakan argumen dalam apiXmetode, Anda mungkin juga melewatkan fungsi panah sama sekali: api().then(api2).then(api3).then(r3 => console.log(r3)).
oligofren
@MichaelMcGinnis - Dampak menguntungkan dari Janji pada panggilan balik yang membosankan seperti supernova yang meledak di sudut ruang yang gelap.
John Weisz
Saya tahu Anda bersungguh-sungguh, tetapi janji-janji itu cukup jauh dari "supernova". Melanggar hukum monadik atau kurangnya dukungan untuk kasus penggunaan yang lebih kuat seperti pembatalan atau pengembalian beberapa nilai muncul di pikiran.
Dmitri Zaitsev
15

Selain jawaban luar biasa di atas, 2 poin lagi dapat ditambahkan:

1. Perbedaan semantik:

Janji mungkin sudah diselesaikan pada saat penciptaan. Ini berarti mereka menjamin kondisi daripada peristiwa . Jika sudah dipecahkan, fungsi yang diselesaikan yang diteruskan ke sana masih dipanggil.

Sebaliknya, panggilan balik menangani acara. Jadi, jika acara yang Anda minati telah terjadi sebelum callback terdaftar, callback tidak dipanggil.

2. Pembalikan kontrol

Callback melibatkan inversi kontrol. Ketika Anda mendaftarkan fungsi panggilan balik dengan API apa pun, runtime Javascript menyimpan fungsi panggilan balik dan memanggilnya dari loop acara setelah siap dijalankan.

Lihat loop Acara Javascript untuk penjelasan.

Dengan Janji , kendali berada dengan program panggilan. Metode .then () dapat dipanggil kapan saja jika kita menyimpan objek janji.

dww
sumber
1
Saya tidak tahu mengapa tetapi ini sepertinya jawaban yang lebih baik.
radiantshaw
13

Selain jawaban lain, sintaksis ES2015 berpadu mulus dengan janji-janji, mengurangi lebih banyak lagi kode boilerplate:

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

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

Janji bukan panggilan balik, keduanya adalah idiom pemrograman yang memfasilitasi pemrograman async. Menggunakan gaya pemrograman async / menunggu menggunakan coroutine atau generator yang mengembalikan janji dapat dianggap sebagai idiom ketiga. Perbandingan idiom-idiom ini di berbagai bahasa pemrograman (termasuk Javascript) ada di sini: https://github.com/KjellSchubert/promise-future-task

Kjell Schubert
sumber
5

Tidak, tidak sama sekali.

Callback hanyalah Fungsi Dalam JavaScript yang harus dipanggil dan kemudian dieksekusi setelah eksekusi fungsi lain selesai. Jadi bagaimana itu terjadi?

Sebenarnya, dalam JavaScript, fungsi itu sendiri dianggap sebagai objek dan karenanya sebagai semua objek lain, bahkan fungsi dapat dikirim sebagai argumen ke fungsi lain . Kasus penggunaan paling umum dan umum yang dapat dipikirkan seseorang adalah fungsi setTimeout () dalam JavaScript.

Janji hanyalah pendekatan yang jauh lebih improvisasi dari penanganan dan penataan kode asinkron dibandingkan dengan melakukan hal yang sama dengan panggilan balik.

Janji menerima dua Callback dalam fungsi konstruktor: menyelesaikan dan menolak. Panggilan balik dalam janji ini memberi kami kendali yang baik atas penanganan kesalahan dan kasus sukses. Resolve callback digunakan ketika eksekusi janji dilakukan dengan sukses dan penolakan panggilan balik digunakan untuk menangani kasus kesalahan.

Ayush Jain
sumber
2

Tidak ada janji yang hanya dibungkus dengan panggilan balik

contoh Anda dapat menggunakan javascript asli yang dijanjikan dengan simpul 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
Apoorv
sumber
1

Javascript Janji sebenarnya menggunakan fungsi callback untuk menentukan apa yang harus dilakukan setelah Janji telah diselesaikan atau ditolak, oleh karena itu keduanya tidak berbeda secara mendasar. Gagasan utama di balik Janji adalah untuk mengambil panggilan balik - terutama panggilan balik bersarang di mana Anda ingin melakukan semacam tindakan, tetapi itu akan lebih mudah dibaca.

Hamid Shoja
sumber
0

Ikhtisar janji:

Di JS kita bisa membungkus operasi asinkron (misalnya panggilan basis data, panggilan AJAX) dengan janji. Biasanya kami ingin menjalankan beberapa logika tambahan pada data yang diambil. Janji-janji JS memiliki fungsi handler yang memproses hasil dari operasi asinkron. Fungsi handler bahkan dapat memiliki operasi asinkron lain di dalamnya yang dapat mengandalkan nilai operasi asinkron sebelumnya.

Sebuah janji selalu memiliki 3 negara berikut:

  1. tertunda: keadaan awal setiap janji, tidak dipenuhi atau ditolak.
  2. terpenuhi: Operasi selesai dengan sukses.
  3. ditolak: Operasi gagal.

Janji yang tertunda dapat diselesaikan / dipenuhi atau ditolak dengan suatu nilai. Kemudian metode handler berikut yang menerima panggilan balik sebagai argumen disebut:

  1. Promise.prototype.then() : Ketika janji terselesaikan argumen callback dari fungsi ini akan dipanggil.
  2. Promise.prototype.catch() : Ketika janji ditolak, argumen callback dari fungsi ini akan dipanggil.

Meskipun keterampilan metode di atas mendapatkan argumen callback mereka jauh lebih unggul daripada hanya menggunakan callback di sini adalah contoh yang akan banyak menjelaskan:

Contoh

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

  • Fungsi createProm membuat janji yang diselesaikan atau ditolak berdasarkan Nr acak setelah 1 detik
  • Jika janji terselesaikan, thenmetode pertama dipanggil dan nilai terselesaikan dilewatkan sebagai argumen dari callback
  • Jika janji itu ditolak, catchmetode pertama dipanggil dan nilai yang ditolak diteruskan sebagai argumen
  • Metode catchdan thenmengembalikan janji itu sebabnya kita bisa rantai mereka. Mereka Promise.resolvememasukkan nilai yang dikembalikan dan nilai yang dilemparkan (menggunakan throwkata kunci) di Promise.reject. Jadi setiap nilai yang dikembalikan ditransformasikan menjadi sebuah janji dan pada janji ini kita dapat lagi memanggil fungsi penangan.
  • Rantai janji memberi kami kontrol yang lebih baik dan tinjauan yang lebih baik daripada panggilan balik bersarang. Misalnya catchmetode ini menangani semua kesalahan yang terjadi sebelum catchpenangan.
Willem van der Veen
sumber