Merangkai Janji async dengan 1 tekad dan 1 penolakan?

Saya memiliki fungsi yang harus melakukan sesuatu yang asinkron, dalam beberapa langkah. Pada setiap langkahnya bisa gagal. Ini mungkin gagal sebelum langkah 1, jadi Anda mungkin langsung mengetahui hasilnya, atau setelah 1,5 detik. Jika gagal, ia harus menjalankan panggilan balik. Idem ketika berhasil. (Saya sengaja menggunakan bila, karena ini bukan hanya jika: waktu itu penting.)

Saya pikir Janji itu sempurna, karena async dan hanya diselesaikan sekali, tetapi masih ada masalah: kapan gagal? Saya dapat melihat secara jelas kapan berhasil (setelah langkah terakhir), tetapi kapan gagal? Di dalam/sebelum langkah apa pun.

Inilah yang saya miliki sekarang, tapi itu konyol:

function clickSpeed() {
    return new Promise(function(resolve, reject) {
        if ( test('one') ) {
            return setTimeout(function() {
                if ( test('two') ) {
                    return setTimeout(function() {
                        if ( test('three') ) {
                            return setTimeout(function() {
                                console.log('resolving...');
                                resolve();
                            }, 500);
                        }
                        console.log('rejecting...');
                        reject();
                    }, 500);
                }
                console.log('rejecting...');
                reject();
            }, 500);
        }
        console.log('rejecting...');
        reject();
    });
}

(test() lolos atau gagal satu langkah secara acak.)

Mainkan biola di sini: http://jsfiddle.net/rudiedirkx/zhdrjjx1/

Saya kira solusinya adalah dengan merangkai janji, yang menyelesaikan atau menolak setiap langkah..? Mungkin. Apakah itu suatu hal? Bagaimana saya menerapkannya?

Bisakah ini berhasil untuk sejumlah langkah yang tidak diketahui?


person Rudie    schedule 23.11.2015    source sumber
comment
Itu gagal segera setelah Anda membiarkannya gagal. Tidak yakin saya mengerti apa pertanyaannya.   -  person Bergi    schedule 23.11.2015
comment
Apa hasil yang diharapkan dari return pada return setTimeout(function() { ?   -  person guest271314    schedule 23.11.2015
comment
@ guest271314: Saya pikir return hanya ada untuk mencegah console.log('rejecting...'); reject(); berjalan   -  person Bergi    schedule 23.11.2015
comment
@Bergi Kapan saya membiarkannya gagal? Langkah-langkahnya tidak sinkron, jadi saya tidak tahu apakah langkah selanjutnya akan berhasil. Apakah saya harus memeriksa setiap langkah secara eksplisit, seperti pada kode di atas?   -  person Rudie    schedule 23.11.2015
comment
@guest271314 Pengembaliannya hanya untuk memutus arus, jadi tidak ditolak.   -  person Rudie    schedule 23.11.2015
comment
Saya pikir kamu ingin sesuatu seperti ini : promise.then(function(res, reject) { res('step1') or reject('err')}).then(function(res) { res('step2')}).catch(function(err) { console.log(err) })   -  person Errorpro    schedule 23.11.2015
comment
@Rudie: Ya, Anda harus secara eksplisit membiarkannya gagal di setiap langkah, karena jika tidak, itu tidak akan gagal. Tentu saja, Anda harus menggunakan janji dengan cara yang berbeda.   -  person Bergi    schedule 23.11.2015
comment
tapi kapan gagal? Di dalam/sebelum langkah apa pun. , Pengembaliannya hanya untuk memutus arus, jadi tidak ditolak. test tidak menolak janji? Bagaimana reject dipanggil berdasarkan nilai kembalian test, jika setTimeout memutus aliran?   -  person guest271314    schedule 23.11.2015
comment
@Bergi Bedanya bagaimana? Tentu saja kenapa?   -  person Rudie    schedule 23.11.2015
comment
@ guest271314 test() hanya mengembalikan bool. Ini mungkin bukan fungsi, tetapi pernyataan, atau periksa objek/properti lain. Penolakan terjadi setelah setiap IF.   -  person Rudie    schedule 23.11.2015
comment
@ guest271314 test() mengembalikan bool, tetapi acak hanya untuk pengujian. Dibutuhkan apa pun dari tempat lain, yang dapat menolak janji tersebut, atau memajukannya ke langkah berikutnya. Cektest() tidak selalu berfungsi dan tidak mengenal janji.   -  person Rudie    schedule 23.11.2015


Jawaban (3)


Anda dapat menulis ulang solusi Anda terhadap janji secara harfiah:

function sleep(ms) {
    return new Promise(function(resolve) {
        setTimeout(resolve, ms);
    });
}

function clickSpeed() {
    if ( test('one') ) {
        return sleep(500).then(function() {
            if ( test('two') ) {
                return sleep(500).then(function() {
                    if ( test('three') ) {
                        return sleep(500).then(function() {
                            console.log('resolving...');
                        });
                    }
                    console.log('rejecting...');
                    return Promise.reject();
                });
            }
            console.log('rejecting...');
            return Promise.reject();
        });
    }
    console.log('rejecting...');
    return Promise.reject();
}

Namun, itu masih sangat buruk. Saya lebih suka membalikkan if/elses sebagai langkah pertama:

function clickSpeed() {
    if (!test('one')) {
        console.log('rejecting...');
        return Promise.reject();
    }
    return sleep(500).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            return Promise.reject();
        }
        return sleep(500).then(function() {
            if (!test('three')) {
                console.log('rejecting...');
                return Promise.reject();
            }
            return sleep(500).then(function() {
                console.log('resolving...');
            });
        });
    });
}

