15 Desember 2021

Decorators dan forwarding, call/apply

Javascript memberikan fleksibilitas yang istimewa ketika harus berurusan dengan fungsi. Mereka bisa dikirim, digunakan sebagai objek, dan sekarang kita akan melihat bagaimana penerusan/forward panggilan diantara mereka dan mendekorasi/decorate mereka.

Cache transparan

Katakan kita mempunyai sebuah fungsi slow(x) yang mana adalah fungsi berat saat diolah pada CPU, tapi hasil dari fungsi tersebut stabil. Dengan kata lain, untuk x yang sama fungsi itu selalu mengembalikan hasil yang sama.

Jika fungsinya sering dipanggil, kita mungkin ingin meng-cache (mengingat) hasilnya untuk menghindari pembuangan waktu saat kalkulasi-ulang.

Tapi sebagai gantinya daripada menambahkan fungsionalitas lain kedalam slow() kita akan membuat sebuah fungsi pembungkus/wrapper, yang menambahkan cache. Seperti yang akan kita lihat, terdapat beberapa keuntungan untuk melakukan cache.

Ini kodenya, dan penjelasannya:

function slow(x) {
  // disini terdapat task berat yang menggunakan sumberdaya CPU
  alert(`Called with ${x}`);
  return x;
}

function cachingDecorator(func) {
  let cache = new Map();

  return function(x) {
    if (cache.has(x)) {    // jika terdapat kunci "x" pada cache
      return cache.get(x); // baca hasil dari cache
    }

    let result = func(x);  // jika tidak, panggil fungsi

    cache.set(x, result);  // dan cache (ingat) hasilnya
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) telah dimasukan kedalam cache
alert( "Again: " + slow(1) ); // sama seperti baris sebelumnya

alert( slow(2) ); // slow(2) telah dimasukan kedalam cache
alert( "Again: " + slow(2) ); // sama seperti baris sebelumnya

Didalam kode diatas cachingDecorator adalah sebuah decorator/dekorator: sebuah fungsi spesial yang menerima fungsi dan mengubah tingkah lakunya.

Idenya adalah kita bisa memanggil cachingDecorator dari fungsi manapun, dan itu akan mengembalikan pembungkus caching. Itu bagus, karena kita bisa mempunyai banyak fungsi yang dapat menggunakan fitur itu, dan semua yang kita butuhkan adalah menerapkan cachingDecorator kedalam fungsinya.

Dengan memisahkan caching dari kode fungsi utama kita juga bisa tetap membuat kode utama tetap sederhana.

Hasil dari cachingDecorator(func) adalah sebuah ???pembungkus/wrapper???: function(x) yang ???membungkus??? pemanggilan dari func(x) kedalam logika penyimpanan cache.

Dari kode luar, fungsi yang dibungkus slow akan melakukan tetap hal yang sama. Fungsinya hanya akan menambahkan aspek caching kedalam prilakunya.

Untuk meringkaskan, terdapat beberapa keuntungan untuk menggunakan cachingDecorator secara terpisah daripada dimasukan kedalam kode slow itu sendiri:

  • cachingDecorator dapat digunakan lagi. Kita bisa menerapkannya kedalam fungsi lainnnya.
  • Logika dari penyimpanan kedalam cache dipisahkan, itu tidak akan menambah kompleksitas dari slow sendiri.
  • Kita bisa menggunakan beberapa dekorator jika dibutuhkan.

Menggunakan ???func.call??? untuk konteksnya

Dekorator penyimpanan kedalam cache diatas tidak cokok untuk bekerja dengan metode objek.

Contoh, didalam kode dibawah worker.slow() akan berhenti bekerja setelah decoration:

// disini membuat worker.slow menyimpan kedalam cache
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // task yang benar-benar menggunakan banyak sumber daya CPU disini
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// same code as before
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // metode aslinya bekerja

worker.slow = cachingDecorator(worker.slow); // sekarang simpan kedalam cache

alert( worker.slow(2) ); // Whoops! Error: Cannot read property 'someMethod' of undefined

Errornya muncul pada baris (*) yang mencoba untuk mengakses this.someMethod dan gagal. Apakah kamu bisa lihat kenapa?

Alasannya adalah karena pembungkusnya memanggil fungsi aslinya sebagai func(x) pada baris (**). Dan, ketika dipanggil seperti itu, fungsinya mendapatkan this = undefined.

Kita harusnya bisa melihat kasus yang serupa jika kita mencoba menjalankan:

let func = worker.slow;
func(2);

Jadi, pembungkusnya mengirimkan pemanggilan pada metode aslinya, tapi tanpa konteks dari this. Karenanya akan terjadi error.

Coba kita perbaiki.

Terdapat sebuah metode bawaan yang spesial func.call(context, ???args) yang mengijinkan untuk melakukan pemanggilan fungsi menyetel nilai dari this.

Sintaksnya adalah:

func.call(context, arg1, arg2, ...)

Itu akan menjalankan func yang menyediakan argumen pertama sebagai this, dan sisanya sebagai argumen-argumennya.

Untuk menyederhanakannya, kedua pemanggilan dibawah hampir melakukan hal yang serupa:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

Keduanya memanggil func dengan argumen 1, 2, dan 3. Perbedaannya adalah func.call juga menyetel this menjadi obj.

Sebagai sebuah contoh, didalam kode dibawah kita memanggil sayHi didalam konteks pada objek yang berbeda: sayHi.call(user) menjalankan sayHi menyediakan this=user, dan baris selanjutnya menyetel this=admin:

function sayHi() {
  alert(this.name);
}

let user = { name: "John" };
let admin = { name: "Admin" };

// lakukan pemanggilan untuk memberikan objek yang berbeda sebagai "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

Dan disini kita menggunakan call untuk memanggil say dengan konteks dan phrase yang diberikan:

function say(phrase) {
  alert(this.name + ': ' + phrase);
}

let user = { name: "John" };

// user menjadi this, dan "Hello" menjadi argumen pertama
say.call( user, "Hello" ); // John: Hello

Didalam kasus kita, kita bisa menggunakan call didalam pembungkus untuk memberikan konteks kedalam fungsi aslinya:

let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // "this" diberikan dengan benar sekarang
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // sekarang disimpan kedalam cache

alert( worker.slow(2) ); // bekerja
alert( worker.slow(2) ); // bekerja, tidak memanggil yang aslinya (dari cache)

Sekarang semuanya berjalan.

Untuk memperjelas, kita akan melihat lebih dalam bagaimana this diberikan:

  1. Setelah dekorasi dari worker.slow sekarang menjadi pembungkusnya function (x) { ... }.
  2. Jadi ketika worker.slow(2) dieksekusi, pembungkusnya mendapatkan 2 sebagai sebuah argumen dan this=worker (sebuah objek sebelum titik).
  3. Didalam pembungkusnya, asumsikan hasilnya belum disimpan didalam cache, func.call(this, x) diberikan kepada this (=worker) dan argumennya (=2) kepada metode aslinya.

Menjadi multi-argument

Sekarang kita buat cachingDecorator menjadi lebih universal. Sampai sekarang fungsi itu hanya bekerja dengan satu-argumen.

Sekarang bagaimana untuk menyimpan multi-argumen metode worker.slow kedalam cache?

let worker = {
  slow(min, max) {
    return min + max; // asumsikan sebuah fungsi yang sangat berat
  }
};

// harus mengingat pemanggilan dengan argument-yang-sama
worker.slow = cachingDecorator(worker.slow);

Sebelumnya, untuk argumen tunggal x kita bisa dengan melakukan cache.set(x, result) untuk menyimpan result-nya dan cache.get(x) untuk mengambilnya. Tapi kita harus mengingat hasil dari sebuah kombinasi dari argumen-argumen (min, max). Map yang asli mengambil nilai tunggal sebagai kuncinya.

Terdapat beberapa solusi yang bisa dilakukan:

  1. Implementasikan struktur data seperti-map baru yang lebih serba guna dan mengijinkan menggunakan banyak-kunci (atau gunakan third-party).
  2. Gunakan maps bercabang: cache.set(min) akan menjadi sebuah Map yang menyimpan pasangan (max, result). Jadi kita bisa mendapatkan result sebagai cache.get(min).get(max).
  3. Gabungkan kedua nilai menjadi satu. Didalam kasus tertentu kita bisa menggunakna sebuah string "min,max" sebagai kunci Map. Untuk fleksibilitas, kita bisa mengijinkan untuk menyediakan sebuah fungsi hashing untuk dekoratornya, yang mengetahui bagaimana cara membuat nilai tunggal dari banyak nilai.

Untuk kebanykan penggunaan yang praktikal, varian ketiga sudahlah cukup, jadi kita akan menggunakannya.

Juga kita harus memberikan bukan hanya x, tapi seluruh argumen-argumen didalam func.call. Kita panggil ulang didalam sebuah function() kita bisa mendapatkan pseudo-array dari argumennya sebagai arguments, jadi func.call(this, x) harus diganti dengan func.call(this, ...arguments).

Ini adalah cachingDecorator yang lebih powerful:

let worker = {
  slow(min, max) {
    alert(`Called with ${min},${max}`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // bekerja
alert( "Again " + worker.slow(3, 5) ); // sama (dari cache)

Sekarang itu bekerja dengan berapapun jumlah argumen (walaupun fungsi hash harusnya disesuaikan untuk menerima argumen dengan jumlah berapapun. Cara yang menarik untuk menangani ini akan dijelaskan dibawah).

Terdapat dua perubahan:

  • Didalam baris (*) memanggil hash untuk membuat sebuah kunci tunggal dari arguments. Disini kita menggunakan fungsi ???joining??? yang sederhana yang mengubah argument (3, 5) menjadi kunci "3,5". Kasus kompleks yang lain mungkin membutuhkan fungsi-fungsi hashing lainnya.
  • Lalu (**) menggunakan func.call(this, ...arguments) untuk memberikan konteks dan seluruh argumen yang pembungkusnya dapatkan (tidak hanya yang pertama) dari fungsi aslinya.

func.apply

Daripada func.call(this, ...arguments) kita bisa gunakan func.apply(this, arguments).

Sintaks dari metode bawaannya func.apply adalah:

func.apply(context, args)

Kode diatas menjalankan func dan menyetel this=context dan menggunakan objek yang seperti array args sebagai daftar dari argumen-argumen.

Perbedaan sintaks antara call dan apply adalah bahwa call mengharapkan sebuah daftar dari argumen-argumen, sementara apply menerima objek yang seperti-array didalamnya.

Jadi kedua pemanggilan dibawah hampir sama:

func.call(context, ...args); // mengirimkan sebuah array sebagai daftar dengan sintaks spread
func.apply(context, args);   // sama seperti pemanggilan call

Hanya terdapat perbedaan yang tipis:

  • Sintaks spread ... mengijinkan untuk mengirimkan iterable args sebagai list untuk call.
  • apply hanya menerima args yang seperti-array.

Jadi, dimana kita mengharapkan sebuah iterasi, gunakan call, dan dimana kita menggunakan seperti-array, gunakan apply.

Dan untuk objek yang bisa diiterasi dan seperti-array, seperti array yang asli, kita bisa gunakan keduanya, tapi apply akan lebih cepat, karena kebanyakan mesin Javascript secara internal mengoptimasi apply lebih baik.

Mengirimkan seluruh argumen bersamaan dengan konteks ke fungsi lainnya dipanggil dengan call forwarding.

Ini adalah contoh paling sederhana dari call forwarding:

let wrapper = function() {
  return func.apply(this, arguments);
};

Ketika sebuah kode eksternal memanggil wrapper yang seperti diatas, pemanggilan itu tidak bisa dibedakan dengan pemanggilan dari fungsi asli func.

Meminjam sebuah metode

Sekarang kita buat satu perubahan minor didalam fungsi hashing:

function hash(args) {
  return args[0] + ',' + args[1];
}

Seperti yang sekarang, fungsi diatas hanya akan bekerja dengan dua argumen. Fungsi diatas akan lebih baik jika dapat menerima berapapun jumlah dari args.

Solusi naturalnya harusnya dengan menggunakan metode arr.join:

function hash(args) {
  return args.join();
}

???Sayangnya, hal diatas tidak akan bekerja. karena kita memanggil hash(arguments), dan objek arguments adalah hal yang bisa diiterasi dan hal yang seperti array, tapi bukanlah array asli.

jadi memanggil join tentu tidak akan bekerja, seperti yang bisa kita lihat dibawah:

function hash() {
  alert( arguments.join() ); // Error: arguments.join is not a function
}

hash(1, 2);

Tetap, terdapat sebuah cara yang mudah untuk menggunakan array join:

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);

Caranya bernama method borrowing.

Kita menggunakan (borrow/meminjam) metode join dari array biasa ([].join) dan gunakan [].join.call untuk menjalankannya didalam konteks dari arguments.

Kenapa hal itu bisa bekerja?

Itu karena algoritma internal dari metode native arr.join(glue) sangatlah sederhana.

Diambil dari spesifikasi hampir ???as-is(apa adanya)???:

  1. Biarkan glue menjadi argumen pertama atau, jika tidak ada argumen, maka sebuah koma ",".
  2. Biarkan result menjadi sebuah string kosong.
  3. Masukan this[0] kedalam result.
  4. Masukan glue dan this[1].
  5. Masukan glue dan this[2].
  6. ???lakukan terus sampai item dari this.length ditempel.
  7. Kembalikan result.

Jadi, secara tekniks itu akan menggunakan this dan menggabungkan this[0], this[1] ???lainnya bersama. Itu secara sengaja ditulis dengan cara yang mengijinkan hal yang seperti array this (bukan kebetulan, banyak metode lainnya mengikuti cara ini). Itulah kenapa hal ini bekerja juga dengan this=arguments.

Decorators and properti fungsi

Secara umum mengganti sebuah fungsi atau metode dengan yang telah diubah adalah hal yang aman, kecuali untuk satu hal kecil. Jika fungsi aslinya memiliki properti didalamnya func.calledCount atau apapun, maka fungsi yang telah diubah tidak akan memilikinya. Karena itu adalah sebuah pembungkus. Jadi haruslah hati-hati saat menggunakannya.

Contoh, didalam contoh diatas jika fungsi slow memiliki properti apapun didalamnya, maka cachingDecorator(slow) adalah sebuah pembungkus tanpa properti itu.

Beberapa dekorator mungkin menyediakan propertinya sendiri. Misalnya sebuah dekorator mungkin menghitung berapa kali fungsinya dipanggil dan berapa lama pemanggilannya, dan mengetahui informasi ini lewat pembungkus properti.

Terdapat sebuah cara untuk membuat dekorator yang tetap menyimpan akses kepada properti fungsi, tapi hal ini membutuhkan objek spesial Proxy untuk membungkus fungsinya. Kita akan pelajari nanti dalam artikel Proxy and Reflect.

Ringkasan

Dekorator adalah sebuah pembungkus fungsi yang mengubah prilaku fungsi tersebut. Pekerjaan utamanya tetap untuk membawa fungsinya.

Dekorator bisa dilihat sebagai ???fitur??? atau ???aspek??? yang bisa ditambahkan kedalam fungsi. Kita bisa menambahkan satu atau banyak. Dan semuanya tanpa mengubah kode dari fungsinya sendiri.

Untuk mengimplementasikan cachingDecorator, kita telah mempelajari metode:

call forwarding biasanya digunakan dengan apply:

let wrapper = function() {
  return original.apply(this, arguments);
};

Kita juga melihat contoh dari method borrowing ketika kita mengambil metode dari sebuah objek dan call/memanggilnya didalam konteks dari objek lain. Hal itu cukup umum untuk mengambil metode array dan mengaplikasikannya kepada arguments. Alternatif lainnya adalah untuk menggukanan objek parameter rest yang mana adalah sebuah array asli.

Terdapat beberapa dekorator yang tersedia. Pecahkan seluruh task untuk mengetahui seberapa paham kamu tentang dekorator tersebut didalam bab ini.

Tugas

Buatlah sebuah dekorator spy(func) yang harus mengembalikan pembungkus yang menyimpan semua pemanggilan kepada fungsinya didalam propertinya sendiri bernama calls.

Setiap pemanggilan disimpan sebagai sebuah array dari argumen.

Contoh:

function work(a, b) {
  alert( a + b ); // bayangkan work adalah sebuah fungsi yang panjang
}

work = spy(work);

work(1, 2); // 3
work(4, 5); // 9

for (let args of work.calls) {
  alert( 'call:' + args.join() ); // "call:1,2", "call:4,5"
}

Catatan. Dekoratornya harus berguna untuk unit-testing. Bentuk lanjutannya adalah sinon.spy didalam librari Sinon.JS.

Buka sandbox dengan tes.

Pembungkus yang dikembalikan oleh spy(f) harus menyimpan semua argumen dan lalu menggunakan f.apply untuk melanjutkan pemanggilannya.

function spy(func) {

  function wrapper(...args) {
    // gunakan ...args daripada argumen untuk menyimpan "array asli" didalam wrapper.calls
    wrapper.calls.push(args);
    return func.apply(this, args);
  }

  wrapper.calls = [];

  return wrapper;
}

Buka solusi dengan tes di sandbox.

Buatlah sebuah dekorator delay(f, ms) yang menunda setiap pemanggilan dari f selama ms milidetik.

Contoh:

function f(x) {
  alert(x);
}

// create wrappers
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);

f1000("test"); // tampilkan "test" setelah 1000ms
f1500("test"); // tampilkan "test" setelah 1500ms

Dengan kata lain, delay(f, ms) mengembalikan sebuah "varian dari f yang telah ditunda selama ms".

Didalam kode diatas, f adalah sebuah fungsi dari sebuah argumen tunggal, tapi solusimu harus bisa melewati seluruh argumen dan konteks dari this.

Buka sandbox dengan tes.

Solusi:

function delay(f, ms) {

  return function() {
    setTimeout(() => f.apply(this, arguments), ms);
  };

}

let f1000 = delay(alert, 1000);

f1000("test"); // tampilkan "test" setelah 1000ms

Perhatikan bagaimana fungsi arrow digunakan disini. Seperti yang kita tahu, fungsi panah tidak memiliki this dan argumennya sendiri, jadi f.apply(this, arguments) akan mengambil this dan arguments dari pembungkusnya.

Jika kita memasukan fungsi yang biasa, setTimeout akan memanggil fungsinya tanpa argumen dan this=window (asumsikan kita berada didalam peramban).

Kita masih bisa memberikan this yang benar dengan menggunakan variabel tambahan, tapi kodenya akan sedikit menjadi lebih rumit:

function delay(f, ms) {

  return function(...args) {
    let savedThis = this; // simpan this kedalam variabel tambahan
    setTimeout(function() {
      f.apply(savedThis, args); // gunakan disini
    }, ms);
  };

}

Buka solusi dengan tes di sandbox.

Hasil dari dekorator debounce(f, ms) adalah sebuah pembungkus yang menghentikan pemanggilan f selama ms milidetik dari ketidakaktifan (tidak ada pemanggilan, ???masa menunggu???), lalu memanggil f sekali dengan argumen terakhir.

Dengan kata lain, debounce seperti seorang sekertaris yang menerima ???telefon???, dan menunggu selama ms milidetik dari ketidakaktifan. Dan lalu menyampaikan pemanggilan terakhir kepada ???boss??? (melakukan pemanggilan f).

Contoh, jika kita mempunyai sebuah fungsi f dan lalu memasukan f = debounce(f, 1000).

Maka jika fungsi pembungkus dipanggil pada 0ms, 200ms, dan 500ms, dan lalu tidak ada pemanggilan lainnya, maka fungsi f akan dipanggil sekali, pada 1500ms. Itulah: setelah beberapa saat fungsi tidak dipanggil maka fungsinya akan benar-benar dipanggil dengan rentang waktu 1000ms setelah pemanggilan terakhir.

???Dan itu akan mendapatkan argumen dari pemanggilan yang paling terakhir, pemanggilan lainnya akan diabaikan.

Ini adalah kodenya (digunakan untuk dekorator debounce dari Lodash library):

let f = _.debounce(alert, 1000);

f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// fungsi debounce menunggu 1000ms setelah pemanggilan terakhir dan lalu menjalankan: alert("c")

Sekarang contoh yang lebih praktikal. Katakan, penggunakan mengetik sesuatu, dan kita ingin mengirim request kepada server ketika pengguna telah selesai mengetik.

Pada hal ini, sangat tidak berguna untuk mengirim request kepada server untuk setiap huruf yang diketik. Lagipula kita ingin menunggu, dan lalu memproses hasil ketikan pengguna.

Didalam peramban, kita bisa menyetel sebuah event handler(penangan event) ??? sebuah fungsi yang dipanggi untuk setiap perubahan pada kotak inputan, sebuah penangan event dipanggil sangat sering untuk setiap huruf yang diketik. Tapi jika kita ingin mendebouncenya selama 1000ms, maka fungsinya akan dipanggil sekali, 1000ms setelah penginputan huruf terakhir.

Didalam contoh ini, handlernya memasukan hasilnya kedalam kotak dibawah, cobalah:

Lihat? inputan kedua memanggil fungsi debounce, jadi kontennya diproses setelah 1000ms dari inputan terakhir.

Jadi, debounce adalah cara terbaik untuk memproses event yang terjadi berurutan: bisa tombol yang dipencet berulang-ulang, pergerakan mouse atau lainnya.

Fungsinya akan menunggu hingga pemanggilan terakhir, dan lalu menjalankan fungsi aslinya, lalu hasilnya akan diolah.

Tugasnya adalah untuk mengimplementasikan dekorator debounce.

Petunjuk: jika kamu perhatikan, perubahan fungsinya hanya dengan menambahkan beberapa baris :)

Buka sandbox dengan tes.

function debounce(func, ms) {
  let timeout;
  return function() {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, arguments), ms);
  };
}

