Sebelum mempelajari isi artikel ini, kita harus memahami terlebih dahulu konsep asynchronous. Hal pertama yang perlu ditekankan adalah ada perbedaan mendasar antara asynchronous dan parallel .
Paralelisme umumnya mengacu pada komputasi paralel, yang berarti bahwa beberapa instruksi dieksekusi pada waktu yang sama. Instruksi ini dapat dieksekusi pada beberapa inti dari CPU
yang sama, atau pada beberapa CPU
, atau pada beberapa host fisik atau bahkan beberapa jaringan.
Sinkronisasi umumnya mengacu pada pelaksanaan tugas dalam urutan yang telah ditentukan. Hanya ketika tugas sebelumnya selesai, tugas berikutnya akan dijalankan.
Asynchronous, berhubungan dengan sinkronisasi, berarti CPU
mengesampingkan sementara tugas saat ini, memproses tugas berikutnya terlebih dahulu, dan kemudian kembali ke tugas sebelumnya untuk melanjutkan eksekusi setelah menerima pemberitahuan panggilan balik dari tugas sebelumnya benang kedua.
Mungkin lebih intuitif untuk menjelaskan paralelisme, sinkronisasi, dan asinkron dalam bentuk gambar. Asumsikan ada dua tugas A dan B yang perlu diproses. Metode pemrosesan paralel, sinkron, dan asinkron akan mengadopsi metode eksekusi seperti yang ditunjukkan pada gambar berikut:
JavaScript
memberi kita banyak fungsi asinkron. Fungsi ini memungkinkan kita menjalankan tugas asinkron dengan mudah. Artinya, kita mulai menjalankan tugas (fungsi) sekarang, tetapi tugas tersebut akan diselesaikan nanti, dan waktu penyelesaiannya spesifik. tidak Tidak yakin.
Misalnya, fungsi setTimeout
adalah fungsi asinkron yang sangat umum. Selain itu, fs.readFile
dan fs.writeFile
juga merupakan fungsi asinkron.
Kita dapat mendefinisikan sendiri kasus tugas asinkron, seperti mengkustomisasi fungsi penyalinan file copyFile(from,to)
:
const fs = require('fs')function copyFile(from, to) { fs.readFile(dari, (err, data) => { jika (salah) { console.log(err.pesan) kembali } fs.writeFile(ke, data, (err) => { jika (salah) { console.log(err.pesan) kembali } console.log('Penyalinan selesai') }) })}
Fungsi copyFile
terlebih dahulu membaca data file dari parameter from
, dan kemudian menulis data ke file yang ditunjuk oleh parameter to
.
Kita dapat memanggil copyFile
seperti ini:
copyFile('./from.txt','./to.txt')//Copy file
Jika ada kode lain setelah copyFile(...)
saat ini, program tidak akan tunggu Eksekusi copyFile
berakhir, tetapi dijalankan langsung ke bawah. Program tidak peduli kapan tugas penyalinan file berakhir.
copyFile('./from.txt','./to.txt')//Kode berikut tidak akan menunggu hingga eksekusi kode di atas berakhir...
Pada titik ini, semuanya tampak normal, tetapi jika we Apa yang terjadi jika Anda langsung mengakses konten file ./to.txt
setelah fungsi copyFile(...)
?
Ini tidak akan membaca konten yang disalin, seperti ini:
copyFile('./from.txt','./to.txt')fs.readFile('./to.txt',(err,data)= >{ ...})
Jika file ./to.txt
belum dibuat sebelum menjalankan program, Anda akan mendapatkan error berikut:
PS E:CodeNodedemos 3-callback> node .index.js selesai Salinan selesai PS E:CodeNodedemos 3-callback> simpul .index.js Kesalahan: ENOENT: tidak ada file atau direktori seperti itu, buka 'E:CodeNodedemos 3-callbackto.txt'Salinan selesai
Meskipun ./to.txt
ada, konten yang disalin tidak dapat dibaca.
Alasan untuk fenomena ini adalah: copyFile(...)
dijalankan secara asinkron. Setelah program menjalankan fungsi copyFile(...)
, ia tidak menunggu hingga penyalinan selesai, tetapi menjalankannya langsung ke bawah, menyebabkan file tersebut. muncul kesalahan ./to.txt
tidak ada, atau kesalahan isi file kosong (jika file dibuat terlebih dahulu).
Waktu akhir eksekusi spesifik dari fungsi panggilan balik fungsi asinkron tidak dapat ditentukan. Misalnya, waktu akhir eksekusi fungsi readFile(from,to)
kemungkinan besar bergantung pada ukuran file from
.
Jadi, pertanyaannya adalah bagaimana kita dapat secara akurat menemukan akhir eksekusi copyFile
dan membaca konten file to
?
Hal ini memerlukan penggunaan fungsi callback. Kita dapat memodifikasi fungsi copyFile
sebagai berikut:
function copyFile(from, to, callback) { fs.readFile(dari, (err, data) => { jika (salah) { console.log(err.pesan) kembali } fs.writeFile(ke, data, (err) => { jika (salah) { console.log(err.pesan) kembali } console.log('Penyalinan selesai') callback()//Fungsi panggilan balik dipanggil ketika operasi penyalinan selesai}) })}
Dengan cara ini, jika kita perlu melakukan beberapa operasi segera setelah penyalinan file selesai, kita dapat menulis operasi ini ke dalam fungsi callback:
function copyFile(from, to, callback) { fs.readFile(dari, (err, data) => { jika (salah) { console.log(err.pesan) kembali } fs.writeFile(ke, data, (err) => { jika (salah) { console.log(err.pesan) kembali } console.log('Penyalinan selesai') callback()//Fungsi panggilan balik dipanggil ketika operasi penyalinan selesai}) })}copyFile('./from.txt', './to.txt', function () { //Masukkan fungsi callback, baca isi file "to.txt" dan output fs.readFile('./to.txt', (err, data) => { jika (salah) { console.log(err.pesan) kembali } konsol.log(data.toString()) })})
Jika sudah menyiapkan file ./from.txt
, maka kode diatas bisa langsung dijalankan:
PS E:CodeNodedemos 3-callback> node .index.js Salinan selesai Bergabunglah dengan komunitas "Xianzong" dan kembangkan keabadian bersama saya. Alamat komunitas: http://t.csdn.cn/EKf1h
Metode pemrograman ini disebut gaya pemrograman asinkron "berbasis panggilan balik". Fungsi yang dijalankan secara asinkron harus menyediakan Parameter panggilan balik digunakan untuk menelepon setelah tugas berakhir.
Gaya ini umum dalam pemrograman JavaScript
. Misalnya, fungsi pembacaan file fs.readFile
dan fs.writeFile
semuanya merupakan fungsi asinkron.
fungsi panggilan balik dapat secara akurat menangani masalah berikutnya setelah pekerjaan asinkron selesai. Jika kita perlu melakukan beberapa operasi asinkron secara berurutan, kita perlu menyarangkan fungsi panggilan balik.
Skenario kasus:
Implementasi kode untuk membaca file A dan file B secara berurutan:
fs.readFile('./A.txt', (err, data) => { jika (salah) { console.log(err.pesan) kembali } console.log('Baca file A: ' + data.toString()) fs.readFile('./B.txt', (err, data) => { jika (salah) { console.log(err.pesan) kembali } console.log("Baca file B: " + data.toString()) })})
Efek eksekusi:
PS E:CodeNodedemos 3-callback> node .index.jsMembaca file A: Sekte Abadi sangat bagus, tetapi ada yang hilang. Membaca file B
:
Jika Anda ingin bergabung dengan Sekte Abadi, Anda harus memiliki tautan http://t.csdn.cn/H1faI
file setelah A, file B segera dibaca.
Bagaimana jika kita ingin melanjutkan membaca file C setelah file B? Hal ini memerlukan callback yang terus disarangkan:
fs.readFile('./A.txt', (err, data) => {//Panggilan balik pertama if (err) { console.log(err.pesan) kembali } console.log('Baca file A: ' + data.toString()) fs.readFile('./B.txt', (err, data) => {//Panggilan balik kedua if (err) { console.log(err.pesan) kembali } console.log("Baca file B: " + data.toString()) fs.readFile('./C.txt',(err,data)=>{//Panggilan balik ketiga... }) })})
Dengan kata lain, jika kita ingin melakukan beberapa operasi asinkron secara berurutan, kita memerlukan beberapa level callback yang disarangkan. Hal ini efektif jika jumlah levelnya sedikit, namun jika ada terlalu banyak waktu yang bersarang, beberapa masalah akan muncul terjadi.
Konvensi Panggilan Balik
Faktanya, gaya fungsi panggilan balik di fs.readFile
bukanlah pengecualian, namun merupakan konvensi umum dalam JavaScript
. Kami akan menyesuaikan sejumlah besar fungsi panggilan balik di masa mendatang, dan kami harus mematuhi konvensi ini dan membentuk kebiasaan pengkodean yang baik.
Konvensinya adalah:
callback
pertama dicadangkan untuk kesalahan. Setelah terjadi kesalahan, callback(err)
akan dipanggil.callback(null, result1, result2,...)
akan dipanggil.Berdasarkan konvensi di atas, fungsi panggilan balik memiliki dua fungsi: penanganan kesalahan dan penerimaan hasil. Misalnya, fungsi panggilan balik fs.readFile('...',(err,data)=>{})
mengikuti konvensi ini.
Jika kita tidak menggali lebih dalam, metode pemrosesan asynchronous berdasarkan callback sepertinya merupakan cara yang cukup sempurna untuk menanganinya. Masalahnya adalah jika kita memiliki perilaku asinkron satu demi satu, kodenya akan terlihat seperti ini:
fs.readFile('./a.txt',(err,data)=>{ jika(salah){ console.log(err.pesan) kembali } //Baca hasil operasi fs.readFile('./b.txt',(err,data)=>{ jika(salah){ console.log(err.pesan) kembali } //Baca hasil operasi fs.readFile('./c.txt',(err,data)=>{ jika(salah){ console.log(err.pesan) kembali } //Baca hasil operasi fs.readFile('./d.txt',(err,data)=>{ jika(salah){ console.log(err.pesan) kembali } ... }) }) })})
Isi eksekusi kode diatas adalah:
Seiring dengan meningkatnya jumlah panggilan, tingkat penyarangan kode menjadi semakin dalam, termasuk semakin banyak pernyataan kondisional, mengakibatkan kode membingungkan yang terus-menerus menjorok ke kanan, sehingga sulit untuk dibaca dan menjaga.
Kami menyebut fenomena pertumbuhan terus menerus ke kanan (lekukan ke kanan) ini sebagai " callback hell " atau " pyramid of doom "!
fs.readFile('a.txt',(err,data)=>{ fs.readFile('b.txt',(err,data)=>{ fs.readFile('c.txt',(err,data)=>{ fs.readFile('d.txt',(err,data)=>{ fs.readFile('e.txt',(err,data)=>{ fs.readFile('f.txt',(err,data)=>{ fs.readFile('g.txt',(err,data)=>{ fs.readFile('h.txt',(err,data)=>{ ... /* Gerbang ke Neraka ===> */ }) }) }) }) }) }) })})
Meskipun kode di atas terlihat cukup biasa, ini hanyalah situasi ideal misalnya. Biasanya terdapat sejumlah besar pernyataan kondisional, operasi pemrosesan data, dan kode lain dalam logika bisnis, yang mengganggu tatanan indah saat ini dan membuat perubahan kode.
Untungnya, JavaScript
memberi kita banyak solusi, dan Promise
adalah solusi terbaik.