Manual Node.js, Bagian 7: Pemrograman Asinkron

Hari ini, dalam terjemahan bagian ketujuh dari manual Node.js, kita akan berbicara tentang pemrograman asinkron, mempertimbangkan masalah-masalah seperti penggunaan panggilan balik, janji-janji dan konstruk async / menunggu, dan membahas cara bekerja dengan acara.




Sinkronisasi dalam bahasa pemrograman


JavaScript sendiri adalah bahasa pemrograman sinkron dan berulir tunggal. Ini berarti Anda tidak dapat membuat utas baru dalam kode yang berjalan secara paralel. Namun, komputer pada dasarnya asinkron. Artinya, tindakan tertentu dapat dilakukan terlepas dari aliran eksekusi program utama. Di komputer modern, setiap program dialokasikan sejumlah waktu prosesor tertentu, ketika waktu ini habis, sistem memberikan sumber daya ke program lain, juga untuk sementara waktu. Pergantian tersebut dilakukan secara siklis, dilakukan dengan sangat cepat sehingga seseorang tidak dapat melihatnya, karena itu, kami berpikir bahwa komputer kami menjalankan banyak program secara bersamaan. Tapi ini ilusi (belum lagi mesin multiprosesor).

Dalam isi program, interupsi digunakan - sinyal dikirimkan ke prosesor dan memungkinkan untuk menarik perhatian sistem. Kami tidak akan masuk ke perincian, yang paling penting adalah untuk mengingat bahwa perilaku asinkron ketika sebuah program dijeda hingga membutuhkan sumber daya prosesor sepenuhnya normal. Pada saat program tidak memuat sistem dengan pekerjaan, komputer dapat memecahkan masalah lain. Misalnya, dengan pendekatan ini, ketika suatu program menunggu respons terhadap permintaan jaringan yang dibuat untuknya, ia tidak memblokir prosesor hingga respons diterima.

Sebagai aturan, bahasa pemrograman asinkron, beberapa dari mereka memberi programmer kemampuan untuk mengontrol mekanisme asinkron, baik menggunakan alat bahasa bawaan atau perpustakaan khusus. Kita berbicara tentang bahasa seperti C, Java, C #, PHP, Go, Ruby, Swift, Python. Beberapa di antaranya memungkinkan Anda memprogram dalam gaya asinkron, menggunakan utas, memulai proses baru.

Asynchrony JavaScript


Seperti yang telah disebutkan, JavaScript adalah bahasa sinkron utas tunggal. Garis-garis kode yang ditulis dalam JS dieksekusi dalam urutan di mana mereka muncul dalam teks, satu demi satu. Misalnya, di sini adalah program JS yang sangat normal yang menunjukkan perilaku ini:

const a = 1 const b = 2 const c = a * b console.log(c) doSomething() 

Tetapi JavaScript dibuat untuk digunakan di browser. Tugas utamanya, pada awalnya, adalah mengatur pemrosesan acara yang terkait dengan aktivitas pengguna. Misalnya, ini adalah acara seperti onClick , onMouseOver , onChange , onSubmit , dan sebagainya. Bagaimana mengatasi masalah seperti itu dalam kerangka model pemrograman sinkron?

Jawabannya terletak pada lingkungan di mana JavaScript berjalan. Yaitu, browser memungkinkan Anda untuk secara efektif menyelesaikan masalah seperti itu, memberikan programmer API yang sesuai.

Di lingkungan Node.js ada alat untuk melakukan I / O non-blocking, seperti bekerja dengan file, mengatur pertukaran data melalui jaringan, dan sebagainya.

Telepon balik


Jika kita berbicara tentang JavaScript berbasis browser, dapat dicatat bahwa tidak mungkin untuk mengetahui terlebih dahulu ketika pengguna mengklik tombol. Untuk memastikan bahwa sistem merespons peristiwa semacam itu, pawang dibuat untuk itu.

