31 Ocak 2022

Dekoratörler ve iletilme, call/apply

JavaScript fonksiyonlar ile uğraşırken inanılmaz derecede esneklik sağlamaktadır. Fonksiyonlar başka fonksiyonlara gönderilebilir, obje olarak kullanılabilir. Şimdi ise bunların nasıl iletileceği ve nasıl dekore edileceğinden bahsedilecektir;

Saydam Saklama

Diyelim ki slow(x) diye yoğun işlemci gücüne ihtiyaç duyan bir fonksiyonunuz olsun, buna rağmen sonucları beklediğiniz şekilde vermekte.

Eğer bu fonksiyon sık sık çağırılıyor ise farklı x’ler için sonucu saklamak bizi tekrar hesaplamadan kurtarabilir.

Fakat bunu slow() fonksiyonunun içine yazmak yerine yeni bir wrapper yazmak daha iyi olacaktır. Göreceğiniz üzere size oldukça fazla yardımı olacaktır.

Kod aşağıdaki gibidir:

function slow(x) {
  // burada baya yoğun işlemci gücüne ihtiyaç duyan işler yapılmaktadır.
  alert(`${x} ile çağırıldı`);
  return x;
}

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

  return function(x) {
    if (cache.has(x)) { // eğer sonuç map içerisinde ise
      return cache.get(x); // değeri gönder
    }

    let result = func(x); // aksi halde hesap yap

    cache.set(x, result); // sonra sonucu sakla
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) saklandı
alert( "Tekrar: " + slow(1) ); // aynısı döndü

alert( slow(2) ); // slow(2) saklandı
alert( "Tekrar: " + slow(2) ); // bir önceki ile aynısı döndü.

Yuarkıdaki kodda cachingDecorator bir dekoratör’dür: Diğer bir fonksiyonu alan ve bunun davranışını değiştiren özel bir fonksiyon.

Aslında her bir fonksiyon için cachingDecorator çağrılabilir ve o da saklama mekanizmasını kullanır. Harika, bu şekilde ihtiyacı olacak birçok fonksiyonumuz olabilir. Tek yapmamız gereken bu fonksiyonlara cachingDecorator uygulamak.

Saklama olayını ana fonksiyonldan ayırarak aslında daha temiz bir yapıya da geçmiş olduk.

Detayına inmeye başlayabiliriz.

cachingDecorator(func) bir çeşit “wrapper(saklayıcı)”'dır. Bu işlem func(x) i “saklama” işine yarar.

Gördüğünüz gibi, saklayıcı func(x)'ı olduğu gibi dönderir. Saklayıcının dışındaki slow olan fonksiyon hala aynı şekilde çalışır. Aslında davranışın üstüne sadece saklama(caching) mekanizması gelmiştir.

Özetlersek, ayrı bir cachingDecorator kullanmanın faydaları şu şekildedir:

  • cachingDecorator tekrar kullanılabilir. Başka bir fonksiyona da uygulanabilir.
  • Saklama(caching) mantığı ayrılmıştır böylece slow kodun içine daha fazla kod yazıp karışması önlenmiştir.
  • İhtiyaç halinde birden fazla dekoratör birlikte kullanılabilir.

Kaynak için “func.all” kullanmak.

Yukarıda bahsettiğimiz saklama dekoratörü obje metodları ile çalışmak için müsait değildir.

Örneğin aşağıdaki kodda user.format() dekorasyondan sonra çalışmayı durdurur:

//  worker.slow sakla yapılacaktır.
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // burada çok zorlu bir görev olabilir.
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// eskisiyle aynı kod
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) ); // orjinal metod çalışmakta

worker.slow = cachingDecorator(worker.slow); // şimdi saklamaya alındı.

alert( worker.slow(2) ); // Whoops! Error: Özellik okunamamaktadır. `someMethod` tanımsız.

(*) satırında hata olur this.someMethod'a erişmeye çalışır fakat başırılı olamaz. Nedeni ne olabilir ?

Sebebi (**) satırında orjinal func(x) çağırılmıştır. Bu şekilde çağırıldığında, fonksiyon this = undefined alır.