Namun kemudian kami juga dapat menghapus sarang callback ini. Biasanya hal ini tidak mungkin dilakukan saat Anda melakukan percabangan dengan if, namun dalam kasus ini satu-satunya hasil alternatif adalah penolakan, seperti throwing, dan tidak akan menjalankan panggilan balik then yang dirantai.

function clickSpeed() {
    return Promise.resolve() // only so that the callbacks look alike, and to use throw
    .then(function() {
        if (!test('one')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        if (!test('two')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500)
    }).then(function() {
        if (!test('three')) {
            console.log('rejecting...');
            throw;
        }
        return sleep(500);
    }).then(function() {
        console.log('resolving...');
    });
}

Sekarang Anda dapat membuat test tersebut mengeluarkan pengecualian itu sendiri sehingga Anda tidak memerlukan if lagi, dan Anda dapat memindahkan pernyataan console.log('rejecting...'); ke dalam .catch().

Dan meskipun menurut saya ini hanya contoh Anda yang semua langkahnya terlihat sama, Anda juga dapat dengan mudah membuat rantai janji secara dinamis dari daftar:

function clickSpeed() {
    return ['one', 'two', 'three'].reduce(function(p, cur) {
        return p.then(function() {
            if (!test(cur))
                throw new Error('rejecting...');
            else
                return sleep(500);
        });
    }, Promise.resolve());
}
person Bergi    schedule 23.11.2015
comment
Bisakah menjelaskan efek throw pada postingan? - person guest271314; 23.11.2015
comment
Itulah yang saya mainkan, tetapi tidak dapat memahaminya: jsfiddle.net/rudiedirkx/ zhdrjjx1/1 Ini lebih mirip! - person Rudie; 23.11.2015
comment
@ guest271314: throw; tidak berbeda dengan return Promise.reject(); saat berada di dalam panggilan balik then. - person Bergi; 23.11.2015
comment
@Rudie: Ah, saya rindu melihat biola Anda. Ia menderita Promise antipattern konstruktor, dan tidak berfungsi karena panggilan ke reject() tidak memutus rantai seperti pengecualian. - person Bergi; 23.11.2015
comment
@Bergi Apa yang dikembalikan ke onRejected dari throw ? Mengenai antipattern konstruktor Janji stackoverflow.com/q/23803743/1048572 , apakah premis bahwa new Promise(function(resolve, reject){}) tidak boleh digunakan? - person guest271314; 23.11.2015
comment
@ guest271314: Tidak ada. Artinya, undefined. Sama seperti argumen ke Promise.reject(). - person Bergi; 23.11.2015
comment
@ guest271314: Mengenai antipattern konstruktor Promise, harap baca jawaban atas pertanyaan tertaut. Mereka tidak mengatakan tidak pernah. Sebenarnya saya menggunakannya dengan benar dalam jawaban ini (sleep). - person Bergi; 23.11.2015
comment
@Bergi Ya dan ya, tepatnya. Saya tahu ada cara Promises yang bagus. Cara ini bahkan lebih dapat dirantai dengan pemanggilan fungsi clickSpeed() yang persis seperti yang saya harapkan dapat terjadi. Terima kasih! - person Rudie; 23.11.2015
comment
@Bergi Baca halaman , beberapa kali sekarang. Apa perbedaan antara sleep seperti di postingan dan function sleep(ms) { Promise.resolve().then(function() { /*setTimeout here*/ })} ? Khususnya @thefourtheyes Jawaban referensi ke programmers.stackexchange.com/a/279003 tampaknya menunjukkan tidak menggunakan konstruktor sama sekali? , berdasarkan kinerja? Mengalami kesulitan di sini dalam mencoba menafsirkan secara pasti kapan, kapan tidak. Akan mengajukan Pertanyaan, meskipun mungkin akan ditutup sebagai duplikat. Tidak sepenuhnya jelas, di sini, bagaimana mengevaluasi kapan harus menggunakan pola konstruktor atau Promise.resolve()? - person guest271314; 23.11.2015
comment
@ guest271314: Gunakan Promise.resolve bila Anda bisa. Terkadang Anda tidak bisa, Anda memerlukan konstruktor Promise. sleep hanyalah salah satu contohnya, implementasi Anda tidak berhasil. Cobalah untuk mengetahui alasannya. Tidak, ini tidak ada hubungannya dengan kinerja (yang khusus untuk perpustakaan, pertanyaan pemrogram yang Anda tautkan sepertinya membahas Bluebird). - person Bergi; 23.11.2015

Karena waktu Anda monoton, dan "pekerjaan" Anda sudah ditentukan sebelumnya, saya akan memfaktorkan ulang kode untuk menggunakan setInterval() dengan konteks untuk melacak "pekerjaan" atau "langkah" yang diperlukan. Faktanya, Anda bahkan tidak harus menggunakan Promise saat melakukannya, meskipun Anda masih bisa, dan jika Anda ingin menangani rantai lebih lanjut, ada baiknya:

function clickSpeed(resolve, reject) {
  var interval = setInterval(function(work) {
    try {
      var current = work.shift();
      if(!test(current)) { // Do current step's "work"
        clearInterval(interval); // reject on failure and clear interval
        console.log('rejecting...', current);
        reject();
      }
      else if(!work.length) { // If this was the last step
        clearInterval(interval); // resolve (success!) and clear interval
        console.log('resolving...');
        resolve();
      }
    }
    catch(ex) { // reject on exceptions as well
      reject(ex);
      clearInterval(interval);
    }
  }, 500, ['one', 'two', 'three']); // "work" array
}

Fungsi tersebut dapat dipanggil "secara langsung" dengan pengendali penyelesaian/penolakan, atau digunakan sebagai argumen konstruktor Janji.

Lihat contoh lengkap di JSFiddle yang dimodifikasi.


Untuk mengatasi komentar terlalu banyak boilerplate Bergi, kode dapat ditulis dengan lebih ringkas, tanpa perlu login:

function clickSpeed(resolve, reject) {
    function done(success, val) {
        clearInterval(interval);
        success ? resolve(val) : reject(val);
    }

    var interval = setInterval(function(work) {
        try {
            if(test(work.shift()) || done(false)) {
                work.length || done(true);
            }
        }
        catch(ex) { // reject on exceptions as well
            done(false, ex);
        }
    }, 500, ['one', 'two', 'three']); // "work" array
}
person Amit    schedule 26.11.2015
comment
Hei, itu cukup pintar! Saya suka itu. - person Rudie; 26.11.2015
comment
Saya sarankan untuk menghindari setInterval saat menggunakan janji, itu tidak cocok. Dan Anda kehilangan keamanan, dan harus kembali mengotak-atik panggilan balik. Selain itu, terlalu banyak boilerplate… Kemonotonan dapat dengan mudah digunakan dalam kode janji dengan mudah (lihat pembaruan pada jawaban saya). - person Bergi; 26.11.2015
comment
@Bergi - Solusinya tidak benar-benar menggunakan janji, tetapi memungkinkan pembungkusan dengan Janji. Penanganan pengecualian dilakukan di dalam, dan mekanisme panggilan balik dirancang khusus (seperti yang dijelaskan OP). Boilerplate minimal (seperti yang ditunjukkan pada versi yang diperbarui) dan sebanding dengan solusi Anda - yang menurut saya juga bagus. - person Amit; 26.11.2015
comment
Ups, saya pasti sudah membaca pernyataan try catch itu secara berlebihan, saya akan menariknya kembali. Tetap saja saya kira kebutuhan untuk try itulah yang membuat solusi Anda sedikit lebih lama daripada solusi saya :-) - person Bergi; 26.11.2015

