JavaScript memberikan fleksibilitas luar biasa ketika berhadapan dengan fungsi. Mereka dapat diedarkan, digunakan sebagai objek, dan sekarang kita akan melihat cara meneruskan panggilan di antara mereka dan menghiasinya .
Katakanlah kita mempunyai fungsi slow(x)
yang membutuhkan banyak CPU, namun hasilnya stabil. Dengan kata lain, untuk x
yang sama selalu memberikan hasil yang sama.
Jika fungsinya sering dipanggil, kita mungkin ingin menyimpan (mengingat) hasilnya dalam cache untuk menghindari menghabiskan waktu ekstra pada penghitungan ulang.
Namun alih-alih menambahkan fungsionalitas tersebut ke slow()
kita akan membuat fungsi wrapper, yang menambahkan caching. Seperti yang akan kita lihat, ada banyak manfaat dari melakukan hal ini.
Berikut kodenya, dan penjelasannya sebagai berikut:
fungsi lambat(x) { // mungkin ada pekerjaan berat yang membutuhkan banyak CPU di sini alert(`Dipanggil dengan ${x}`); kembalikan x; } fungsi cachingDecorator(fungsi) { biarkan cache = Peta baru(); fungsi pengembalian(x) { if (cache.has(x)) {// jika ada kunci tersebut di cache kembalikan cache.get(x); // baca hasilnya } misalkan hasil = func(x); // jika tidak, panggil func cache.set(x, hasil); // dan cache (ingat) hasilnya hasil pengembalian; }; } lambat = cachingDecorator(lambat); waspada( lambat(1) ); // lambat(1) di-cache dan hasilnya dikembalikan alert("Lagi: " + lambat(1) ); // lambat(1) hasil yang dikembalikan dari cache waspada( lambat(2) ); // lambat(2) di-cache dan hasilnya dikembalikan alert("Lagi: "+lambat(2)); // lambat(2) hasil dikembalikan dari cache
Dalam kode di atas cachingDecorator
adalah dekorator : fungsi khusus yang mengambil fungsi lain dan mengubah perilakunya.
Idenya adalah kita bisa memanggil cachingDecorator
untuk fungsi apa pun, dan itu akan mengembalikan pembungkus caching. Itu bagus, karena kita dapat memiliki banyak fungsi yang dapat menggunakan fitur tersebut, dan yang perlu kita lakukan hanyalah menerapkan cachingDecorator
ke fungsi tersebut.
Dengan memisahkan caching dari kode fungsi utama, kami juga menjaga kode utama tetap sederhana.
Hasil dari cachingDecorator(func)
adalah “wrapper”: function(x)
yang “membungkus” pemanggilan func(x)
ke dalam logika caching:
Dari kode luar, fungsi slow
yang dibungkus masih melakukan hal yang sama. Itu baru saja menambahkan aspek caching ke perilakunya.
Ringkasnya, ada beberapa keuntungan menggunakan cachingDecorator
terpisah daripada mengubah kode slow
itu sendiri:
cachingDecorator
dapat digunakan kembali. Kita bisa menerapkannya ke fungsi lain.
Logika caching terpisah, tidak menambah kompleksitas slow
itu sendiri (jika ada).
Kita dapat menggabungkan beberapa dekorator jika diperlukan (dekorator lain akan menyusul).
Dekorator caching yang disebutkan di atas tidak cocok untuk bekerja dengan metode objek.
Misalnya, dalam kode di bawah ini worker.slow()
berhenti bekerja setelah dekorasi:
// kita akan membuat cache pekerja.lambat biarkan pekerja = { beberapaMetode() { kembali 1; }, lambat(x) { // tugas berat CPU yang menakutkan di sini alert("Dipanggil dengan "+x); kembalikan x * ini.someMethod(); // (*) } }; // kode yang sama seperti sebelumnya fungsi cachingDecorator(fungsi) { biarkan cache = Peta baru(); fungsi pengembalian(x) { if (cache.has(x)) { kembalikan cache.get(x); } misalkan hasil = func(x); // (**) cache.set(x, hasil); hasil pengembalian; }; } peringatan( pekerja.lambat(1) ); // metode asli berhasil pekerja.lambat = cachingDecorator(pekerja.lambat); // sekarang buatlah cache waspada( pekerja.lambat(2) ); // Ups! Kesalahan: Tidak dapat membaca properti 'someMethod' yang tidak ditentukan
Kesalahan terjadi pada baris (*)
yang mencoba mengakses this.someMethod
dan gagal. Dapatkah Anda mengetahui alasannya?
Alasannya adalah pembungkus memanggil fungsi asli sebagai func(x)
di baris (**)
. Dan, ketika dipanggil seperti itu, fungsinya menjadi this = undefined
.
Kami akan mengamati gejala serupa jika kami mencoba menjalankan:
biarkan func = pekerja.lambat; fungsi(2);
Jadi, wrapper meneruskan panggilan ke metode asli, tetapi tanpa konteks this
. Oleh karena itu kesalahannya.
Mari kita perbaiki.
Ada metode fungsi bawaan khusus func.call(context, …args) yang memungkinkan untuk memanggil fungsi yang secara eksplisit menyetel this
.
Sintaksnya adalah:
fungsi.panggilan(konteks, arg1, arg2, ...)
Ini berjalan func
dengan memberikan argumen pertama sebagai this
, dan berikutnya sebagai argumen.
Sederhananya, kedua panggilan ini melakukan hal yang hampir sama:
fungsi(1, 2, 3); fungsi.panggilan(obj, 1, 2, 3)
Keduanya memanggil func
dengan argumen 1
, 2
dan 3
. Satu-satunya perbedaan adalah func.call
juga this
ke obj
.
Sebagai contoh, pada kode di bawah ini kita memanggil sayHi
dalam konteks objek yang berbeda: sayHi.call(user)
menjalankan sayHi
dengan menyediakan this=user
, dan baris berikutnya menyetel this=admin
:
fungsi ucapkan Hai() { alert(ini.nama); } biarkan pengguna = { nama: "John" }; biarkan admin = { nama: "Admin" }; // gunakan panggilan untuk meneruskan objek berbeda sebagai "ini" sayHi.call( pengguna ); // Yohanes sayHi.call( admin ); // Admin
Dan di sini kami menggunakan call
to call say
dengan konteks dan frasa tertentu:
fungsi katakan(frasa) { alert(ini.nama + ': ' + frase); } biarkan pengguna = { nama: "John" }; // pengguna menjadi ini, dan "Halo" menjadi argumen pertama say.call( pengguna, "Halo" ); // John: Halo
Dalam kasus kita, kita bisa menggunakan call
di wrapper untuk meneruskan konteks ke fungsi aslinya:
biarkan pekerja = { beberapaMetode() { kembali 1; }, lambat(x) { alert("Dipanggil dengan "+x); kembalikan x * ini.someMethod(); // (*) } }; fungsi cachingDecorator(fungsi) { biarkan cache = Peta baru(); fungsi pengembalian(x) { if (cache.has(x)) { kembalikan cache.get(x); } biarkan hasil = func.call(ini, x); // "ini" diteruskan dengan benar sekarang cache.set(x, hasil); hasil pengembalian; }; } pekerja.lambat = cachingDecorator(pekerja.lambat); // sekarang buatlah cache waspada( pekerja.lambat(2) ); // berhasil waspada( pekerja.lambat(2) ); // berfungsi, tidak memanggil yang asli (cache)
Sekarang semuanya baik-baik saja.
Untuk memperjelas semuanya, mari kita lihat lebih dalam bagaimana this
diteruskan:
Setelah worker.slow
sekarang menjadi function (x) { ... }
.
Jadi ketika worker.slow(2)
dijalankan, pembungkus mendapat 2
sebagai argumen dan this=worker
(itu adalah objek sebelum titik).
Di dalam pembungkus, dengan asumsi hasilnya belum di-cache, func.call(this, x)
meneruskan this
saat ini ( =worker
) dan argumen saat ini ( =2
) ke metode asli.
Sekarang mari kita jadikan cachingDecorator
lebih universal. Hingga saat ini, ini hanya berfungsi dengan fungsi argumen tunggal.
Sekarang bagaimana cara men-cache metode worker.slow
multi-argumen?
biarkan pekerja = { lambat(min, maks) { pengembalian min + maks; // diasumsikan CPU-hogger menakutkan } }; // harus mengingat panggilan dengan argumen yang sama pekerja.lambat = cachingDecorator(pekerja.lambat);
Sebelumnya, untuk satu argumen x
kita hanya bisa cache.set(x, result)
untuk menyimpan hasilnya dan cache.get(x)
untuk mengambilnya. Namun sekarang kita perlu mengingat hasil kombinasi argumen (min,max)
. Map
asli hanya mengambil nilai tunggal sebagai kuncinya.
Ada banyak solusi yang mungkin:
Menerapkan struktur data baru (atau menggunakan pihak ketiga) seperti peta yang lebih serbaguna dan memungkinkan multi-kunci.
Gunakan peta bersarang: cache.set(min)
akan menjadi Map
yang menyimpan pasangan (max, result)
. Jadi kita bisa mendapatkan result
sebagai cache.get(min).get(max)
.
Gabungkan dua nilai menjadi satu. Dalam kasus khusus kita, kita cukup menggunakan string "min,max"
sebagai kunci Map
. Untuk fleksibilitas, kami dapat mengizinkan untuk menyediakan fungsi hashing untuk dekorator, yang mengetahui cara membuat satu nilai dari banyak nilai.
Untuk banyak aplikasi praktis, varian ke-3 sudah cukup baik, jadi kami akan tetap menggunakannya.
Kita juga harus meneruskan tidak hanya x
, tetapi semua argumen di func.call
. Ingatlah bahwa dalam suatu function()
kita bisa mendapatkan array semu dari argumennya sebagai arguments
, jadi func.call(this, x)
harus diganti dengan func.call(this, ...arguments)
.
Inilah cachingDecorator
yang lebih kuat :
biarkan pekerja = { lambat(min, maks) { alert(`Dipanggil dengan ${min},${max}`); pengembalian min + maks; } }; fungsi cachingDecorator(fungsi, hash) { biarkan cache = Peta baru(); fungsi kembali() { biarkan kunci = hash(argumen); // (*) if (cache.has(kunci)) { kembalikan cache.get(kunci); } biarkan hasil = func.call(ini, ...argumen); // (**) cache.set(kunci, hasil); hasil pengembalian; }; } fungsi hash(argumen) { kembalikan args[0] + ',' + args[1]; } pekerja.lambat = cachingDecorator(pekerja.lambat, hash); waspada( pekerja.lambat(3, 5) ); // berhasil alert("Lagi" + pekerja.lambat(3, 5) ); // sama (di-cache)
Sekarang ia berfungsi dengan sejumlah argumen (walaupun fungsi hash juga perlu disesuaikan untuk memungkinkan sejumlah argumen. Cara menarik untuk menangani hal ini akan dibahas di bawah).
Ada dua perubahan:
Di baris (*)
ia memanggil hash
untuk membuat satu kunci dari arguments
. Di sini kita menggunakan fungsi “bergabung” sederhana yang mengubah argumen (3, 5)
menjadi kunci "3,5"
. Kasus yang lebih kompleks mungkin memerlukan fungsi hashing lainnya.
Kemudian (**)
menggunakan func.call(this, ...arguments)
untuk meneruskan konteks dan semua argumen yang didapat pembungkus (bukan hanya argumen pertama) ke fungsi aslinya.
Daripada func.call(this, ...arguments)
kita bisa menggunakan func.apply(this, arguments)
.
Sintaks metode bawaan func.apply adalah:
func.apply(konteks, argumen)
Ini menjalankan pengaturan func
this=context
dan menggunakan objek seperti array args
sebagai daftar argumen.
Satu-satunya perbedaan sintaksis antara call
dan apply
adalah bahwa call
mengharapkan daftar argumen, sedangkan apply
membawa objek seperti array.
Jadi kedua panggilan ini hampir setara:
func.call(konteks, ...args); func.apply(konteks, argumen);
Mereka melakukan panggilan func
yang sama dengan konteks dan argumen tertentu.
Hanya ada sedikit perbedaan mengenai args
:
Sintaks penyebaran ...
memungkinkan untuk meneruskan args
yang dapat diubah sebagai daftar yang akan call
.
apply
hanya menerima args
seperti array .
…Dan untuk objek yang dapat diubah dan mirip array, seperti array nyata, kita dapat menggunakan salah satu dari objek tersebut, namun apply
mungkin akan lebih cepat, karena sebagian besar mesin JavaScript mengoptimalkannya secara internal dengan lebih baik.
Meneruskan semua argumen beserta konteksnya ke fungsi lain disebut penerusan panggilan .
Itu bentuk paling sederhananya:
biarkan pembungkus = fungsi() { return func.apply(ini, argumen); };
Ketika kode eksternal memanggil wrapper
tersebut, kode tersebut tidak dapat dibedakan dari pemanggilan fungsi asli func
.
Sekarang mari kita lakukan satu lagi perbaikan kecil pada fungsi hashing:
fungsi hash(argumen) { kembalikan args[0] + ',' + args[1]; }
Saat ini, ini hanya berfungsi pada dua argumen. Akan lebih baik jika bisa merekatkan sejumlah args
.
Solusi alaminya adalah dengan menggunakan metode arr.join:
fungsi hash(argumen) { return args.join(); }
…Sayangnya, itu tidak akan berhasil. Karena kita memanggil hash(arguments)
, dan objek arguments
dapat diubah dan mirip array, tetapi bukan array sebenarnya.
Jadi memanggil join
akan gagal, seperti yang bisa kita lihat di bawah:
fungsi hash() { waspada(argumen.join() ); // Kesalahan: argument.join bukan sebuah fungsi } hash(1, 2);
Namun, ada cara mudah untuk menggunakan gabungan array:
fungsi hash() { alert( [].join.call(argumen) ); // 1,2 } hash(1, 2);
Caranya disebut metode peminjaman .
Kami mengambil (meminjam) metode join dari array biasa ( [].join
) dan menggunakan [].join.call
untuk menjalankannya dalam konteks arguments
.
Mengapa ini berhasil?
Itu karena algoritma internal metode asli arr.join(glue)
sangat sederhana.
Diambil dari spesifikasinya yang hampir “apa adanya”:
Biarkan glue
menjadi argumen pertama atau, jika tidak ada argumen, maka koma ","
.
Biarkan result
menjadi string kosong.
Tambahkan this[0]
ke result
.
Tambahkan glue
dan this[1]
.
Tambahkan glue
dan this[2]
.
…Lakukan sampai item this.length
direkatkan.
result
pengembalian.
Jadi, secara teknis dibutuhkan this
dan menggabungkan this[0]
, this[1]
…etc bersama-sama. Itu sengaja ditulis sedemikian rupa sehingga memungkinkan array seperti this
(bukan suatu kebetulan, banyak metode mengikuti praktik ini). Itu sebabnya ini juga berfungsi dengan this=arguments
.
Umumnya aman untuk mengganti fungsi atau metode dengan fungsi yang dihias, kecuali untuk satu hal kecil. Jika fungsi asli memiliki properti di dalamnya, seperti func.calledCount
atau apa pun, maka fungsi yang dihias tidak akan menyediakannya. Karena itu adalah pembungkus. Jadi seseorang perlu berhati-hati jika menggunakannya.
Misalnya dalam contoh di atas jika fungsi slow
memiliki properti apa pun di dalamnya, maka cachingDecorator(slow)
adalah pembungkus tanpa properti tersebut.
Beberapa dekorator mungkin menyediakan propertinya sendiri. Misalnya, seorang dekorator dapat menghitung berapa kali suatu fungsi dipanggil dan berapa lama waktu yang dibutuhkan, dan memaparkan informasi ini melalui properti wrapper.
Terdapat cara untuk membuat dekorator yang mempertahankan akses ke properti fungsi, tetapi ini memerlukan penggunaan objek Proxy
khusus untuk menggabungkan suatu fungsi. Nanti kita bahas di artikel Proxy dan Reflect.
Dekorator adalah pembungkus fungsi yang mengubah perilakunya. Pekerjaan pokok tetap dijalankan oleh fungsinya.
Dekorator dapat dilihat sebagai “fitur” atau “aspek” yang dapat ditambahkan ke suatu fungsi. Kita bisa menambahkan satu atau menambahkan banyak. Dan semua ini tanpa mengubah kodenya!
Untuk mengimplementasikan cachingDecorator
, kami mempelajari metode:
func.call(context, arg1, arg2…) – memanggil func
dengan konteks dan argumen tertentu.
func.apply(context, args) – memanggil func
meneruskan context
seperti this
dan args
seperti array ke dalam daftar argumen.
Penerusan panggilan umum biasanya dilakukan dengan apply
:
biarkan pembungkus = fungsi() { return original.apply(ini, argumen); };
Kita juga melihat contoh peminjaman metode ketika kita mengambil metode dari suatu objek dan call
dalam konteks objek lain. Sangat umum untuk mengambil metode array dan menerapkannya pada arguments
. Alternatifnya adalah dengan menggunakan objek parameter lainnya yang merupakan array nyata.
Ada banyak dekorator di alam liar. Periksa seberapa baik Anda menguasainya dengan menyelesaikan tugas-tugas bab ini.
pentingnya: 5
Buat spy(func)
yang akan mengembalikan pembungkus yang menyimpan semua panggilan agar berfungsi di properti calls
.
Setiap panggilan disimpan sebagai serangkaian argumen.
Misalnya:
fungsi kerja(a, b) { peringatan( a + b ); // kerja adalah fungsi atau metode arbitrer } kerja = mata-mata(kerja); pekerjaan(1, 2); // 3 kerja(4, 5); // 9 for (biarkan argumen pekerjaan.panggilan) { peringatan('panggilan:' + args.join() ); // "panggilan:1,2", "panggilan:4,5" }
PS Dekorator itu terkadang berguna untuk pengujian unit. Bentuk lanjutannya adalah sinon.spy
di perpustakaan Sinon.JS.
Buka kotak pasir dengan tes.
Pembungkus yang dikembalikan oleh spy(f)
harus menyimpan semua argumen dan kemudian menggunakan f.apply
untuk meneruskan panggilan.
fungsi mata-mata(fungsi) { pembungkus fungsi(...args) { // menggunakan ...args alih-alih argumen untuk menyimpan array "asli" di wrapper.calls wrapper.calls.push(args); return func.apply(ini, args); } wrapper.panggilan = []; bungkus kembali; }
Buka solusi dengan pengujian di kotak pasir.
pentingnya: 5
Buat dekorator delay(f, ms)
yang menunda setiap panggilan f
sebanyak ms
milidetik.
Misalnya:
fungsi f(x) { peringatan(x); } // membuat wrapper misalkan f1000 = penundaan(f, 1000); misalkan f1500 = penundaan(f, 1500); f1000("tes"); // menampilkan "tes" setelah 1000 ms f1500("tes"); // menampilkan "tes" setelah 1500 ms
Dengan kata lain, delay(f, ms)
mengembalikan varian “delayed by ms
” dari f
.
Dalam kode di atas, f
adalah fungsi dari argumen tunggal, tetapi solusi Anda harus meneruskan semua argumen dan konteks this
.
Buka kotak pasir dengan tes.
Solusinya:
penundaan fungsi(f, ms) { fungsi kembali() { setTimeout(() => f.apply(ini, argumen), ms); }; } misalkan f1000 = penundaan(peringatan, 1000); f1000("tes"); // menampilkan "tes" setelah 1000 ms
Harap perhatikan bagaimana fungsi panah digunakan di sini. Seperti yang kita ketahui, fungsi panah tidak memiliki this
dan arguments
sendiri, jadi f.apply(this, arguments)
mengambil this
dan arguments
dari pembungkusnya.
Jika kita meneruskan fungsi reguler, setTimeout
akan memanggilnya tanpa argumen dan this=window
(dengan asumsi kita berada di browser).
Kita masih bisa this
dengan menggunakan variabel perantara, tapi itu sedikit lebih rumit:
penundaan fungsi(f, ms) { fungsi pengembalian(...args) { biarkan disimpanIni = ini; // simpan ini ke dalam variabel perantara setTimeout(fungsi() { f.apply(savedThis, args); // gunakan di sini }, MS); }; }
Buka solusi dengan pengujian di kotak pasir.
pentingnya: 5
Hasil dari debounce(f, ms)
dekorator adalah wrapper yang menangguhkan panggilan ke f
hingga tidak ada aktivitas selama ms
milidetik (tidak ada panggilan, “periode jeda pakai”), lalu memanggil f
satu kali dengan argumen terbaru.
Dengan kata lain, debounce
seperti sekretaris yang menerima "panggilan telepon", dan menunggu hingga ada ms
milidetik untuk diam. Dan baru kemudian ia mentransfer informasi panggilan terbaru ke "bos" (memanggil f
yang sebenarnya).
Misalnya, kami memiliki fungsi f
dan menggantinya dengan f = debounce(f, 1000)
.
Kemudian jika fungsi yang dibungkus dipanggil pada 0 ms, 200 ms, dan 500 ms, lalu tidak ada panggilan, maka f
sebenarnya hanya akan dipanggil satu kali, pada 1500 ms. Yaitu: setelah periode cooldown 1000ms dari panggilan terakhir.
…Dan itu akan mendapatkan argumen dari panggilan terakhir, panggilan lainnya diabaikan.
Berikut kodenya (menggunakan dekorator debounce dari perpustakaan Lodash):
misalkan f = _.debounce(alert, 1000); f("sebuah"); setTimeout( () => f("b"), 200); setTimeout( () => f("c"), 500); // fungsi yang di-debounce menunggu 1000 md setelah panggilan terakhir dan kemudian berjalan: alert("c")
Sekarang contoh praktisnya. Katakanlah, pengguna mengetik sesuatu, dan kami ingin mengirim permintaan ke server setelah input selesai.
Tidak ada gunanya mengirimkan permintaan untuk setiap karakter yang diketik. Sebaliknya kami ingin menunggu, lalu memproses seluruh hasilnya.
Di browser web, kita dapat mengatur event handler – sebuah fungsi yang dipanggil pada setiap perubahan kolom input. Biasanya, event handler dipanggil sangat sering, untuk setiap kunci yang diketik. Namun jika kita debounce
sebanyak 1000 md, maka itu hanya akan dipanggil sekali, setelah 1000 md setelah input terakhir.
Dalam contoh langsung ini, pawang memasukkan hasilnya ke dalam kotak di bawah ini, cobalah:
Melihat? Input kedua memanggil fungsi debounce, sehingga kontennya diproses setelah 1000 ms dari input terakhir.
Jadi, debounce
adalah cara terbaik untuk memproses rangkaian peristiwa: baik itu rangkaian penekanan tombol, gerakan mouse, atau yang lainnya.
Ia menunggu waktu tertentu setelah panggilan terakhir, dan kemudian menjalankan fungsinya, yang dapat memproses hasilnya.
Tugasnya adalah mengimplementasikan debounce
.
Petunjuk: itu hanya beberapa baris jika Anda memikirkannya :)
Buka kotak pasir dengan tes.
fungsi debounce(fungsi, ms) { biarkan batas waktu; fungsi kembali() { clearTimeout(batas waktu); timeout = setTimeout(() => func.apply(ini, argumen), ms); }; }
Panggilan untuk debounce
akan mengembalikan wrapper. Saat dipanggil, ia menjadwalkan pemanggilan fungsi asli setelah ms
tertentu dan membatalkan batas waktu sebelumnya.
Buka solusi dengan pengujian di kotak pasir.
pentingnya: 5
Buat dekorator “pelambatan” throttle(f, ms)
– yang mengembalikan pembungkus.
Jika dipanggil beberapa kali, panggilan ke f
akan diteruskan maksimal satu kali per ms
milidetik.
Dibandingkan dengan debounce debounce, perilakunya sangat berbeda:
debounce
menjalankan fungsi tersebut satu kali setelah periode "cooldown". Baik untuk mengolah hasil akhir.
throttle
menjalankannya tidak lebih sering daripada waktu ms
yang diberikan. Baik untuk pembaruan rutin yang tidak boleh terlalu sering.
Dengan kata lain, throttle
seperti sekretaris yang menerima panggilan telepon, tetapi mengganggu bos (memanggil f
sebenarnya) tidak lebih dari sekali per ms
milidetik.
Mari kita periksa penerapannya di kehidupan nyata untuk lebih memahami persyaratan tersebut dan melihat dari mana asalnya.
Misalnya, kami ingin melacak pergerakan mouse.
Di browser kita dapat mengatur fungsi untuk dijalankan pada setiap gerakan mouse dan mendapatkan lokasi penunjuk saat mouse bergerak. Selama penggunaan mouse aktif, fungsi ini biasanya berjalan sangat sering, bisa sekitar 100 kali per detik (setiap 10 ms). Kami ingin memperbarui beberapa informasi di halaman web saat penunjuk bergerak.
…Tetapi memperbarui fungsi update()
terlalu berat untuk dilakukan pada setiap gerakan mikro. Juga tidak ada gunanya memperbarui lebih dari sekali per 100 md.
Jadi kita akan menggabungkannya ke dalam dekorator: gunakan throttle(update, 100)
sebagai fungsi untuk dijalankan pada setiap gerakan mouse, bukan update()
yang asli. Dekorator akan sering dipanggil, tetapi meneruskan panggilan ke update()
maksimal satu kali per 100 md.
Secara visual akan terlihat seperti ini:
Untuk pergerakan mouse pertama, varian yang dihias segera meneruskan panggilan untuk update
. Itu penting, pengguna segera melihat reaksi kita terhadap gerakan mereka.
Kemudian saat mouse bergerak, hingga 100ms
tidak terjadi apa-apa. Varian yang dihias mengabaikan panggilan.
Di akhir 100ms
– satu update
lagi terjadi pada koordinat terakhir.
Lalu, akhirnya, tikus itu berhenti di suatu tempat. Varian yang dihias menunggu hingga 100ms
berakhir dan kemudian menjalankan update
dengan koordinat terakhir. Jadi, yang cukup penting, koordinat akhir mouse diproses.
Contoh kode:
fungsi f(a) { konsol.log(a); } // f1000 meneruskan panggilan ke f maksimal satu kali per 1000 ms misalkan f1000 = throttle(f, 1000); f1000(1); // menunjukkan 1 f1000(2); // (pelambatan, 1000 ms belum keluar) f1000(3); // (pelambatan, 1000 ms belum keluar) // ketika waktu habis 1000 ms... // ...output 3, nilai antara 2 diabaikan
Argumen PS dan konteks this
diteruskan ke f1000
harus diteruskan ke f
asli.
Buka kotak pasir dengan tes.
fungsi throttle(fungsi, ms) { biarkan isThrottled = salah, disimpanArgs, disimpanIni; pembungkus fungsi() { jika (dibatasi) {// (2) saveArgs = argumen; disimpanIni = ini; kembali; } isThrottled = benar; func.apply(ini, argumen); // (1) setTimeout(fungsi() { isThrottled = salah; // (3) jika (savedArgs) { wrapper.apply(savedThis, saveArgs); disimpanArgs = disimpanIni = null; } }, MS); } bungkus kembali; }
Panggilan ke throttle(func, ms)
mengembalikan wrapper
.
Selama panggilan pertama, wrapper
hanya menjalankan func
dan menyetel status cooldown ( isThrottled = true
).
Dalam keadaan ini semua panggilan diingat dalam savedArgs/savedThis
. Harap dicatat bahwa konteks dan argumen sama-sama penting dan harus dihafal. Kami membutuhkannya secara bersamaan untuk mereproduksi panggilan tersebut.
Setelah ms
milidetik berlalu, setTimeout
terpicu. Status cooldown dihapus ( isThrottled = false
) dan, jika kita mengabaikan panggilan, wrapper
dieksekusi dengan argumen dan konteks yang terakhir dihafal.
Langkah ke-3 berjalan bukan func
, tapi wrapper
, karena kita tidak hanya perlu mengeksekusi func
, tapi sekali lagi masuk ke status cooldown dan mengatur batas waktu untuk meresetnya.
Buka solusi dengan pengujian di kotak pasir.