Aşağıdaki kod çalıştırılırsa da aynısı görülebilir:

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

Saklayıcı çağrıyı gerçek çalışacak metoda gönderir. Fakat this olmadığından dolayı hata alır.

Bunu düzeltmek için.

Özel bir metod bulunmaktadır func.call(context, …args) this'i belirterek doğrudan fonksiyonu çağırmaya yarar.

Yazımı aşağıdaki gibidir:

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

İlk argüman this'dir diğerleri ise fonksiyon için gerekli argümanlardır.

Kullanımı şu şekildedir:

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

Her ikisi de aslında func fonksiyonlarını 1, 2, 3 argümanları ile çağırır tek fark func.call fonksiyonunda thisde gönderilir.

Örneğin, aşağıdaki kod sayHi metodunu iki farklı objeye değer atayarak çağırır. Birinci satırda this=user ikinci satırda ise this=admin değeri atanarak bu çağrı gerçekleştirilir.

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

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

// farklı objeler "this" objesi olarak gönderilebilir.
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

Burada say metodunu çağırarak ne söyleneceğini gönderiyoruz:

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

let user = { name: "John" };

// user `this` olmakta ve `phrase` ilk argüman olmaktadır.
say.call( user, "Hello" ); // John: Hello

Bizim durumumuzda saklayıcı içinde call kullanarak içeriği orijinal fonksiyona aktarabiliriz:

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

  slow(x) {
    alert(x + "ile çağırıldı");
    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" is passed correctly now
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // now make it caching

alert( worker.slow(2) ); // çalışır
alert( worker.slow(2) ); // orjinali değilde hafızadaki çalışır.

Şimdi her şey beklendiği gibi çalışıyor.

Daha açıklayıcı olması için this'in nasıl ilerlediğini inceleyebiliriz:

  1. Dekorasyon işleminden sonra worker.slow artık function(x){ ...} halini almıştır.
  2. Öyleyse worker.slow(2) çalıştırıldığında saklayıcı 2 ve this=worker ( noktadan önceki obje ) argümanlarını alır.
  3. Saklayıcı(wrapper) içinde sonucun henüz belleğe alınmadığını varsayarsak func.call(this,x) o anki this (=worker) ve (’=2`) değerini orjinal metoda gönderir.

“func.apply” ile çoklu argüman kullanımı

cachingDecorator daha evrensel yapmak için ne değişiklikler yapmalıdır?

let worker = {
  slow(min, max) {
    return min + max; // CPU'ya çok yük bindiren bir işlem.
  }
};

// aynı argüman ile çağırılmalıdır.
worker.slow = cachingDecorator(worker.slow);

Burada çözmemiz gereken iki problem bul

İlki min ve max değerlerinin bu bellek haritasında anahtar olarak nasıl tutulacağı. Önceki konuda tek x argümanı için cache.set(x,result) şeklinde sonucu belleğe kaydetmiş ve sonra cache.get(x) şeklinde almıştık. Fakat şimdi sonucu argümanların birleşimi şeklinde hatırlamak gerekmektedir. Normalde Map anahtarı tek değer olarak almaktadır.

Bu sorunun çözümü için bazı çözümler şu şekildedir:

  1. Map-benzeri bir yapı kurarak birkaç anahtarı kullanabilen bir veri yapısı oluşturmak.
  2. İç içe map kullanarak; Örneğin cache.set(min) aslında (max, result)'ı tutmaktadır. Böylece result cache.get(min).get(max) şeklinde alınabilir.
  3. İki değeri teke indirerek. Bizim durumumuzda bu "min,max" şeklinde bir karakter dizisini Map'in anahtarı yapmak olabilir. Ayrıca hashing fonksiyonu’u dekoratöre sağlayabiliriz. Bu fonksiyon da birçok değerden bir değer yapabilir.

Çoğu uygulama için 3. çözüm yeterlidir. Biz de bu çözüm ile devam edeceğiz.

İkinci görev ise fonksiyona birden fazla argümanın nasıl gönderileceğidir. Şu anda saklayıcı fonksiyona function(x) şeklinde tek argüman gönderilmektedir. Bu da func.call(this,x) şeklinde uygulanır.

Burada kullanılacak diğer metod func.apply’dır.

Yazımı:

func.apply(context, args)

Bu functhis=context ve args için dizi benzeri bir argüman dizisi ile çalıştırır.

Örneğin aşağıdaki iki çağrı tamamen aynıdır.

func(1, 2, 3);
func.apply(context, [1, 2, 3])

Her ikisi de func1,2,3argümanları ile çalıştırır. Fakat apply ayrıca this=context'i ayarlar.

function say(time, phrase) {
  alert(`[${time}] ${this.name}: ${phrase}`);
}

let user = { name: "John" };

let messageData = ['10:00', 'Hello']; // time, phrase'e dönüşür.

// this = user olur , messageData liste olarak (time,phrase) şeklinde gönderilir.
say.apply(user, messageData); // [10:00] John: Hello (this=user)

call argüman listesi beklerken apply dizi benzeri bir obje ile onları alır.

Yayma operatörü Gerisi parametreleri ve yayma operatörleri konusunda ... yayma operatörünün ne iş yaptığını işlemiştik. Dizilerin argüman listesi şeklinde gönderilebileceğinden bahsemiştik. Öyleyse call ile bunu kullanırsak neredeyse apply'ın işlevini görebiliriz.

Aşağıdaki iki çağrı birbirinin aynısıdır:

let args = [1, 2, 3];

func.call(context, ...args); // dizileri yayma operatörü ile liste şeklinde gönderir.
func.apply(context, args);   // aynısını apply ile yapar.

İşleme daha yakından bakılacak olursa call ile apply arasında oldukça küçük bir fark vardır.

  • Yayma operatörü ... list gibi döngülenebilir argümanları call edilmek üzere iletebilir.
  • apply ise sadece dizi-benzeri args alır.

Öyleyse bu çağrılar birbirinin tamamlayıcısıdır. Döngülenebilir beklediğimizde call, dizi-benzeri beklediğimizde ise apply çalışır.

Eğer args hem döngülenebilir bende dizi ise teknik olarak ikisini de kullanabiliriz, fakat apply muhtemelen daha hızlı olacaktır. Çünkü tek bir işlemden oluşur. Çoğu JavaScript motoru bir kaç call + spread kullanmaktan daha iyi şekilde optimizasyon yapar.

Apply’ın en çok çağrıyı diğer fonksiyona iletirken işe yarar:

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

Buna çağrı iletme denir. Saklayıcı sahip olduğu her şeyi iletir: this ile argümanları anotherFunction'a iletir ve sonucunu döner.

Böyle bir saklayıcı kod çağırıldığında içerideki orjinal fonksiyon çağıran tarafından ayrıştırılamaz.

Şimdi bunları daha güçlü cachingDecoratır'da işleyelim:

let worker = {
  slow(min, max) {
    alert(`${min},${max} ile çağırıldı`);
    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.apply(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) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)

Şimdi saklayıcı(wrapper) birçok argüman ile çalışabilir.

İki tane değişiklik oldu:

  • (*) satırında bir has ile argümanlardan tek bir anahtar meydana getirildi. Bunun için basit “birleştirme” fonksiyonu kullanılmıştır. (3,5) "3,5" şekline getirildi. Tabi başka hash fonksiyonları için daha karmaşık bir yapı gerekebilir.
  • (**) satırında ise func.apply ile hem kaynak ( this ) hem de saklayıcı argümanları (ne kadar olduğu önemli değil) orjinal fonksiyona iletilmiştir.

Metod Ödünç Alma

Hash fonksiyonunda biraz değişiklik yapalım:

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

Bundan sonra bu fonksiyon sadece iki argümanla çalışacak. Bunun yerine belirsiz sayıdaki argümanla çalışsa daha iyi olur.

Bunu kullanmanın doğal yolu arr.join metodudur:

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

… Malesef bu çalışmaz. Çünkü hash(argümanlar) çağırılmakta ve arguments objei hem döngülenebilir hemde dizi-benzeri olduğundan, fakat gerçek dizi olmadığından çalışmaz.

Öyleyse join bu durumda çağırılamaz, örneğin:

function hash() {
  alert( arguments.join() ); // Error: arguments.join fonksiyon değil.

}

hash(1, 2);

Fakat yine de dizi birleştirme kullanmanın kolay bir yolu vardır.

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

hash(1, 2);

Bu cambazlığa metod ödünç alma denir.

Normal diziden [].join join ödünç alınır. Sonrasında arguments contexi ile çağrı yapılır [].join.call

Peki neden çalışır?

Çünkü gerçek dizinin join metodu oldukça basittir.

Tanımından “olduğu” gibi alındığında işlem şu şekildedir:

  1. yapistir ilk argüman olsun, eğer argüman yoksa "," ilk argüman olsun.
  2. sonuc boş karakter dizisi olsun
  3. this[0]'ı sonuca ekle.
  4. yapistir ve this[1]'i ekle
  5. yapistir ve this[2]'i ekle
  6. this.length'e kadarki tüm elemanlar yapıştırılana kadar ekle.
  7. sonuc dön
  8. Return result.

Teknik olarak this'i alır ve this[0], this[1] …vs. şeklinde birleştirir. Bu şekilde yazılarak tüm dizi-gibi'lerin this şeklinde çalışmasını sağlar. Bundan dolayı this=arguments şeklinde de çalışır.

Özet

Decoratör fonksiyonun davranışını değiştiren saklayıcılardır(wrapper). İş hala ana fonksiyonda yapılır.

Genelde gerçek fonksiyonu dekoratör ile değiştirmek güvenlidir, bir olay haricinde. Eğer orjinal fonksiyon func.calledCount gibi bir özelliğe sahipse, dekoratör bunu sağlamayacaktır çünkü bu bir saklayıcıdır. Bundan dolayı kullanırken dikkatli olunmalıdır. Bazı dekoratörler kendine ait özellikler tutarlar.

cachingDecorator kullanmak için bazı metodlar denedik:

çağrı iletme genelde apply ile gerçekleştirilir:

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

Ayrıca metod ödünç alma’yı da gördük. Bir metotdan obje alındığında ve bu diğer objenin kaynağında(context) çağırıldığında gerçekleşir. Dizi metodlarını alıp argümanlara uygulama çokça kullanılır. Bunun alternatifi geriye kalan parametre objelerini kullanmaktır. Bunlar gerçek dizilerdir.

Araştırırsanız birçok dekoratör görebilirsiniz. Bu bölümdeki görevleri çözerekte kendinizi geliştirebilirsiniz.

Görevler

önem: 5

spy(func) adında bir dekoratör yazın ve bu fonksiyona gelen tüm çağrıları calls özelliğine kaydetsin.

Çağrıların tamamı argüman dizisi olarak kaydedilsin.

Örneğin:

function work(a, b) {
  alert( a + b ); // work keyfi bir metod veya fonksiyondur.
}

work = spy(work);

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

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

Not: Birim testleri için dekoratörler oldukça yararlıdır. Bunun daha gelişmiş bir versiyonu sinon.spy olarak Sinon.JS kütüphanesinde bulunmaktadır.

Testler ile korunaklı olan aç.

Burada calls.push(args) ile f.apply(this,args)'a gönderilen tüm çağrıların argümanları tutulabilir.

function spy(func) {

  function wrapper(...args) {
    wrapper.calls.push(args);
    return func.apply(this, arguments);
  }

  wrapper.calls = [];

  return wrapper;
}

Çözümü testler korunaklı alanda olacak şekilde aç.

önem: 5

delay(f,ms) adında bir dekoratör yazın ve bu dekoratör her f in çağırılmasında ms milisaniye kadar geciktirilsin.

Örneğin:

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

// Saklayıcı
let f1000 = delay(f, 1000);
let f1500 = delay(f, 1500);

f1000("test"); // "test" yazısı 1000ms sonra gelir.
f1500("test"); // "test" yazısı 1500ms sonra gelir.

Diğer bir deyişle delay(f, ms) f fonksiyonunun ms kadar geciktirilmiş versiyonunu döner.

Yukarıdaki kodda, f tek argümanlı bir fonksiyondur. Fakat sizin çözümünüzde bu tüm argümanların ve kaynağın this şeklinde fonksiyona iletilmesi gerekmektedir.

Testler ile korunaklı olan aç.

Çözüm:

function delay(f, ms) {

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

}

Yukarıda ok fonksiyonunun nasıl kullanıldığına dikkat edin. Bildiğiniz gibi, ok fonksiyonlarında this ve arguments bulunmaz, bunun için f.apply(this, arguments) , this ve arguments'ı saklayıcıdan(wrapper) alır.

Eğer sıradan bir fonksiyon paslarsanız, setTimeout bunu argümansız this=window ( tarayıcıda ) olacak şekilde çağırır, bundan dolayı saklayıcıdan bu değerleri iletebilmek için biraz daha kod yazmalıyız:

function delay(f, ms) {

  // `this` ve diğer argümanların setTimeout içerisindeki saklayıcıdan iletilmesini sağlar.
  return function(...args) {
    let savedThis = this;
    setTimeout(function() {
      f.apply(savedThis, args);
    }, ms);
  };

}
function delay(f, ms) {

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

};

Çözümü testler korunaklı alanda olacak şekilde aç.

önem: 5

debounce(f, ms) dekoratörü f çağrısına ms zarfında en fazla bir defa izin vermelidir.

Diğer bir deyişle “debounced” fonksiyonu çağırıldığında, ms'e yakın diğer tüm özellikler görmezden gelinecektir.

Örneğin:

let f = debounce(alert, 1000);

f(1); // Anında çalışacak
f(2); // görmezden gelinecek

setTimeout( () => f(3), 100); // görmezden gelinecek ( 100 ms'de çalıştığından )
setTimeout( () => f(4), 1100); // çalışır
setTimeout( () => f(5), 1500); // görmezden gelinecek çünkü son çağrıdan itibaren 1000ms'den az bir zaman geçmiştir.

Pratikte geri sektiren dekoratör değişmeyeceğini bildiğimiz bir zaman süresince aynı kaynağı tekrar çağırmamak için kullanılabilir.

Testler ile korunaklı olan aç.

function debounce(f, ms) {

  let isCooldown = false;

  return function() {
    if (isCooldown) return;

    f.apply(this, arguments);

    isCooldown = true;

    setTimeout(() => isCooldown = false, ms);
  };

}

debounce çağrısı bir saklayıcı döner. İki durum söz konusudur:

  • isCooldown = false – çalışmaya hazır.
  • isCooldown = true – timeout’u bekliyor…

İlk çağırıldığında isCooldown false döner, bundan dolayı çalışır ve isCooldown true olur.

isCooldown true iken diğer çağrılar görmezden gelinir. setTimeout belirlenen vakit geçtikten sonra tekrar isCooldown'u false’a çevirir.

function debounce(f, ms) {

  let isCooldown = false;

  return function () {
    if (isCooldown) return;

    f.apply(this, arguments);

    isCooldown = true;

    setTimeout(() => isCooldown = false, ms);
  };

}

Çözümü testler korunaklı alanda olacak şekilde aç.

önem: 5

“Sıkma” dekoratörü throttle(f,ms) oluşturun ve bu bir kapsayıcı döndersin, bu kapsayıcı çağrıyı f'e iletsin ve bu çağrıyı belirtilen ms içerisinde sadece bir defa yapabilsin. Geri kalan “cooldown” periyodundakiler görmezden gelinsin.

** Geri sektiren dekoratör ile Kısma dekoratörü arasındaki fark; görmezden gelinen çağrı eğer belirlenen süre zarfında yaşayabilirse, gecikme sonrasında çağırılır.

Daha iyi anlayabilmek için günlük kullanılan bir uygulamadan yararlanabiliriz.

Örneğin fare olaylarını takip etmek istiyorsunuz.

Tarayıcı üzerinde bir fonksiyon ile farenin her mikro seviyeli hareketinde gittiği yerlerin bilgileri alınabilir. Aktif fare kullanımı sırasında akıcı bir şekilde çalışacaktır. Her sn’de 100 defa ( 10ms ) çalışabilir.

İzleme fonksiyonu web sayfası üzerinde bazı bilgileri güncellemeli.

Güncelleme fonksiyonu update()`'in her fare mikro hareketinde çalışması sisteme çok ağır gelmektedir. Aslında bu fonksiyonun 100ms’de birden fazla çalışmasının da bir mantığı yoktur.

Bundan dolayı update() yerine, her bir fare hareketinde çalışacak throttle(update,100) fonksiyonu kullanılacaktır. Bu dekoratör her fare değişiminde çağırılabilir fakat update() 100ms içerisinde maksimum bir defa çağırılacaktır.

Şu şekilde görünecektir:

  1. İlk fare hareketinde dekoratör çağrıyı doğrudan update'e yönlendirecektir. Bu önemlidir, kullanıcı böylece hareketinin sonucunu doğrudan görür.
  2. Sonrasında fare hareket etse de 100ms geçene kadar hiçbir şey olmaz. Dekoratör çağrıları görmezden gelir.
  3. 100ms sonunda son koordinatlar ile tekrardan bir update çalışır.
  4. En sonunda fare bir yerlerde durur. Dekoratör 100ms bekler ve bu bekleme bittikten sonra update fonksiyonu son koordinatlar ile çalışır. Belki de en önemlisi son fare koordinatlarının da işlenmiş olmasıdır.

Kod örneği:

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

// f1000 f'e çağrıların 1000ms en fazla bir defa geçmesini sağlar.
let f1000 = throttle(f, 1000);

f1000(1); //  1 yazar
f1000(2); // (throttling, 1000ms henüz bitmedi)
f1000(3); // (throttling, 1000ms henüz bitmedi)

// 1000 ms bittiğinde...
// ...çıktısı 3 olur , aradaki 2 değeri pas geçilir.

P.S. kaynağın argümanı this f1000'e iletilmiştir. Bu argüman f fonksiyonuna da iletilmelidir.

Testler ile korunaklı olan aç.

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

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

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

    isThrottled = true;

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

  return wrapper;
}

throttle(func,ms)'e yapılan çağrı saklayıcı(wrapper) döner.

  1. İlk çağrıda saklayıcı func döner ve rahatlama yavaşlama durumuna girer ( isThrottled=true ).
  2. Bu durumda tüm çağrılar savedArgs/savedThis içerisinde tutulur. Bu kaynaklar ve argümanlar hafızada tutulmalıdır. Çünkü çağrıyı eşzamanlı olarak çoğaltmamız için bu bilgiler gereklidir.
  3. ms süresi geçtikten sonra setTimeout çalışır. Yavaşlama durumu sona erer (isThrottled = false ). Eğer görmezden gelinmiş çağrı var ise saklayıcı son hafızada tutulan kaynağı ve argüman ile çalışır.

Son adım func yerine wrapper çalıştırır, çünkü sadece func'ın çalışması yetmez ayrıca yavaşlama durumuna girilmesi gereklidir.

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) {
      // sakinleştikten sonra son argümanları çağırmak için kaydet.
      savedArgs = arguments;
      savedThis = this;
      return;
    }

    // diğer türlü sakinleşme durumuna geç.
    func.apply(this, arguments);

    isThrottled = true;

    // isThrottled'ı geciktirmeden sonra tekrardan başlatmak için plan yap.
    setTimeout(function() {
      isThrottled = false;
      if (savedArgs) {
        // eğer çağrı varsa savedThis/savedArgs değerleri mevcuttur.
        // Kendini yineleyen çağrılar fonksiyonu çağırır ve tekrar sakin duruma geçer.
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

Çözümü testler korunaklı alanda olacak şekilde aç.

Eğitim haritası