Topik wawancara representatif

Bagaimana Cara Mengimplementasikan Promise.all dengan Pengurutan dan Semantik Kegagalan yang Benar?

FrontendSedang
Tim Redaksi Offer.ccDipublikasikan Diperbarui

Pertanyaan

Tanpa memanggil Promise.all, implementasikan promiseAll(iterable) sehingga menghasilkan kembali perilaku inti dari metode bawaan tersebut.

Masalah dan Konteks yang Berlaku

Implementasikan promiseAll(iterable) tanpa memanggil Promise.all bawaan di dalam solusi Anda. Inputnya adalah iterable sinkron terbatas yang berisi promise, thenable, atau plain value. Outputnya adalah Promise baru.

Ketika setiap input terpenuhi (fulfill), hasilnya harus berupa array dalam urutan iterasi, terlepas dari urutan penyelesaiannya. Ketika ada input yang menolak (reject), Promise luar harus menolak dengan alasan dari penolakan pertama yang terjadi. Input kosong harus menghasilkan []. Jika pembacaan dari iterator itu sendiri melempar error (throw), Promise luar juga harus menolak.

Masalah ini cocok untuk sesi live coding posisi frontend atau JavaScript. Solusi ini boleh menggunakan Promise bawaan untuk mengasimilasi thenable dan menjadwalkan reaksi asinkron. Solusi tidak perlu mereproduksi setiap detail spesifikasi yang melibatkan konstruktor subclass, internal slot, dan penutupan iterator. Nyatakan batasan tersebut sebelum menulis kode: "perilaku inti" dan "implementasi yang sepenuhnya kompatibel dengan spesifikasi" adalah tugas yang berbeda.

Apa yang Dinilai oleh Pewawancara

Jawaban yang kuat menjabarkan semantik sebelum menulis perulangan (loop). Lima sinyal yang berguna adalah menerima iterable alih-alih hanya array, menormalisasi plain value dan thenable melalui Promise.resolve, mencadangkan indeks untuk menjaga urutan, menangani input kosong secara terpisah, dan mengarahkan error iterasi maupun penolakan promise ke penolakan luar.

Jawaban yang umum biasanya memanggil map pada sebuah array dan melakukan push pada setiap nilai yang selesai. Cara ini mungkin berhasil pada satu contoh di mana semuanya terpenuhi secara berurutan, namun akan gagal untuk Set, generator, input kosong, atau penyelesaian yang tidak berurutan. Jawaban yang lebih kuat dimulai dengan invarian: results[i] selalu menjadi milik nilai pada indeks iterasi i; pending adalah jumlah input yang belum terpenuhi; pemenuhan hanya diizinkan ketika pending mencapai nol.

Pewawancara juga akan memperhatikan apakah konsep "fail fast" tertukar dengan "batalkan sisanya". Begitu Promise luar menolak, upaya setelahnya tidak dapat mengubah statusnya, tetapi pekerjaan yang sudah dimulai akan tetap berjalan. Pembatalan memerlukan protokol tambahan yang didukung oleh operasi input; Promise agregat tidak dapat menciptakan kemampuan tersebut dengan sendirinya.

Pertanyaan untuk Diklarifikasi Sebelum Menjawab

  • Apakah inputnya selalu berupa array, atau iterable sinkron apa pun? Dukungan hanya array memungkinkan perulangan berbasis indeks. Mendukung kontrak iterable yang ditentukan memerlukan for...of dan penanganan error yang dilemparkan saat meminta nilai berikutnya.
  • Apakah plain value dan thenable merupakan input yang valid? Jika ya, setiap item harus melewati Promise.resolve. Memanggil item.then secara langsung akan gagal pada angka, string, dan objek biasa.
  • Apakah ini polyfill tingkat spesifikasi atau implementasi wawancara untuk perilaku inti? Versi tingkat spesifikasi juga menangani konstruktor this, subclass Promise, proteksi internal terhadap pemanggilan berulang, dan aturan penutupan iterator yang tepat. Solusi ini mengembalikan Promise bawaan dan tidak mengklaim kesesuaian penuh.
  • Haruskah operasi yang tersisa dibatalkan setelah terjadi kegagalan? Jika ya, instruksi soal harus mendefinisikan antarmuka AbortSignal atau pembatalan tugas. Kontrak saat ini hanya menolak Promise luar lebih awal; operasi lainnya tetap berjalan.
  • Apakah inputnya terbatas (finite)? Promise.all mengonsumsi inputnya secara sinkron. Penelusuran iterator tak terbatas tidak akan pernah selesai. Solusi ini mengasumsikan input terbatas sehingga kompleksitas ruang dapat dijelaskan dalam n.

