Reaktivitas JavaScript: Contoh Sederhana dan Intuitif

Banyak kerangka kerja front-end JavaScript (seperti Angular, React, dan Vue) memiliki sistem reaktivitasnya sendiri. Memahami fitur-fitur sistem ini akan berguna bagi pengembang mana pun, akan membantunya lebih efisien menggunakan kerangka kerja JS modern.



Bahan, terjemahan yang kami terbitkan hari ini, menunjukkan contoh langkah-demi-langkah dari pengembangan sistem reaktivitas dalam JavaScript murni. Sistem ini mengimplementasikan mekanisme yang sama yang digunakan dalam Vue.

Sistem reaktivitas


Bagi seseorang yang pertama kali menemukan sistem reaktivitas Vue, itu mungkin tampak seperti kotak hitam misterius. Pertimbangkan aplikasi Vue sederhana. Ini markupnya:

<div id="app">    <div>Price: ${{ price }}</div>    <div>Total: ${{ price*quantity }}</div>    <div>Taxes: ${{ totalPriceWithTax }}</div> </div> 

Berikut adalah kerangka perintah koneksi dan kode aplikasi.

 <script src="https://cdn.jsdelivr.net/npm/vue"></script> <script>   var vm = new Vue({       el: '#app',       data: {           price: 5.00,           quantity: 2       },       computed: {           totalPriceWithTax() {               return this.price * this.quantity * 1.03           }       }   }) </script> 

Entah bagaimana, Vue mengetahui bahwa ketika price berubah, mesin perlu melakukan tiga hal:

  1. Refresh nilai price di halaman web.
  2. Hitung ulang ekspresi di mana price dikalikan dengan quantity , dan tampilkan nilai yang dihasilkan pada halaman.
  3. Panggil fungsi totalPriceWithTax dan, sekali lagi, masukkan apa yang dikembalikan pada halaman.

Apa yang terjadi di sini ditunjukkan pada ilustrasi berikut.


Bagaimana Vue tahu apa yang harus dilakukan ketika harga properti berubah?

Sekarang kami memiliki pertanyaan tentang bagaimana Vue tahu apa yang sebenarnya perlu diperbarui ketika price berubah, dan bagaimana mesin melacak apa yang terjadi di halaman. Apa yang dapat Anda amati di sini tidak terlihat seperti aplikasi JS biasa.

Mungkin ini belum jelas, tetapi masalah utama yang perlu kita selesaikan di sini adalah bahwa program JS biasanya tidak berfungsi seperti itu. Sebagai contoh, mari kita jalankan kode berikut:

 let price = 5 let quantity = 2 let total = price * quantity //  10 price = 20; console.log(`total is ${total}`) 

Menurut Anda apa yang akan ditampilkan di konsol? Karena tidak ada yang digunakan di sini kecuali JS biasa, 10 akan sampai ke konsol.


Hasil program

Dan ketika menggunakan kemampuan Vue, dalam situasi yang sama, kita dapat menerapkan skenario di mana nilai total dihitung ketika variabel price atau quantity berubah. Artinya, jika sistem reaktivitas digunakan dalam pelaksanaan kode di atas, maka bukan 10, tetapi 40 akan ditampilkan pada konsol:


Output konsol dihasilkan oleh kode hipotetis menggunakan sistem reaktivitas

JavaScript adalah bahasa yang dapat berfungsi baik secara prosedural maupun berorientasi objek, tetapi tidak memiliki sistem reaktivitas bawaan, oleh karena itu kode yang kami pertimbangkan saat mengubah price tidak akan menampilkan angka 40 ke konsol. Agar indikator total dihitung ulang ketika price atau quantity berubah, kita perlu membuat sistem reaktivitas sendiri dan dengan demikian mencapai perilaku yang kita butuhkan. Kami akan memecah jalan menuju tujuan ini menjadi beberapa langkah kecil.

Tugas: penyimpanan aturan untuk menghitung indikator


Kita memerlukan suatu tempat untuk menyimpan informasi tentang bagaimana indikator total dihitung, yang akan memungkinkan kita untuk menghitung ulang ketika mengubah nilai variabel price atau quantity .

OlSolusi


