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:
- Refresh nilai
price
di halaman web. - Hitung ulang ekspresi di mana
price
dikalikan dengan quantity
, dan tampilkan nilai yang dihasilkan pada halaman. - 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
Menurut Anda apa yang akan ditampilkan di konsol? Karena tidak ada yang digunakan di sini kecuali JS biasa,
10
akan sampai ke konsol.
Hasil programDan 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 reaktivitasJavaScript 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 nantiKode 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()
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)
Inilah yang akan ditampilkan di konsol browser setelah dimulai.
Hasil kodeTantangan: 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()
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 kuantitasSekarang 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 berbedaJika 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 hargaAgar 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 SetterSeperti 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 konsolJadi, 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 setterPerakitan 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 siapSeperti 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 VueLihat 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 penjelasanKami 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?