Pertama, mari kita atur ulang pernyataan pembuka dengan sangat halus agar mencerminkan penggunaan janji yang lebih umum dan lebih tepat.

Alih-alih :

sebuah fungsi yang harus melakukan sesuatu yang asinkron, dalam beberapa langkah

Katakanlah :

sebuah fungsi yang harus melakukan sesuatu dalam beberapa langkah asinkron

Jadi, pertama-tama kita mungkin memilih untuk menulis sebuah fungsi yang mengeksekusi rantai janji dan mengembalikan janji yang dihasilkannya:

function doAnAsyncSequence() {
    return Promise.resolve()
    .then(function() {
        doSomethingAsync('one');
    })
    .then(function() {
        doSomethingAsync('two');
    })
    .then(function() {
        doSomethingAsync('three');
    });
}

Dan, demi demonstrasi, kita dapat menulis doSomethingAsync() sedemikian rupa sehingga mengembalikan janji yang memiliki peluang 50:50 untuk diselesaikan: ditolak (yang lebih berguna di sini daripada penundaan):

function doSomethingAsync(x) {
    return new Promise(function(resolve, reject) {
        if(Math.random() > 0.5 ) {
            resolve(x);
        } else {
            reject(x); // importantly, this statement reports the input argument `x` as the reason for failure, which can be read and acted on where doSomethingAsync() is called.
        }
    });
}