Pemanggilan kepada debounce mengembalikan sebuah pembungkus. Ketika dipanggil, debounce akan menunggu lalu memanggil fungsi aslinya setelah ms milidetik dan membatal kan timeout sebelumnya.

Buka solusi dengan tes di sandbox.

Buatlah sebuah dekorator ???penutup??? throttle(f, ms) ??? yang mengembalikan sebuah pembungkus.

Ketika fungsinya dipanggil beberapa kali, fungsinya akan melakukan pemanggilan kepada f maksimal sekali per ms milidetik.

Perbedaannya dengan dekorator debounce adalah keduanya benar-benar dekorator berbeda:

  • debounce menjalankan fungsinya sekali setelah masa ???tidak aktif???. Bagus untuk memproses hasil akhir.
  • throttle menjalankan fungsinya tidak lebih banyak dari waktu ms yang diberikan. Bagus untuk update tersusun yang tidak terlalu sering dipanggil.

Dengan kata lain, throttle seperti seorang sekertaris yang menerima panggilan telefon, tapi menggangu bos nya (memanggil fungsi f asli) tidak lebih sering dari sekali per ms milidetik.

Ayo kita lihat contoh pengaplikasiannya langsung untuk mengerti lebih dalam tentang kebutuhannya dan dimana digunakannya.