Jawaban-jawaban ini mengubah perulangan, penanganan error, dan kontrak API, sehingga patut dikonfirmasi sebelum implementasi.

Kerangka Jawaban 30 Detik

"Saya akan mengembalikan Promise baru dan menelusuri iterable terbatas secara sinkron. Untuk setiap item, saya mencadangkan indeks iterasinya, menaikkan hitungan pending, dan menggunakan Promise.resolve untuk menormalisasi plain value, thenable, atau Promise. Handler pemenuhan menulis ke indeks yang dicadangkan dan me-resolve array hasil ketika hitungan mencapai nol. Handler penolakan akan menolak Promise luar dengan segera. Saya akan membungkus iterasi dalam try...catch sehingga error iterator juga ikut menolak. Input kosong tidak memiliki handler pemenuhan, jadi saya akan me-resolve [] secara eksplisit setelah penelusuran. Pendekatan ini menjaga urutan input dan bersifat fail-fast, tetapi tidak membatalkan operasi yang sudah terlanjur dimulai."

Pembahasan Mendalam Langkah demi Langkah

Mulailah dengan pendekatan dasar yang tampak menarik tetapi tidak setara: me-await setiap item secara berurutan dan mengumpulkan hasilnya. Cara ini mempertahankan urutan, tetapi membuat proses tunggu menjadi serial padahal seharusnya bisa tumpang tindih. Sebelum item sebelumnya selesai (settle), reaksi berikutnya bahkan belum didaftarkan. Operasi yang diminta mengagregasikan input yang sudah diperoleh; ini bukan antrean tugas serial.

Implementasi yang direkomendasikan memerlukan satu array hasil, dua penghitung, dan satu penelusuran:

javascript
function promiseAll(iterable) {
  return new Promise((resolve, reject) => {
    const results = [];
    let pending = 0;
    let index = 0;

    try {
      for (const item of iterable) {
        const currentIndex = index;
        index += 1;
        pending += 1;

        Promise.resolve(item).then(
          (value) => {
            results[currentIndex] = value;
            pending -= 1;

            if (pending === 0) {
              resolve(results);
            }
          },
          reject,
        );
      }
    } catch (error) {
      reject(error);
      return;
    }

    if (index === 0) {
      resolve([]);
    }
  });
}

currentIndex bersifat tetap selama iterasi tersebut. Misalkan tiga input selesai masing-masing setelah 30, 10, dan 20 milidetik. Handler pemenuhannya akan berjalan dalam urutan 1, 2, 0, tetapi mereka tetap menulis ke posisi 1, 2, 0, sehingga array akhir dikembalikan dalam urutan 0, 1, 2. Mengganti penetapan terindeks dengan results.push(value) akan secara keliru mengembalikan urutan berdasarkan waktu selesai.

Promise.resolve(item) mencakup dua batasan sekaligus. Plain value menjadi Promise yang sudah terpenuhi. Thenable diasimilasi metode then-nya; bahkan jika thenable yang rusak memanggil callback-nya lebih dari sekali, sifat status satu arah dari Promise bawaan mencegah penyelesaian berulang mencapai handler ini. Pengecekan dengan item instanceof Promise akan melewatkan Promise dari realm lain sekaligus thenable yang valid.