Pertama, kita perlu memberi tahu aplikasi ini sebagai berikut: "Ini adalah kode yang akan saya jalankan, simpan, saya mungkin perlu menjalankannya lain kali." Maka kita perlu menjalankan kode. Kemudian, jika indikator price atau quantity telah berubah, Anda harus memanggil kode yang disimpan untuk menghitung ulang total . Ini terlihat seperti ini:


Kode perhitungan total perlu disimpan di suatu tempat agar dapat mengaksesnya nanti

Kode yang dapat Anda panggil dalam JavaScript untuk melakukan beberapa tindakan diformat sebagai fungsi. Oleh karena itu, kami akan menulis fungsi yang berhubungan dengan perhitungan total , dan juga membuat mekanisme untuk menyimpan fungsi yang mungkin kita perlukan nanti.

 let price = 5 let quantity = 2 let total = 0 let target = null target = function () {   total = price * quantity } record() //       ,       target() //   

Perhatikan bahwa kami menyimpan fungsi anonim dalam variabel target , dan kemudian memanggil fungsi record . Kami akan membicarakannya di bawah ini. Saya juga ingin mencatat bahwa fungsi target , menggunakan sintaks fungsi panah ES6, dapat ditulis ulang sebagai berikut:

 target = () => { total = price * quantity } 

Berikut adalah deklarasi fungsi record dan struktur data yang digunakan untuk menyimpan fungsi:

 let storage = [] //     target function record () { // target = () => { total = price * quantity }   storage.push(target) } 

Menggunakan fungsi record , kami menyimpan fungsi target (dalam kasus kami, { total = price * quantity } ) dalam array storage , yang memungkinkan kami untuk memanggil fungsi ini nanti, mungkin menggunakan fungsi replay , kode yang ditunjukkan di bawah ini. Ini akan memungkinkan kita untuk memanggil semua fungsi yang tersimpan dalam storage .

 function replay () {   storage.forEach(run => run()) } 

Di sini kita pergi melalui semua fungsi anonim yang disimpan dalam array storage dan menjalankan masing-masing.

Kemudian dalam kode kita kita dapat melakukan hal berikut:

 price = 20 console.log(total) // 10 replay() console.log(total) // 40 

Tidak semua terlihat sulit, bukan? Inilah keseluruhan kode, fragmen-fragmen yang telah kita bahas di atas, seandainya lebih nyaman bagi Anda untuk akhirnya menanganinya. Ngomong-ngomong, kode ini tidak sengaja ditulis seperti itu.

 let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] function record () {   storage.push(target) } function replay () {   storage.forEach(run => run()) } target = () => { total = price * quantity } record() target() price = 20 console.log(total) // 10 replay() console.log(total) // 40 

Inilah yang akan ditampilkan di konsol browser setelah dimulai.


Hasil kode

Tantangan: solusi andal untuk menyimpan fungsi


Kita dapat terus menuliskan fungsi yang kita butuhkan saat diperlukan, tetapi alangkah baiknya jika kita memiliki solusi yang lebih andal yang dapat ditingkatkan dengan aplikasi. Mungkin itu akan menjadi kelas yang memelihara daftar fungsi yang awalnya ditulis untuk variabel target , dan yang menerima pemberitahuan jika kita perlu menjalankan kembali fungsi-fungsi ini.

▍Solusi: Kelas ketergantungan


Salah satu pendekatan untuk memecahkan masalah di atas adalah merangkum perilaku yang kita butuhkan di kelas, yang dapat disebut Ketergantungan. Kelas ini akan menerapkan pola pemrograman pengamat standar.