Lalu, bagian inti dari pertanyaannya:

kapan gagalnya?

mungkin dapat diulangi:

kapan gagal?

yang merupakan pertanyaan yang lebih realistis karena kita biasanya akan memanggil proses asinkron yang pengaruhnya kecil terhadap kita (proses tersebut mungkin berjalan di beberapa server di tempat lain di dunia), dan yang kita harap akan berhasil tetapi mungkin gagal secara acak. Jika ya, kode kami (dan/atau pengguna akhir kami) ingin mengetahui kode mana yang gagal, dan alasannya.

Dalam kasus doAnAsyncSequence(), kita dapat melakukannya sebagai berikut:

doAnAsyncSequence().then(function(result) {
    console.log(result); // if this line executes, it will always log "three", the result of the *last* step in the async sequence.
}, function(reason) {
    console.log('error: ' + reason);
});

Meskipun tidak ada pernyataan console.log() di doAnAsyncSequence() atau doSomethingAsync() :

  • pada kesuksesan, kita dapat mengamati hasil keseluruhannya (yang akan selalu menjadi "tiga" dalam contoh ini).
  • jika terjadi kesalahan, kita tahu persis langkah async mana yang menyebabkan proses gagal ("satu", "dua" atau "tiga").

Cobalah


Jadi itulah teorinya.

Untuk menjawab pertanyaan spesifik (seperti yang saya pahami) ...

untuk doSomethingAsync(), tulis :

function test_(value, delay) {
    return new Promise(function(resolve, reject) {
        //call your own test() function immediately
        var result = test(value);
        // resolve/reject the returned promise after a delay
        setTimeout(function() {
            result ? resolve() : reject(value);
        }, delay);
    });
}

untuk doAnAsyncSequence(), tulis :

function clickSpeed() {
    var delayAfterTest = 500;
    return Promise.resolve()
    .then(function() {
        test_('one', delayAfterTest);
    })
    .then(function() {
        test_('two', delayAfterTest);
    })
    .then(function() {
        test_('three', delayAfterTest);
    });
}

Dan panggil sebagai berikut:

clickSpeed().then(function() {
    console.log('all tests passed');
}, function(reason) {
    console.log('test sequence failed at: ' + reason);
});
person Roamer-1888    schedule 26.11.2015
comment
Sangat mudah dibaca, tetapi saya memerlukan waktu tunggu, karena hal-hal lain melakukan hal-hal yang tidak sinkron dan saya harus menunggu ~ 500 ms. - person Rudie; 26.11.2015
comment
Seperti yang saya katakan, doSomethingAsync() saya adalah untuk demonstrasi. Dalam kode dunia nyata, Anda mungkin memilih untuk menulis sesuatu yang sama sekali berbeda. - person Roamer-1888; 26.11.2015