Masalah dan Konteks yang Berkenaan
Laksanakan promiseAll(iterable) tanpa memanggil Promise.all natif di dalam penyelesaian anda. Input adalah iterable segerak terhingga yang mengandungi promise, thenable, atau nilai biasa. Output adalah Promise baharu.
Apabila setiap input dipenuhi (fulfill), hasilnya mestilah tatasusunan mengikut susunan lelaran (iteration order), tanpa mengira susunan penyelesaian. Apabila mana-mana input ditolak (reject), Promise luaran mesti menolak dengan sebab daripada penolakan pertama yang berlaku. Input kosong mesti menghasilkan []. Jika membaca daripada iterator itu sendiri melontarkan ralat (throw), Promise luaran juga mesti menolak.
Ini sesuai untuk pusingan pengekodan langsung bagi peranan frontend atau JavaScript. Penyelesaian ini boleh menggunakan Promise natif untuk mengasimilasikan thenable dan menjadualkan reaksi tak segerak (asynchronous reactions). Ia tidak perlu menghasilkan semula setiap butiran spesifikasi yang melibatkan pembina subkelas (subclass constructors), slot dalaman, dan penutupan iterator. Nyatakan sempadan tersebut sebelum mengekod: “tingkah laku teras” dan “pelaksanaan yang serasi sepenuhnya dengan spesifikasi” adalah tugasan yang berbeza.
Perkara yang Dinilai oleh Penemu Duga
Jawapan yang kukuh menyenaraikan semantik sebelum menulis gelung (loop). Lima isyarat berguna adalah menerima iterable dan bukan hanya tatasusunan, menormalkan nilai biasa dan thenable melalui Promise.resolve, memperuntukkan indeks untuk mengekalkan susunan, mengendalikan input kosong secara berasingan, dan menghalakan ralat lelaran serta penolakan promise kepada penolakan luaran.
Jawapan biasa memanggil map pada tatasusunan dan push setiap nilai yang selesai. Ia mungkin lulus satu contoh yang mana semuanya dipenuhi mengikut susunan, tetapi gagal untuk Set, generator, input kosong, atau penyelesaian di luar susunan. Jawapan yang lebih kukuh bermula dengan varian tak berubah (invariants): results[i] sentiasa milik nilai pada indeks lelaran i; pending ialah bilangan input yang belum dipenuhi; pemenuhan hanya dibenarkan apabila pending mencapai sifar.
Penemu duga juga akan menyedari sama ada “fail fast” dikelirukan dengan “membatalkan selebihnya”. Sebaik sahaja Promise luaran menolak, cubaan kemudian tidak boleh mengubah keadaannya, tetapi kerja yang telah dimulakan akan diteruskan. Pembatalan memerlukan protokol tambahan yang disokong oleh operasi input; Promise agregat tidak boleh mereka cipta keupayaan tersebut.
Soalan untuk Dijelaskan Sebelum Menjawab
- Adakah input sentiasa tatasusunan, atau sebarang iterable segerak? Sokongan tatasusunan sahaja membolehkan gelung indeks. Menyokong kontrak iterable yang dinyatakan memerlukan
for...ofdan mengendalikan ralat yang dilontarkan semasa meminta nilai seterusnya. - Adakah nilai biasa dan thenable merupakan input yang sah? Jika ya, setiap item mesti melalui
Promise.resolve. Memanggilitem.thensecara langsung akan gagal pada nombor, rentetan, dan objek biasa. - Adakah ini polyfill peringkat spesifikasi atau pelaksanaan temu duga bagi tingkah laku teras? Versi peringkat spesifikasi juga menangani pembina
this, subkelas Promise, kawalan dalaman terhadap panggilan berulang, dan peraturan penutupan iterator yang tepat. Penyelesaian ini mengembalikan Promise natif dan tidak menuntut pematuhan penuh. - Adakah operasi selebihnya mesti dibatalkan selepas kegagalan? Jika ya, gesaan mesti mentakrifkan AbortSignal atau antara muka pembatalan tugas. Kontrak semasa hanya menolak Promise luaran lebih awal; operasi lain terus berjalan.
- Adakah input itu terhingga?
Promise.allmenggunakan inputnya secara segerak. Penjelajahan iterator tak terhingga tidak akan selesai. Penyelesaian ini mengandaikan input terhingga supaya ruang boleh diterangkan dalam sebutann.
Jawapan ini mengubah gelung, pengendalian ralat, dan kontrak API, jadi ia wajar disahkan sebelum pelaksanaan.
Rangka Kerja Jawapan 30 Saat
“Saya akan mengembalikan Promise baharu dan menjelajah iterable terhingga secara segerak. Untuk setiap item, saya memperuntukkan indeks lelarannya, meningkatkan kiraan tergantung (pending count), dan menggunakan Promise.resolve untuk menormalkan nilai biasa, thenable, atau Promise. Pengendali pemenuhan menulis ke indeks yang diperuntukkan dan menyelesaikan tatasusunan hasil apabila kiraan mencapai sifar. Pengendali penolakan menolak Promise luaran serta-merta. Saya akan membalut lelaran dalam try...catch supaya ralat iterator juga menolak. Input kosong tidak mempunyai pengendali pemenuhan, jadi saya akan menyelesaikan [] secara eksplisit selepas penjelajahan. Ini mengekalkan susunan input dan fail-fast, tetapi ia tidak membatalkan operasi yang telah pun dimulakan.”
Pendalaman Langkah demi Langkah
Mulakan dengan garis dasar yang menggoda tetapi tidak setara: await setiap item secara berurutan dan kumpulkan hasilnya. Ia mengekalkan susunan, tetapi mensirikan penantian yang sebaliknya boleh bertindih. Sehingga item sebelumnya diselesaikan, reaksi seterusnya tidak didaftarkan pun. Operasi yang diminta mengagregatkan input yang telah diperoleh; ia bukan barisan tugas bersiri.
Pelaksanaan yang disyorkan memerlukan satu tatasusunan hasil, dua pembilang, dan satu penjelajahan:
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 ditetapkan semasa lelaran tersebut. Andaikan tiga input selesai selepas 30, 10, dan 20 milisaat. Pengendali pemenuhan mereka berjalan mengikut urutan 1, 2, 0, tetapi mereka masih menulis ke kedudukan 1, 2, 0, jadi tatasusunan akhir dikembalikan dalam susunan 0, 1, 2. Menggantikan penugasan berindeks dengan results.push(value) akan mengembalikan susunan penyiapan secara salah.
Promise.resolve(item) merangkumi dua sempadan sekali gus. Nilai biasa menjadi Promise yang telah pun dipenuhi. Thenable mempunyai kaedah then yang diasimilasikan; walaupun thenable yang rosak memanggil panggil baliknya (callbacks) lebih daripada sekali, keadaan sehala Promise natif menghalang penyelesaian berulang daripada mencapai pengendali ini. Ujian item instanceof Promise akan terlepas kedua-dua Promise daripada alam (realm) lain dan thenable yang sah.
Input kosong memerlukan cawangan eksplisit. Pembilang bermula pada sifar, dan tiada pengendali pemenuhan wujud untuk memanggil resolve. Menyemak index === 0 selepas penjelajahan memenuhi Promise yang dikembalikan dengan tatasusunan kosong. .then yang didaftarkan oleh pemanggil masih berjalan secara tak segerak di bawah peraturan Promise biasa.
try...catch mengendalikan kegagalan lelaran segerak. Sebagai contoh, generator mungkin melontarkan ralat pada panggilan next() yang kedua selepas pengendali telah dilampirkan pada nilai pertama. Blok catch menolak Promise luaran; jika nilai terdahulu dipenuhi kemudian, pengendalinya tidak boleh mengubah keadaan yang ditolak. Penolakan daripada item individu dihalakan ke reject yang sama. Jika beberapa input menolak, mana-mana pengendali penolakan yang berjalan terlebih dahulu menentukan sebab luaran.
Penjelajahan dan penyelesaian melakukan kerja sebanyak O(n) secara keseluruhan. Tatasusunan hasil dan reaksi pemenuhan bagi setiap item memerlukan ruang O(n). Mengendalikan satu pemenuhan menambah kerja O(1) untuk penulisan berindeks dan pengurangan (decrement). Jika produk memerlukan setiap pemenuhan dan penolakan, gunakan kontrak all-settled. Jika ia memerlukan had keserentakan (concurrency limit), terima kilang tugas (task factories) dan tambah penjadual; menghantar koleksi Promise yang telah pun dimulakan kepada fungsi ini tidak boleh mengehadkan keserentakan secara retroaktif.
Pengesahan harus melangkaui laluan gembira (happy path). Rangkumi iterable kosong; Set; campuran nilai biasa, Promise, dan thenable; penyiapan dalam susunan input terbalik; penolakan paling awal; dan iterator yang melontarkan ralat separuh jalan. Setiap kes menguji varian tak berubah yang khusus, yang lebih meyakinkan daripada membandingkan satu tatasusunan sampel.
Jawapan Sampel Berkualiti Tinggi
“Saya akan menghadkan skop ini kepada iterable segerak terhingga dan hasil Promise natif, tanpa mendakwa pematuhan subkelas Promise sepenuhnya. Tiga semantik mesti dipegang: hasil mengikut susunan lelaran, Promise luaran menolak sebaik sahaja input menolak, dan input kosong dipenuhi dengan tatasusunan kosong.
Semasa penjelajahan, saya memberikan setiap item indeks yang stabil dan meningkatkan pending. Setiap item melalui Promise.resolve, jadi nombor, Promise sedia ada, dan thenable berkongsi satu laluan. Apabila dipenuhi, saya menulis ke kedudukan yang stabil dan mengurangkan kiraan; pemenuhan terakhir menyelesaikan tatasusunan yang lengkap. Pengendali penolakan ialah reject luaran. Saya meletakkan for...of di dalam try...catch kerana mendapatkan nilai iterable seterusnya mungkin melontarkan ralat secara segerak. Jika penjelajahan mendapati tiada elemen, saya selesaikan dengan [] secara terus.
Penjelajahan dan penyelesaian mengambil kerja O(n) dan ruang O(n). Tingkah laku fail-fast hanya mengubah hasil agregat; ia tidak menghentikan operasi tak segerak yang lain. Jika pembatalan diperlukan, saya akan menambah AbortSignal pada kontrak tugas input. Jika setiap ralat mesti dikumpulkan, saya akan menggunakan semantik all-settled dan bukannya mengubah peraturan penolakan fungsi ini.”
Kesilapan Biasa
- Mengumpul nilai yang dipenuhi dengan
push→ susunan tatasusunan mengikut kelajuan penyiapan, jadi item pertama yang perlahan boleh muncul terakhir → tangkapcurrentIndexsemasa penjelajahan dan tetapkanresults[currentIndex]. - Hanya memanggil
item.then(...)→ nilai biasa tidak mempunyaithen, dan thenable bukan natif tidak diasimilasikan dengan selamat → normalkan setiap item denganPromise.resolve(item). - Memulakan kiraan tergantung daripada panjang input sambil mendakwa sokongan iterable → Set dan generator tidak mempunyai
lengthyang boleh dipercayai, mengecilkan kontrak secara senyap → tingkatkanpendingsemasa nilai dijelajah. - Melupakan input kosong → tiada pengendali yang boleh mencetuskan pemenuhan, meninggalkan Promise yang dikembalikan tergantung selama-lamanya → selesaikan tatasusunan kosong apabila
index === 0selepas penjelajahan. - Menggunakan
forEach(async ...)dan kemudian melalukan await padanya →forEachtidak menunggu panggil balik tak segerak, jadi aliran kawalan dan penyebaran ralat menjadi salah → daftarkan pengendali.thensecara langsung dan agregatkan dengan pembilang. - Hanya mengendalikan penolakan input → lontaran ralat segerak daripada
next()milik iterable terlepas daripada penaakulan → balut keseluruhan penjelajahan dalamtry...catchdan tolak Promise luaran. - Mendakwa tingkah laku fail-fast membatalkan permintaan → keadaan Promise yang tidak boleh diubah tidak menghentikan operasi asas → nyatakan bahawa pembatalan memerlukan AbortSignal atau protokol pembatalan peringkat tugas yang lain.
Soalan Susulan dan Maklum Balas
Susulan 1: Bagaimanakah anda boleh membuktikan bahawa susunan hasil adalah betul?
Gunakan varian tak berubah ini: nilai pada indeks lelaran i hanya memiliki indeks i, dan pengendali pemenuhannya hanya menulis ke results[i]. Susunan pengendali mengubah masa penulisan berlaku, bukan lokasi ia berlaku. Selepas semua input n dipenuhi, kedudukan 0 hingga n - 1 mengandungi nilai input yang sepadan. Uji dakwaan tersebut dengan tiga Promise yang kelewatannya adalah terbalik daripada susunan inputnya dan bukannya bergantung pada kelewatan yang sama.
Susulan 2: Bagaimanakah anda akan membatalkan permintaan rangkaian yang selebihnya selepas penolakan?
Fungsi semasa tidak dapat berbuat demikian, kerana Promise tidak mendedahkan titik masuk pembatalan operasi asas. Ubah input kepada kilang tugas yang menerima AbortSignal, dan cipta satu AbortController yang dikongsi. Apabila sesuatu tugas gagal, panggil controller.abort() sebelum menolak Promise luaran. Sesetengah tugas mungkin telah pun selesai, mengabaikan isyarat, atau gagal semasa pembatalan, jadi pembatalan permintaan adalah kontrak lanjutan dan bukannya sebahagian tersembunyi daripada semantik Promise.all standard.
Susulan 3: Bagaimana jika paling banyak tiga tugas boleh dijalankan pada satu-satu masa?
Ubah input daripada Promise yang telah dimulakan kepada fungsi yang belum dimulakan. Jejak indeks tugas seterusnya, bilangan yang sedang berjalan, dan tatasusunan hasil. Mulakan tugas lain setiap kali satu tugas selesai, sambil mengekalkan kiraan berjalan pada atau di bawah 3. Jika API masih menerima tatasusunan Promise, operasinya biasanya bermula semasa tatasusunan itu dibina, jadi penjadual sudah terlambat. Isu utamanya ialah masa mula, bukan menamakan semula pembilang penyiapan.
Susulan 4: Apakah yang masih berbeza daripada Promise.all peringkat spesifikasi?
Pelaksanaan ini sentiasa menggunakan Promise natif. Ia tidak memperoleh pembina daripada this, atau menghasilkan semula tingkah laku subkelas Promise, slot dalaman, langkah penutupan iterator spesifikasi, dan setiap butiran penyiapan mendadak (abrupt-completion). Dalam temu duga, sebutkannya sebagai pelaksanaan tingkah laku teras. Polyfill yang boleh diterbitkan memerlukan pemetaan langkah demi langkah ke algoritma ECMAScript serta ujian keserasian; contoh biasa sahaja tidak membuktikan pematuhan.