Akibatnya, jika kita membuat kelas JS yang digunakan untuk mengelola dependensi kita (yang akan dekat dengan bagaimana mekanisme serupa diimplementasikan di Vue), mungkin terlihat seperti ini:

 class Dep { // Dep -    Dependency   constructor () {       this.subscribers = [] //  ,                               //    notify()   }   depend () { //   record       if (target && !this.subscribers.includes(target)){           //    target                //                this.subscribers.push(target)       }   }   notify () { //   replay       this.subscribers.forEach(sub => sub())       //  -     } } 

Harap perhatikan bahwa alih-alih array storage , kami sekarang menyimpan fungsi anonim kami dalam array subscribers . Alih-alih fungsi record , metode depend sekarang disebut. Juga di sini, alih-alih fungsi replay , fungsi notify . Berikut cara menjalankan kode kami menggunakan kelas Dep :

 const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend() //   target    target() //     total console.log(total) // 10 -   price = 20 console.log(total) // 10 -    ,    dep.notify() //   -  console.log(total) // 40 -    

Kode baru kami berfungsi sama seperti sebelumnya, tetapi sekarang dirancang lebih baik, dan rasanya lebih baik digunakan kembali.

Satu-satunya hal yang tampak aneh di dalamnya sejauh ini adalah bekerja dengan fungsi yang disimpan dalam variabel target .

Tugas: mekanisme untuk membuat fungsi anonim


Di masa depan, kita perlu membuat objek kelas Dep untuk setiap variabel. Selain itu, akan lebih baik untuk merangkum perilaku membuat fungsi anonim di suatu tempat, yang harus dipanggil saat memperbarui data yang relevan. Mungkin ini akan membantu kita dengan fungsi tambahan, yang akan kita sebut watcher . Ini akan mengarah pada fakta bahwa kita dapat mengganti konstruksi ini dari contoh sebelumnya dengan fungsi baru:

 let target = () => { total = price * quantity } dep.depend() target() 

Bahkan, panggilan ke fungsi watcher yang menggantikan kode ini akan terlihat seperti ini:

 watcher(() => {   total = price * quantity }) 

▍ Solusi: fungsi pengamat


Di dalam fungsi watcher , kode yang disajikan di bawah ini, kita dapat melakukan beberapa tindakan sederhana:

 function watcher(myFunc) {   target = myFunc //   target   myFunc   dep.depend() //  target      target() //     target = null //   target } 

Seperti yang Anda lihat, fungsi watcher mengambil, sebagai argumen, fungsi myFunc , menulisnya ke variabel target global, memanggil dep.depend() untuk menambahkan fungsi ini ke daftar pelanggan, memanggil fungsi ini dan mengatur ulang variabel target .
Sekarang kita mendapatkan semua nilai yang sama 10 dan 40 jika kita menjalankan kode berikut:

 price = 20 console.log(total) dep.notify() console.log(total) 

Mungkin Anda bertanya-tanya mengapa kami menerapkan target sebagai variabel global, alih-alih meneruskan variabel ini ke fungsi kami, jika perlu. Kami punya alasan bagus untuk melakukan hal itu, nanti Anda akan mengerti.

Tugas: memiliki objek Dep untuk setiap variabel


Kami memiliki objek tunggal dari kelas Dep . Bagaimana jika kita membutuhkan masing-masing variabel kita untuk memiliki objek kelas Dep sendiri? Sebelum kita melanjutkan, mari kita pindahkan data yang kita kerjakan ke properti objek:

 let data = { price: 5, quantity: 2 } 

Bayangkan sejenak bahwa masing-masing properti kita ( price dan quantity ) memiliki objek kelas Dep internal sendiri.


Properti harga dan kuantitas

Sekarang kita dapat memanggil fungsi watcher seperti ini:

 watcher(() => {   total = data.price * data.quantity }) 

Karena kita bekerja di sini dengan nilai properti data.price , kita memerlukan objek kelas Dep dari properti price untuk menempatkan fungsi anonim (disimpan dalam target ) dalam array pelanggannya (dengan memanggil dep.depend() ). Selain itu, karena kami bekerja dengan data.quantity , kami memerlukan objek Dep dari properti quantity untuk menempatkan fungsi anonim (sekali lagi, disimpan dalam target ) dalam array pelanggannya.

Jika Anda menggambarkan ini dalam bentuk diagram, Anda mendapatkan yang berikut ini.


Fungsi jatuh ke dalam array pelanggan objek kelas Dep yang sesuai dengan properti yang berbeda

Jika kita memiliki satu lagi fungsi anonim di mana kita hanya bekerja dengan properti data.price , maka fungsi anonim yang sesuai hanya akan pergi ke Depot objek dari properti ini.


Pengamat tambahan dapat ditambahkan hanya ke salah satu properti yang tersedia.

Kapan Anda mungkin perlu menelepon dep.notify() untuk fungsi yang berlangganan perubahan price properti? Ini akan diperlukan saat mengubah price . Ini berarti bahwa ketika contoh kita sudah benar-benar siap, kode berikut harus bekerja untuk kita.


Di sini, saat mengubah harga, Anda perlu menelepon dep.notify () untuk semua fungsi yang berlangganan perubahan harga

Agar semuanya berfungsi dengan cara ini, kita perlu beberapa cara untuk mencegat acara akses properti (dalam kasus kami, itu adalah price atau quantity ). Ini akan memungkinkan, ketika ini terjadi, untuk menyimpan fungsi target ke dalam array pelanggan, dan ketika variabel terkait berubah, untuk mengeksekusi fungsi yang disimpan dalam array ini.

▍Solusi: Object.defineProperty ()


Sekarang kita perlu berkenalan dengan metode ES5 standar Object.defineProperty (). Ini memungkinkan Anda untuk menetapkan getter dan setter ke properti objek. Izinkan saya, sebelum kita beralih ke penggunaan praktisnya, untuk menunjukkan operasi mekanisme ini dengan contoh sederhana.

 let data = { price: 5, quantity: 2 } Object.defineProperty(data, 'price', { //       price   get() { //        console.log(`I was accessed`)   },   set(newVal) { //        console.log(`I was changed`)   } }) data.price //       data.price = 20 //      

Jika Anda menjalankan kode ini di konsol browser, itu akan menampilkan teks berikut.


Hasil Getter dan Setter

Seperti yang Anda lihat, contoh kami hanya mencetak beberapa baris teks ke konsol. Namun, itu tidak membaca atau menetapkan nilai, karena kami mendefinisikan ulang fungsionalitas standar getter dan setter. Kami akan mengembalikan fungsionalitas metode ini. Yaitu, diharapkan getter mengembalikan nilai metode yang sesuai, dan setter menyetelnya. Oleh karena itu, kami akan menambahkan variabel baru, internalValue , ke kode, yang akan kami gunakan untuk menyimpan nilai price saat ini.

 let data = { price: 5, quantity: 2 } let internalValue = data.price //   Object.defineProperty(data, 'price', { //       price   get() { //        console.log(`Getting price: ${internalValue}`)       return internalValue   },   set(newVal) {       console.log(`Setting price to: ${newVal}`)       internalValue = newVal   } }) total = data.price * data.quantity //       data.price = 20 //      

Sekarang setelah pengambil dan penyetel bekerja sebagaimana mestinya, apa yang menurut Anda akan masuk ke konsol ketika kode ini dieksekusi? Lihatlah gambar berikut.


Output data ke konsol

Jadi, sekarang kami memiliki mekanisme yang memungkinkan Anda menerima pemberitahuan saat membaca nilai properti dan ketika nilai baru dituliskan kepada mereka. Sekarang, setelah mengerjakan ulang kode sedikit, kita dapat melengkapi getter dan setter dengan semua properti dari objek data . Di sini kita akan menggunakan metode Object.keys() , yang mengembalikan array kunci dari objek yang diteruskan ke sana.

 let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { //        data   let internalValue = data[key]   Object.defineProperty(data, key, {       get() {           console.log(`Getting ${key}: ${internalValue}`)           return internalValue       },       set(newVal) {           console.log(`Setting ${key} to: ${newVal}`)           internalValue = newVal       }   }) }) let total = data.price * data.quantity data.price = 20 

Sekarang semua properti objek data memiliki getter dan setter. Inilah yang muncul di konsol setelah menjalankan kode ini.


Output data ke konsol oleh getter dan setter

Perakitan Sistem Reaktivitas


Ketika sebuah fragmen kode seperti total = data.price * data.quantity dan nilai properti price diperoleh di dalamnya, kita perlu properti price untuk "mengingat" fungsi anonim yang sesuai ( target dalam kasus kami). Akibatnya, jika properti price diubah, yaitu diatur ke nilai baru, ini akan menyebabkan panggilan ke fungsi ini untuk mengulang operasi yang dilakukan olehnya, karena ia tahu bahwa baris kode tertentu tergantung padanya. Akibatnya, operasi yang dilakukan dalam getter dan setter dapat dibayangkan sebagai berikut:

  • Getter - Anda harus mengingat fungsi anonim, yang akan kami panggil lagi ketika nilainya berubah.
  • Setter - perlu untuk menjalankan fungsi anonim yang disimpan, yang akan mengarah pada perubahan nilai hasil yang sesuai.

Jika Anda menggunakan kelas Dep sudah Anda kenal dalam deskripsi ini, Anda mendapatkan yang berikut ini:

  • Saat membaca nilai properti, dep.depend() dipanggil untuk menyimpan fungsi target saat ini.
  • Ketika nilai ditulis ke properti, dep.notify() dipanggil untuk memulai kembali semua fungsi yang disimpan.

Sekarang kita akan menggabungkan kedua ide ini dan, akhirnya, kita akan sampai pada kode yang memungkinkan kita untuk mencapai tujuan kita.

 let data = { price: 5, quantity: 2 } let target = null //  -    ,     class Dep {   constructor () {       this.subscribers = []   }   depend () {       if (target && !this.subscribers.includes(target)){           this.subscribers.push(target)       }   }   notify () {       this.subscribers.forEach(sub => sub())   } } //      ,  //      Object.keys(data).forEach(key => {   let internalValue = data[key]   //         //   Dep   const dep = new Dep()   Object.defineProperty(data, key, {       get() {           dep.depend() //    target           return internalValue       },       set(newVal) {           internalValue = newVal           dep.notify() //           }   }) }) //   watcher   dep.depend(), //        function watcher(myFunc){   target = myFunc   target()   target = null } watcher(() => {   data.total = data.price * data.quantity }) 

Mari kita bereksperimen dengan kode ini di konsol browser.


Eksperimen kode siap

Seperti yang Anda lihat, itu berfungsi persis seperti yang kita butuhkan! Properti price dan quantity telah menjadi reaktif! Semua kode yang bertanggung jawab untuk menghasilkan total ketika perubahan price atau quantity dieksekusi berulang kali.

Sekarang, setelah kami menulis sistem reaktivitas kami sendiri, ilustrasi dari dokumentasi Vue ini akan terasa familier dan mudah dipahami oleh Anda.


Sistem reaktivitas Vue

Lihat lingkaran ungu cantik yang bertuliskan mengandung getter dan setter? Sekarang dia harus terbiasa dengan Anda. Setiap instance dari komponen memiliki instance dari metode observer (lingkaran biru), yang mengumpulkan dependensi pada getter (garis merah). Ketika, kemudian, setter dipanggil, ia memberitahukan metode pengamat, yang mengarah pada rendering ulang komponen. Berikut adalah skema yang sama, dilengkapi dengan penjelasan yang menghubungkannya dengan pengembangan kami.


Diagram reaktivitas Vue dengan penjelasan

Kami percaya bahwa sekarang, setelah kami menulis sistem reaktivitas kami sendiri, skema ini tidak perlu penjelasan tambahan.

Tentu saja, di Vue, semua ini lebih rumit, tetapi sekarang Anda harus memahami mekanisme yang mendasari sistem reaktivitas.

Ringkasan


Setelah membaca materi ini, Anda mempelajari yang berikut:

  • Cara membuat kelas Dep yang mengumpulkan fungsi menggunakan metode depend , dan, jika perlu, memanggil mereka lagi menggunakan metode notify .
  • Cara membuat fungsi watcher yang memungkinkan Anda untuk mengontrol kode yang kami jalankan (ini adalah fungsi target ), yang mungkin perlu Anda simpan di objek kelas Dep .
  • Cara menggunakan metode Object.defineProperty() untuk membuat getter dan setter.

Semua ini, dikompilasi dalam satu contoh tunggal, mengarah pada penciptaan sistem responsif dalam JavaScript murni, dengan memahami yang mana Anda dapat memahami fitur-fitur fungsi sistem yang digunakan dalam kerangka web modern.

Pembaca yang budiman! Jika, sebelum membaca materi ini, Anda dengan buruk membayangkan fitur-fitur mekanisme sistem reaktivitas, katakan padaku, apakah Anda sekarang berhasil mengatasinya?

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


All Articles