Pengatur kejadian menerima fungsi yang akan dipanggil ketika acara tersebut terjadi. Ini terlihat seperti ini:

 document.getElementById('button').addEventListener('click', () => { //    }) 

Fungsi seperti ini juga disebut fungsi callback atau panggilan balik.

Panggilan balik adalah fungsi biasa yang diteruskan sebagai nilai ke fungsi lain. Ini akan dipanggil hanya ketika suatu peristiwa tertentu terjadi. JavaScript mengimplementasikan konsep fungsi kelas satu. Fungsi semacam itu dapat ditugaskan ke variabel dan diteruskan ke fungsi lain (disebut fungsi tingkat tinggi).

Pendekatan pengembangan JavaScript sisi klien tersebar luas ketika semua kode klien dibungkus dengan pendengar peristiwa load objek window , yang memanggil panggilan balik yang diteruskan setelah halaman siap untuk bekerja:

 window.addEventListener('load', () => { //  //     }) 

Callback digunakan di mana-mana, dan tidak hanya untuk menangani acara DOM. Misalnya, kami telah bertemu dengan penggunaannya dalam timer:

 setTimeout(() => { //   2  }, 2000) 

Permintaan XHR juga menggunakan callback. Dalam hal ini, sepertinya menetapkan fungsi ke properti yang sesuai. Fungsi serupa akan dipanggil ketika peristiwa tertentu terjadi. Dalam contoh berikut, peristiwa semacam itu adalah perubahan status permintaan:

 const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) {   xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send() 

▍ Kesalahan dalam menangani panggilan balik


Mari kita bicara tentang cara menangani kesalahan dalam panggilan balik. Ada satu strategi umum untuk menangani kesalahan seperti itu, yang juga digunakan di Node.js. Terdiri dari fakta bahwa parameter pertama dari setiap fungsi panggilan balik adalah objek kesalahan. Jika tidak ada kesalahan, null akan ditulis ke parameter ini. Jika tidak, akan ada objek kesalahan yang berisi uraiannya dan informasi tambahan tentangnya. Begini tampilannya:

 fs.readFile('/file.json', (err, data) => { if (err !== null) {   //    console.log(err)   return } // ,   console.log(data) }) 

▍ Masalah panggilan balik


Panggilan balik mudah digunakan dalam situasi sederhana. Namun, setiap panggilan balik adalah level tambahan kode yang bersarang. Jika beberapa panggilan balik bersarang digunakan, ini dengan cepat menyebabkan komplikasi yang signifikan dari struktur kode:

 window.addEventListener('load', () => { document.getElementById('button').addEventListener('click', () => {   setTimeout(() => {     items.forEach(item => {       //,  -      })   }, 2000) }) }) 

Dalam contoh ini, hanya 4 level kode yang ditampilkan, tetapi dalam praktiknya seseorang dapat menemukan sejumlah besar level, biasanya disebut "panggilan balik neraka". Anda dapat mengatasi masalah ini menggunakan konstruksi bahasa lain.

Janji dan async / menunggu


Dimulai dengan standar ES6, JavaScript memperkenalkan fitur-fitur baru yang membuatnya lebih mudah untuk menulis kode asinkron, menghilangkan kebutuhan akan callback. Kita berbicara tentang janji-janji yang muncul di ES6, dan konstruk async / menunggu yang muncul di ES8.

▍ Janji


Janji (objek janji) adalah salah satu cara untuk bekerja dengan konstruksi perangkat lunak asinkron dalam JavaScript, yang, secara umum, mengurangi penggunaan panggilan balik.

Kenalan dengan Janji


Janji-janji biasanya didefinisikan sebagai objek proxy untuk nilai-nilai tertentu, yang penampilannya diharapkan di masa depan. Janji juga disebut "janji" atau "hasil yang dijanjikan." Meskipun konsep ini telah ada selama bertahun-tahun, janji-janji dibakukan dan ditambahkan ke bahasa hanya di ES2015. Dalam ES2017, desain async / menunggu, yang didasarkan pada janji, dan yang dapat dianggap sebagai pengganti yang mudah digunakan, telah muncul. Oleh karena itu, bahkan jika Anda tidak berencana untuk menggunakan janji-janji biasa, pemahaman tentang bagaimana mereka bekerja adalah penting untuk penggunaan yang efektif dari async / menunggu konstruksi

Cara kerja janji


Setelah janji dipanggil, itu masuk ke status tertunda. Ini berarti bahwa fungsi yang menyebabkan janji terus dieksekusi, sementara beberapa perhitungan dilakukan dalam janji, setelah itu janji menginformasikan tentang hal itu. Jika operasi yang dilakukan oleh janji selesai dengan sukses, maka janji tersebut ditransfer ke negara yang dipenuhi. Janji semacam itu dikatakan berhasil diselesaikan. Jika operasi selesai dengan kesalahan, janji ditempatkan di negara yang ditolak.

Mari kita bicara tentang bekerja dengan janji.

Buat Janji


API untuk bekerja dengan janji memberi kita konstruktor yang sesuai, yang dipanggil oleh perintah dari bentuk new Promise() . Begini caranya janji dibuat:

 let done = true const isItDoneYet = new Promise( (resolve, reject) => {   if (done) {     const workDone = 'Here is the thing I built'     resolve(workDone)   } else {     const why = 'Still working on something else'     reject(why)   } } ) 

Promis memeriksa konstanta global yang done , dan jika nilainya true , ia berhasil diselesaikan. Kalau tidak, janji itu ditolak. Menggunakan parameter resolve dan reject , yang merupakan fungsi, kita dapat mengembalikan nilai dari janji. Dalam hal ini, kami mengembalikan string, tetapi di sini objek dapat digunakan.

Bekerja dengan Janji


Kami menciptakan janji di atas, sekarang pertimbangkan untuk bekerja dengannya. Ini terlihat seperti ini:

 const isItDoneYet = new Promise( //... ) const checkIfItsDone = () => { isItDoneYet   .then((ok) => {     console.log(ok)   })   .catch((err) => {     console.error(err)   }) } checkIfItsDone() 

Memanggil checkIfItsDone() akan mengarah pada pelaksanaan isItDoneYet() isItDoneYet isItDoneYet() dan organisasi yang menunggu penyelesaiannya. Jika janji berhasil diselesaikan, panggilan balik yang dilewati ke metode .then() akan berfungsi. Jika kesalahan terjadi, yaitu, janji itu akan ditolak, itu bisa diproses dalam fungsi yang diteruskan ke metode .catch() .

Janji-janji Rantai


Metode janji mengembalikan janji, yang memungkinkan Anda untuk menggabungkannya menjadi rantai. Contoh yang baik dari perilaku ini adalah Ambil API berbasis browser, yang merupakan lapisan abstraksi atas XMLHttpRequest . Ada paket npm yang cukup populer untuk Node.js yang mengimplementasikan Fetch API, yang akan kita bahas nanti. API ini dapat digunakan untuk memuat sumber daya jaringan tertentu dan, berkat kemungkinan menggabungkan janji dalam rantai, untuk mengatur pemrosesan selanjutnya dari data yang diunduh. Bahkan, saat Anda memanggil API Ambil melalui panggilan ke fungsi fetch() , sebuah janji dibuat.

Pertimbangkan contoh janji perantaraan berikut:

 const fetch = require('node-fetch') const status = (response) => { if (response.status >= 200 && response.status < 300) {   return Promise.resolve(response) } return Promise.reject(new Error(response.statusText)) } const json = (response) => response.json() fetch('https://jsonplaceholder.typicode.com/todos') .then(status) .then(json) .then((data) => { console.log('Request succeeded with JSON response', data) }) .catch((error) => { console.log('Request failed', error) }) 

Di sini kita menggunakan paket npm node-fetch dan sumber jsonplaceholder.typicode.com sebagai sumber data JSON.

Dalam contoh ini, fungsi fetch() digunakan untuk memuat item daftar TODO menggunakan rantai janji. Setelah mengeksekusi fetch() , respons dikembalikan yang memiliki banyak properti, di antaranya kami tertarik pada yang berikut:

  • status adalah nilai numerik yang mewakili kode status HTTP.
  • statusText - deskripsi tekstual dari kode status HTTP, yang diwakili oleh string OK jika permintaan berhasil.

Objek response memiliki metode json() yang mengembalikan janji, setelah resolusi yang konten diproses dari badan permintaan disajikan, disajikan dalam format JSON.

Dengan penjelasan di atas, kami menjelaskan apa yang terjadi dalam kode ini. Janji pertama dalam rantai diwakili oleh fungsi status() yang kami umumkan, yang memeriksa status respons, dan jika itu menunjukkan bahwa permintaan gagal (yaitu, kode status HTTP tidak dalam kisaran antara 200 dan 299), janji itu ditolak. Operasi ini mengarah pada fakta bahwa ekspresi .catch() lainnya dalam rantai janji tidak dieksekusi dan kami segera sampai ke metode .catch() , menghasilkan ke konsol, bersama dengan pesan kesalahan, teks Request failed .

Jika kode status HTTP cocok untuk kita, fungsi json() dideklarasikan oleh kita disebut. Karena janji sebelumnya, jika berhasil diselesaikan, mengembalikan objek response , kami menggunakannya sebagai nilai input untuk janji kedua.

Dalam hal ini, kami mengembalikan data JSON yang diproses, sehingga janji ketiga menerimanya, setelah itu, didahului oleh pesan bahwa sebagai hasil dari permintaan itu mungkin untuk mendapatkan data yang diperlukan, ditampilkan di konsol.

Menangani kesalahan


Dalam contoh sebelumnya, kami memiliki metode .catch() yang melekat pada rantai janji. Jika sesuatu dalam rantai janji salah dan terjadi kesalahan, atau jika salah satu janji ternyata ditolak, kontrol ditransfer ke ekspresi terdekat .catch() . Inilah situasi ketika kesalahan terjadi dalam sebuah janji:

 new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) 

Berikut adalah contoh pemicu .catch() setelah menolak janji:

 new Promise((resolve, reject) => { reject('Error') }) .catch((err) => { console.error(err) }) 

Penanganan kesalahan Cascading


Bagaimana jika terjadi kesalahan dalam ekspresi .catch() ? Untuk menangani kesalahan ini, Anda bisa memasukkan ekspresi .catch() di rantai janji (dan kemudian Anda bisa melampirkan ekspresi .catch() ke rantai sesuai kebutuhan):

 new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { throw new Error('Error') }) .catch((err) => { console.error(err) }) 

Sekarang mari kita lihat beberapa metode berguna yang digunakan untuk mengelola janji.

Promise.all ()


Jika Anda perlu melakukan beberapa tindakan setelah menyelesaikan beberapa janji, Anda bisa melakukan ini menggunakan perintah Promise.all() . Pertimbangkan sebuah contoh:

 const f1 = fetch('https://jsonplaceholder.typicode.com/todos/1') const f2 = fetch('https://jsonplaceholder.typicode.com/todos/2') Promise.all([f1, f2]).then((res) => {   console.log('Array of results', res) }) .catch((err) => { console.error(err) }) 

Di ES2015, sintaks penugasan destruktif telah muncul, dengan menggunakannya, Anda dapat membuat konstruksi dari formulir berikut:

 Promise.all([f1, f2]).then(([res1, res2]) => {   console.log('Results', res1, res2) }) 

Di sini, sebagai contoh, kami mempertimbangkan Ambil API, tetapi Promise.all() , tentu saja, memungkinkan Anda untuk bekerja dengan janji apa pun.

Promise.race ()


Perintah Promise.race() memungkinkan Anda untuk melakukan tindakan yang ditentukan setelah salah satu dari janji yang dijatuhkan kepadanya diselesaikan. Callback terkait yang berisi hasil dari janji pertama ini disebut hanya sekali. Pertimbangkan sebuah contoh:

 const first = new Promise((resolve, reject) => {   setTimeout(resolve, 500, 'first') }) const second = new Promise((resolve, reject) => {   setTimeout(resolve, 100, 'second') }) Promise.race([first, second]).then((result) => { console.log(result) // second }) 

Kesalahan TypeError yang tidak tertangkap yang terjadi saat bekerja dengan janji


Jika, ketika bekerja dengan janji-janji, Anda menemukan Uncaught TypeError: undefined is not a promise kesalahan Uncaught TypeError: undefined is not a promise , pastikan bahwa konstruk new Promise() digunakan bukan hanya Promise() saat membuat janji.

▍ async / tunggu desain


Konstruksi async / await adalah pendekatan modern untuk pemrograman asinkron, yang menyederhanakannya. Fungsi asinkron dapat direpresentasikan sebagai kombinasi dari janji dan generator, dan, secara umum, konstruksi ini adalah abstraksi atas janji.

Desain async / await mengurangi jumlah kode boilerplate yang harus Anda tulis saat bekerja dengan janji. Ketika janji muncul dalam standar ES2015, mereka bertujuan memecahkan masalah membuat kode asinkron. Mereka mengatasi tugas ini, tetapi dalam dua tahun, berbagi output dari standar ES2015 dan ES2017, menjadi jelas bahwa mereka tidak dapat dianggap sebagai solusi akhir untuk masalah ini.

Salah satu masalah yang dijanjikan diselesaikan adalah "neraka panggilan balik" yang terkenal, tetapi mereka, memecahkan masalah ini, menciptakan masalah mereka sendiri dengan sifat yang serupa.

Janji adalah konstruksi sederhana di mana seseorang dapat membangun sesuatu dengan sintaksis yang lebih sederhana. Akibatnya, ketika saatnya tiba, konstruk async / menunggu muncul. Penggunaannya memungkinkan Anda untuk menulis kode yang terlihat seperti sinkron, tetapi asinkron, khususnya, tidak memblokir utas utama.

Bagaimana async / menunggu konstruksi berfungsi


Fungsi asinkron mengembalikan janji, seperti dalam contoh berikut:

 const doSomethingAsync = () => {   return new Promise((resolve) => {       setTimeout(() => resolve('I did something'), 3000)   }) } 

Ketika Anda perlu memanggil fungsi yang serupa, Anda harus menempatkan kata kunci yang await sebelum perintah untuk memanggilnya. Ini akan menyebabkan kode yang memanggilnya menunggu izin atau penolakan dari janji yang sesuai. Perlu dicatat bahwa fungsi yang menggunakan kata kunci await harus dinyatakan menggunakan async :

 const doSomething = async () => {   console.log(await doSomethingAsync()) } 

Gabungkan dua fragmen kode di atas dan periksa perilakunya:

 const doSomethingAsync = () => {   return new Promise((resolve) => {       setTimeout(() => resolve('I did something'), 3000)   }) } const doSomething = async () => {   console.log(await doSomethingAsync()) } console.log('Before') doSomething() console.log('After') 

Kode ini akan menampilkan yang berikut:

 Before After I did something 

Teks I did something masuk ke konsol dengan penundaan 3 detik.

Tentang Janji dan Fungsi Asinkron


Jika Anda mendeklarasikan fungsi tertentu menggunakan async , ini akan berarti bahwa fungsi tersebut akan mengembalikan janji bahkan jika itu tidak dilakukan secara eksplisit. Itulah sebabnya, misalnya, contoh berikut adalah kode yang berfungsi:

 const aFunction = async () => { return 'test' } aFunction().then(console.log) //    'test' 

Desain ini mirip dengan ini:

 const aFunction = async () => { return Promise.resolve('test') } aFunction().then(console.log) //    'test' 

Kekuatan async / menunggu


Dengan menganalisis contoh-contoh di atas, Anda dapat melihat bahwa kode yang menggunakan async / menunggu lebih sederhana daripada kode yang menggunakan pengingkaran janji, atau kode berdasarkan fungsi panggilan balik. Di sini, tentu saja, kami melihat contoh yang sangat sederhana. Anda dapat sepenuhnya merasakan manfaat di atas dengan bekerja dengan kode yang jauh lebih kompleks. Di sini, misalnya, adalah cara memuat dan mem-parsing data JSON menggunakan janji:

 const getFirstUserData = () => { return fetch('/users.json') //      .then(response => response.json()) //  JSON   .then(users => users[0]) //      .then(user => fetch(`/users/${user.name}`)) //       .then(userResponse => userResponse.json()) //  JSON } getFirstUserData() 

Inilah solusi untuk masalah yang sama seperti menggunakan async / tunggu:

 const getFirstUserData = async () => { const response = await fetch('/users.json') //    const users = await response.json() //  JSON const user = users[0] //    const userResponse = await fetch(`/users/${user.name}`) //     const userData = await userResponse.json() //  JSON return userData } getFirstUserData() 

Menggunakan urutan dari fungsi asinkron


Fungsi asinkron dapat dengan mudah digabungkan ke dalam desain yang menyerupai rantai Janji. Namun, hasil dari kombinasi semacam itu adalah keterbacaan yang jauh lebih baik:

 const promiseToDoSomething = () => {   return new Promise(resolve => {       setTimeout(() => resolve('I did something'), 10000)   }) } const watchOverSomeoneDoingSomething = async () => {   const something = await promiseToDoSomething()   return something + ' and I watched' } const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {   const something = await watchOverSomeoneDoingSomething()   return something + ' and I watched as well' } watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {   console.log(res) }) 

Kode ini akan menampilkan teks berikut:

 I did something and I watched and I watched as well 

Debugging sederhana


Janji sulit untuk di-debug, karena dengan menggunakannya Anda tidak bisa menggunakan alat debugger yang biasa (seperti "step bypass", step-over). Kode yang ditulis menggunakan async / await dapat didebug menggunakan metode yang sama seperti kode sinkron biasa.

Pembuatan Acara di Node.js


Jika Anda bekerja dengan JavaScript di browser, maka Anda tahu bahwa acara memainkan peran besar dalam menangani interaksi pengguna dengan halaman. Ini tentang menangani acara yang disebabkan oleh klik dan gerakan mouse, penekanan tombol pada keyboard, dan sebagainya. Di Node.js, Anda bisa bekerja dengan acara yang dibuat oleh programmer sendiri. Di sini Anda dapat membuat sistem acara Anda sendiri menggunakan modul acara . Secara khusus, modul ini menawarkan kelas EventEmitter , yang kemampuannya dapat digunakan untuk mengatur pekerjaan dengan acara. Sebelum menggunakan mekanisme ini, Anda harus menghubungkannya:

 const EventEmitter = require('events').EventEmitter 

Ketika bekerja dengannya, metode on() dan emit() tersedia untuk kita, antara lain. Metode emit digunakan untuk memanggil peristiwa. Metode on digunakan untuk mengonfigurasi panggilan balik, penangan peristiwa yang dipanggil saat peristiwa tertentu dipanggil.

Misalnya, mari kita buat acara start . Ketika itu terjadi, kami akan menampilkan sesuatu ke konsol:

 eventEmitter = new EventEmitter(); eventEmitter.on('start', () => { console.log('started') }) 

Untuk memicu acara ini, konstruksi berikut digunakan:

 eventEmitter.emit('start') 

Sebagai hasil dari pelaksanaan perintah ini, pengendali event dipanggil dan string started sampai ke konsol.

Anda bisa meneruskan argumen ke event handler, mewakili mereka sebagai argumen tambahan ke metode emit() :

 eventEmitter.on('start', (number) => { console.log(`started ${number}`) }) eventEmitter.emit('start', 23) 

Hal yang sama terjadi dalam kasus di mana pawang harus melewati beberapa argumen:

 eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`) }) eventEmitter.emit('start', 1, 100) 

EventEmitter kelas EventEmitter memiliki beberapa metode berguna lainnya:

  • once() - memungkinkan Anda untuk mendaftar pengendali acara yang hanya dapat dipanggil sekali.
  • removeListener() - memungkinkan Anda untuk menghapus handler yang diteruskan ke dalamnya dari array handler dari event yang diteruskan ke sana.
  • removeAllListeners() - memungkinkan Anda untuk menghapus semua penangan acara yang diteruskan ke sana.

Ringkasan


Hari ini kita berbicara tentang pemrograman asinkron dalam JavaScript, khususnya, kita membahas panggilan balik, janji, dan konstruk async / menunggu. Di sini kami menyinggung masalah bekerja dengan acara yang dijelaskan oleh pengembang menggunakan modul events . Topik kita berikutnya adalah mekanisme jaringan platform Node.js.

Pembaca yang budiman! Saat memprogram untuk Node.js, apakah Anda menggunakan konstruk async / wait?

Source: https://habr.com/ru/post/id424555/


All Articles