Contoh, kita ingin mengetahui posisis dari pergerakan mouse.

Didalam peramban kita bisa menyetel sebuah fungsi yang berjalan untuk setiap pergerakan mouse dan mendapatkan lokasi pointernya selama mouse-nya bergerak. Selama mouse-nya bergerak terus-menerus, fungsi ini biasanya berjalan sangat sering, bisa menjadi seperti 100 kali per-detik (setiap 10ms). Kota ingin meng-update beberapa informasi didalam halaman webnya ketika pointernya bergerak.

???Akan tetapi meng-update fungsi update() terlalu berat dilakukan untuk dijalankan terus menerus mengikuti pergerakan mouse. Tidak ada alasan yang bagus untuk meng-update lebih sering daripada sekali per 100ms.

Jadi kita akan membungkusnya dengan dekorator: gunakan throttle(update, 100) sebagai fungsi untuk berjalan setiap pergerakan mouse daripada secara langsung menggunakan update(). Dekoratornya akan sering dipanggil, tapi untuk pemanggilan kepada update akan dilakukan maksimal sekali per 100ms.

Secara visual, langkah-langkahnya akan seperti ini:

  1. Untuk pergerakan mouse pertama dekoratornya langsung memanggil fungsi update. Itu penting, untuk penggunanya melihat reaksi sistemnya ketika mereka baru saja bergerak.
  2. Lalu selama mousenya bergerak, sampai 100ms tidak akan terjadi apa-apa. Dekoratornya akan mengabaikan pemanggilannya.
  3. Setelah melewati 100ms ??? satu pemanggilan fungsi updateterjadi dengan kondisi paling terakhir.
  4. Lalu, pada akhirnya, mousenya berhenti disuatu tempat. Dekoratornya menunggu sampai melewati 100ms dan lalu menjalankan update dengan kondisi terakhir. Jadi, cukup penting, pergerakan mouse terakhir akan diproses.