Input kosong memerlukan percabangan eksplisit. Penghitung dimulai dari nol, dan tidak ada handler pemenuhan yang tersedia untuk memanggil resolve. Memeriksa index === 0 setelah penelusuran akan memenuhi Promise yang dikembalikan dengan array kosong. .then yang didaftarkan oleh pemanggil tetap berjalan secara asinkron di bawah aturan normal Promise.

try...catch menangani kegagalan iterasi sinkron. Misalnya, sebuah generator dapat melempar error pada pemanggilan next() keduanya setelah sebuah handler terpasang pada nilai pertama. Blok catch akan menolak Promise luar; jika nilai sebelumnya kemudian terpenuhi, handlernya tidak dapat mengubah status yang sudah ditolak. Penolakan dari masing-masing item diarahkan ke reject yang sama. Jika beberapa input menolak, handler penolakan mana pun yang berjalan lebih dulu akan menentukan alasan penolakan luar.

Penelusuran dan penyelesaian melakukan total pekerjaan sebesar O(n). Array hasil dan reaksi pemenuhan per item membutuhkan ruang sebesar O(n). Menangani satu pemenuhan menambah pekerjaan sebesar O(1) untuk penulisan terindeks dan penurunan hitungan. Jika produk memerlukan setiap hasil pemenuhan dan penolakan, gunakan kontrak all-settled. Jika memerlukan batas konkurensi, terima task factory dan tambahkan scheduler; meneruskan koleksi Promise yang sudah berjalan ke fungsi ini tidak dapat membatasi konkurensi secara retroaktif.

Validasi harus mencakup lebih dari sekadar skenario normal (happy path). Ujilah iterable kosong; Set; campuran plain value, Promise, dan thenable; penyelesaian dengan urutan terbalik dari input; penolakan tercepat; dan iterator yang melempar error di tengah jalan. Setiap kasus menguji invarian tertentu, yang jauh lebih meyakinkan daripada sekadar membandingkan satu sampel array.

Contoh Jawaban Berkualitas Tinggi

"Saya akan membatasi cakupan ini pada iterable sinkron terbatas dan hasil Promise bawaan, tanpa mengklaim kepatuhan penuh terhadap subclass Promise. Tiga semantik harus terpenuhi: hasil mengikuti urutan iterasi, Promise luar menolak segera setelah ada input yang menolak, dan input kosong terpenuhi dengan array kosong.

Selama penelusuran, saya memberikan indeks stabil untuk setiap item dan menaikkan pending. Setiap item diproses melalui Promise.resolve, sehingga angka, Promise yang ada, dan thenable berbagi satu jalur yang sama. Saat terpenuhi, saya menulis ke posisi stabil tersebut dan menurunkan hitungan; pemenuhan terakhir akan me-resolve array yang lengkap. Handler penolakannya adalah reject luar. Saya menempatkan for...of di dalam try...catch karena pengambilan nilai iterable berikutnya dapat melempar error secara sinkron. Jika penelusuran tidak menemukan elemen, saya langsung me-resolve dengan [].

Penelusuran dan penyelesaian membutuhkan waktu kerja O(n) dan ruang O(n). Perilaku fail-fast hanya mengubah hasil agregat; ini tidak menghentikan operasi asinkron lainnya. Jika pembatalan diperlukan, saya akan menambahkan AbortSignal ke kontrak tugas input. Jika setiap error harus dikumpulkan, saya akan menggunakan semantik all-settled daripada mengubah aturan penolakan fungsi ini."

Kesalahan Umum

  • Mengumpulkan nilai yang terpenuhi dengan push urutan array mengikuti kecepatan penyelesaian, sehingga item pertama yang lambat bisa muncul di posisi terakhir → tangkap currentIndex selama penelusuran dan tetapkan results[currentIndex].
  • Hanya memanggil item.then(...) plain value tidak memiliki then, dan thenable non-bawaan tidak diasimilasi dengan aman → normalisasi setiap item dengan Promise.resolve(item).
  • Memulai hitungan pending dari panjang input sambil mengklaim mendukung iterable → Set dan generator tidak memiliki length yang dapat diandalkan, sehingga secara diam-diam mempersempit kontrak → tingkatkan pending seiring nilai-nilai ditelusuri.
  • Melupakan input kosong → tidak ada handler yang dapat memicu pemenuhan, membuat Promise yang dikembalikan berstatus pending selamanya → resolve array kosong ketika index === 0 setelah penelusuran.
  • Menggunakan forEach(async ...) lalu meng-await-nya → forEach tidak meng-await callback asinkron, sehingga alur kontrol dan propagasi error menjadi salah → daftarkan handler .then secara langsung dan lakukan agregasi dengan penghitung.
  • Hanya menangani penolakan input → error sinkron yang dilemparkan dari next() iterable luput dari penanganan → bungkus seluruh penelusuran dalam try...catch dan tolak Promise luar.
  • Mengklaim bahwa perilaku fail-fast membatalkan permintaan → status Promise yang tidak dapat diubah tidak menghentikan operasi di baliknya → nyatakan bahwa pembatalan memerlukan AbortSignal atau protokol pembatalan tingkat tugas lainnya.

Pertanyaan Lanjutan dan Tanggapan

Pertanyaan Lanjutan 1: Bagaimana Anda dapat membuktikan bahwa pengurutan hasil sudah benar?

Gunakan invarian ini: nilai pada indeks iterasi i hanya memiliki indeks i, dan handler pemenuhannya hanya menulis ke results[i]. Urutan eksekusi handler hanya mengubah kapan penulisan terjadi, bukan di mana ia ditulis. Oleh karena itu, setelah semua n input terpenuhi, posisi 0 hingga n - 1 akan berisi nilai dari input yang sesuai. Uji klaim ini dengan tiga Promise yang jeda waktunya berbanding terbalik dengan urutan inputnya, alih-alih mengandalkan jeda waktu yang sama.

Pertanyaan Lanjutan 2: Bagaimana cara membatalkan permintaan jaringan yang tersisa setelah terjadi penolakan?

Fungsi saat ini tidak dapat melakukannya, karena Promise tidak mengekspos titik masuk pembatalan untuk operasi di baliknya. Ubah input menjadi task factory yang menerima AbortSignal, dan buat satu AbortController bersama. Ketika suatu tugas gagal, panggil controller.abort() sebelum menolak Promise luar. Beberapa tugas mungkin sudah selesai, mengabaikan sinyal, atau gagal selama pembatalan, sehingga pembatalan permintaan merupakan kontrak tambahan dan bukan bagian tersembunyi dari semantik standar Promise.all.

Pertanyaan Lanjutan 3: Bagaimana jika paling banyak hanya tiga tugas yang boleh berjalan secara bersamaan?

Ubah input dari Promise yang sudah berjalan menjadi fungsi yang belum dijalankan. Lacak indeks tugas berikutnya, jumlah yang sedang berjalan, dan array hasil. Mulai tugas baru setiap kali ada yang selesai, sambil menjaga jumlah yang berjalan tetap pada atau di bawah 3. Jika API masih menerima array Promise, operasinya biasanya sudah dimulai saat array tersebut dibuat, sehingga scheduler sudah terlambat. Masalah utamanya adalah waktu mulai, bukan mengubah nama penghitung penyelesaian.

Pertanyaan Lanjutan 4: Apa yang masih membedakannya dari Promise.all tingkat spesifikasi?

Implementasi ini selalu menggunakan Promise bawaan. Solusi ini tidak mengambil konstruktor dari this, atau mereproduksi perilaku subclass Promise, internal slot, langkah-langkah penutupan iterator spesifikasi, dan setiap detail abrupt completion. Dalam wawancara, sebut ini sebagai implementasi perilaku inti. Polyfill yang siap dirilis memerlukan pemetaan langkah demi langkah ke algoritma ECMAScript beserta pengujian kompatibilitas; sekadar lolos contoh umum belum membuktikan kesesuaian spesifikasi.

Sumber publik

Pertanyaan terkait