Contoh kode:

function f(a) {
  console.log(a);
}

// f1000 mengirimkan pemanggilan kepada f maksimal sekali per 1000ms
let f1000 = throttle(f, 1000);

f1000(1); // tampilkan 1
f1000(2); // (ditahan, belum melewati 1000ms)
f1000(3); // (ditahan, belum melewati 1000ms)

// ketika 1000ms terlewati...
// ...menampilkan 3, nilai sebelum tiga yaitu 2 akan diabaikan

Catatan. Argumen dan konteks this yang dikirimkan kepada f1000 harus bisa dikirimkan kepada fungsi asli f.

Buka sandbox dengan tes.

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) { // (2)
      savedArgs = arguments;
      savedThis = this;
      return;
    }
    isThrottled = true;

    func.apply(this, arguments); // (1)

    setTimeout(function() {
      isThrottled = false; // (3)
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

Pemanggilan kepada throttle(func, ms) mengembalikan wrapper.

  1. Selama pemanggilan pertama, wrappernya hanya menjalankan func dan menyetel kondisi tidak aktif (isThrottled = true).
  2. Didalam kondisi ini semua pemanggilan akan diingat/disimpan didalam savedArgs/savedThis. Ingat baik-baik bahwa konteks dan argumennya sama-sama penting dan harus diingat/disimpan. Kita akan membutuhkannya untuk membuat panggilannya.
  3. Setelah ms milidetik berlalu, setTimeout akan berjalan. Kondisi tidak aktif dihilangkan (isThrottled = false) dan, jika kita memiliki daftar panggilan yang diabaikan, wrapper akan dieksekusi dengan argumen dan konteks yang terakhir diingat/disimpan.

Langkah ketika yang berjalan bukanlah func, tapi wrapper, karena kita tidak hanya perlu mengeksekusi func, tapi sekali-lagi kita memasuki kondisi tidak aktif dan perlu menyetel ulang timeout.

Buka solusi dengan tes di sandbox.

Peta tutorial