۷ فوریه ۲۰۲۲

دکوراتورها و ارسال کردن، متدهای call/apply

هنگام کار کردن با تابع‌ها، جاوااسکریپت انعطاف‌پذیری بی‌نظیری را ارائه می‌دهد. تابع‌ها می‌توانند رد و بدل شوند، به عنوان شیء استفاده شوند و حالا ما خواهیم دید که چگونه فراخوانی‌ها را بین تابع‌ها ارسال کنیم و رفتار آن‌ها را تغییر دهیم.

کش کردن پنهانی

فرض کنیم تابع slow(x) را داریم که از پردازنده خیلی کار می‌کشد اما نتیجه‌های آن همیشه ثابت هستند. به عبارتی دیگر، برای x یکسان همیشه نتیجه‌ای یکسان را برمی‌گردند.

اگر تابع زیاد فراخوانی می‌شود، ممکن است بخواهیم که نتیجه‌ها را کَش کنیم (به یاد بسپاریم) تا از مصرف زمان اضافی برای محاسبات دوباره جلوگیری کنیم.

اما به جای اینکه این قابلیت را به slow(x) اضافه کنیم یک تابع دربرگیرنده (wrapper) می‌سازیم که کش کردن را اضافه می‌کند. همانطور که خواهیم دید، مزایای زیادی از انجام این کار دریافت می‌کنیم.

کد اینگونه است و توضیحات به دنبال آن:

function slow(x) {
  // اینجا می‌تواند یک کاری که پردازنده را زیاد مشغول می‌کند وجود داشته باشد
  alert(`با ${x} فراخوانی شد`);
  return x;
}

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

  return function(x) {
    if (cache.has(x)) {    // وجود داشت cache اگر چنین کلیدی در
      return cache.get(x); // نتیجه را از آن بخوان
    }

    let result = func(x);  // را فراخوانی کن func در غیر این صورت

    cache.set(x, result);  // و نتیجه را کش کن (به خاطر بسپار)
    return result;
  };
}

slow = cachingDecorator(slow);

alert( slow(1) ); // کش شده و نتیجه آن برگردانده شد slow(1)
alert( "Again: " + slow(1) ); // از کش برگردانده شد slow(1) نتیجه

alert( slow(2) ); // کش شده و نتیجه آن برگردانده شد slow(2)
alert( "Again: " + slow(2) ); // از کش برگردانده شد slow(2) نتیجه

در کد بالا cachingDecorator یک دکوراتور است: تابعی خاص که یک تابع دیگر را دریافت می‌کند و رفتار آن را تغییر می‌دهد.

ایده این است که ما می‌توانیم cachingDecorator را برای هر تابعی فراخوانی کنیم و این تابع، دربرگیرنده کش‌کننده را برمی‌گرداند. این عالی است چون ما می‌توانیم تابع‌های زیادی داشته باشیم که از چنین خاصیتی استفاده کنند و تنها کاری که ما باید انجام دهیم، اعمال cachingDecorator روی آن‌ها است.

با جدا کردن کش کردن از کد تابع اصلی، ما کد اصلی را هم ساده‌تر نگه داشتیم.

نتیجه‌ی cachingDecorator(func) یک «دربرگیرنده» است: تابع function(x) که فراخوانی func(x) را در منطق کش کردن «می‌پوشاند»:

از یک کد بیرونی، تابع slow دربر گرفته شده کار یکسانی انجام می‌دهد. فقط یک جنبه کش کردن به رفتار این تابع اضافه شده است.

برای خلاصه‌سازی، چند مزیت در استفاده کردن از یک cachingDecorator به صورت جداگانه به جای تغییر کد خود slow وجود دارد:

  • تابع cachingDecorator را می‌توان دوباره استفاده کرد. ما می‌توانیم آن را روی تابع دیگری هم اعمال کنیم.
  • منطق کش کردن جدا است، این منطق پیچیدگی خود slow را افزایش نداد (اگر وجود داشت).
  • اگر نیاز باشد ما می‌توانیم چند دکوراتور را ترکیب کنیم (دکوراتورهای دیگر پیروی خواهند کرد).

استفاده از “func.call” برای زمینه

دکوراتور کش کردن که در بالا گفته شد برای کار با متدهای شیء مناسب نیست.

برای مثال، در کد پایین worker.slow() بعد از دکور کردن کار نمی‌کند:

// کش کند worker.slow کاری خواهیم کرد که
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // کاری که به پردازنده خیلی فشار می‌آورد را اینجا داریم
    alert("فراخوانی شده با " + 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(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // متد اصلی کار می‌کند

worker.slow = cachingDecorator(worker.slow); // حالا کاری می‌کنیم که کش کند

alert( worker.slow(2) ); // Error: Cannot read property 'someMethod' of undefined !وای یک ارور

ارور در خط (*) اتفاق می‌افتد، خطی که تلاش می‌کند به this.someMethod دسترسی پیدا کند و شکست می‌خورد. می‌توانید ببینید چرا؟

دلیلش این است که دربرگیرنده تابع اصلی را به عنوان func(x) در خط (**) فراخوانی می‌کند. و زمانی که اینگونه فرا خواند، تابع this = undefined را دریافت می‌کند.

اگر سعی می‌کردیم که این را اجرا کنیم هم مشکل یکسانی پیش می‌آمد:

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

پس دربرگیرنده فراخوانی را به متد اصلی می‌فرستد اما بدون زمینه this. به همین دلیل ارور ایجاد می‌شود.

بیایید این را درست کنیم.

یک متد درون ساخت خاص برای تابع‌ها وجود دارد به نام func.call(context, …args) که به ما این امکان را می‌دهد تا به صراحت با تنظیم کردن this یک تابع را فرا بخوانیم.

سینتکس اینگونه است:

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

این متد با دریافت اولین آرگومان به عنوان this و بقیه آن‌ها به عنوان آرگومان‌های تابع func را اجرا می‌کند.

برای اینکه ساده بگوییم، این دو فراخوانی تقریبا کار یکسانی را انجام می‌دهند:

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

هر دوی آن‌ها func را با آرگومان‌های 1، 2 و 3 فراخوانی می‌کنند. تنها تفاوت این است که func.call مقدار this را هم برابر با obj قرار می‌دهد.

به عنوان مثال، در کد پایین ما sayHi را با زمینه‌های مختلفی از شیءها فراخوانی می‌کنیم: sayHi.call(user) تابع sayHi را با تنظیم کردن this=user اجرا می‌کند و خط بعدی this=admin را تنظیم می‌کند:

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

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

// استفاده کنید "this" برای قرار دادن شیءهای متفاوت به عنوان call از
sayHi.call( user ); // John
sayHi.call( admin ); // Admin

و اینجا ما از call برای فراخوانی say همراه با زمینه و عبارت داده شده استفاده می‌کنیم:

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

let user = { name: "John" };

// قرار می‌گیرد و "سلام" اولین آرگومان می‌شود this در user
say.call( user, "سلام" ); // John: سلام

در این مورد ما، می‌توانیم از call درون دربرگیرنده استفاده کنیم تا زمینه را در تابع اصلی تنظیم کنیم:

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

  slow(x) {
    alert("فراخوانی شده با " + 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" حالا
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // حالا کاری می‌کنیم که کش کند

alert( worker.slow(2) ); // کار می‌کند
alert( worker.slow(2) ); // کار می‌کند، تابع اصلی را فراخوانی نمی‌کند (کش شده است)

حالا همه چیز درست است.

برای اینکه همه چیز را روشن کنیم، بیایید عمیق‌تر ببینیم که this چگونه تنظیم شده است:

  1. بعد از دکور کردن، worker.slow همان دربرگیرنده‌ی function (x) { ... } است.
  2. پس زمانی که worker.slow(2) اجرا می‌شود، دربرگیرنده 2 را به عنوان آرگومان دریافت می‌کند و this=worker است (همان شیء قبل از نقطه).
  3. درون دربرگیرنده، با فرض اینکه نتیجه هنوز کش نشده است، func.call(this, x) مقدار this کنونی (=worker) و آرگومان کنونی (=2) را در متد اصلی تنظیم می‌کند.

چند آرگومانی شدن

حالا بیایید cachingDecorator را جامع‌تر کنیم. تا حالا فقط با تابع‌هایی که یک آرگومان داشتند کار می‌کرد.

حالا چگونه متد worker.slow که چند آرگومان دارد را کش کنیم؟

let worker = {
  slow(min, max) {
    return min + max; // یک کاری که به پردازنده فشار می‌آورد
  }
};

// باید فراخوانی‌هایی که آرگومان‌های یکسانی دارند را یه خاطر بسپارد
worker.slow = cachingDecorator(worker.slow);

قبلا، برای یک آرگومان می‌توانستیم از cache.set(x, result) برای ذخیره نتیجه و cache.get(x) برای دریافت آن استفاده کنیم. اما حالا باید نتیجه را برای ترکیبی از آرگومان‌ها(min,max) ذخیره کنیم. ساختار Map فقط یک مقدار را به عنوان کلید دریافت می‌کند.

چند راه‌حل احتمالی وجود دارد:

  1. یک ساختار داده جدید شبیه map پیاده‌سازی کنیم (یا از شخص ثالث استفاده کنیم) که همه‌کاره است و چندکلیدی را ممکن می‌سازد.
  2. از mapهای پیچیده استفاده کنیم: cache.set(min) یک Map خواهد بود که جفت (max, result) را ذخیره می‌کند. پس ما می‌توانیم result را به صورت cache.get(min).get(max) دریافت کنیم.
  3. دو مقدار را به یک مقدار تبدیل کنیم. در این مورد خاص، می‌توانیم از رشته "min,max" به عنوان کلید Map استفاده کنیم. برای انعطاف پذیری، می‌توانیم یک تابع ترکیب‌سازی(hashing function) برای دکوراتور تعیین کنیم که می‌داند چگونه از چند مقدار یک مقدار بدست آورد.

برای بسیاری از موارد عملی، نوع سوم به اندازه کافی مناسب است پس ما با همان کار می‌کنیم.

همچنین ما باید نه تنها x بلکه تمام آرگومان‌ها را در func.call قرار دهیم. بیایید یادآوری کنیم که در یک تابع function() می‌توانیم یک شبه‌آرایه از آرگومان‌های آن را با arguments دریافت کنیم پس func.call(this, ...arguments) باید جایگزین func.call(this, x) شود.

اینجا یک cachingDecorator قدرتمندتر داریم:

let worker = {
  slow(min, max) {
    alert(`فراخوانی شده با ${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) ); // کار می‌کند
alert( "Again " + worker.slow(3, 5) ); // یکسان است (کش شده)

حالا این تابع با هر تعداد آرگومان کار می‌کند (گرچه تابع ترکیب‌سازی هم باید جوری تنظیم شود که هر تعداد آرگومان را قبول کند. یک راه جالب برای کنترل این موضوع پایین‌تر پوشش داده شده است).

دو تفاوت وجود دارد:

  • در خط (*) این تابع، hash را فراخوانی می‌کند تا یک کلید را از arguments بسازد. اینجا ما از تابع ساده «پیوند دادن» استفاده کردیم که آرگومان‌های (3, 5) را به کلید "3,5" تبدیل می‌کند. موارد پیچیده‌تر ممکن است تابع‌های ترکیب‌سازی دیگری را نیاز داشته باشند.
  • سپس خط (**) برای اینکه زمینه و تمام آرگومان‌هایی که دربرگیرنده دریافت کرد (نه فقط اولی) را در تابع اصلی قرار دهد از func.call(this, ...arguments) استفاده می‌کند.

متد func.apply

می‌توانستیم به جای func.call(this, ...arguments) از func.apply(this, arguments) استفاده کنیم.

سینتکس متد درون‌ساخت func.apply اینگونه است:

func.apply(context, args)

این متد با تنظیم کردن this=context و استفاده از شیء args به عنوان لیستی از آرگومان‌ها، تابع func را فراخوانی می‌کند.

تنها تفاوت بین call و apply این است که call لیستی از آرگومان‌ها را قبول می‌کند در حالی که apply یک شیء شبه‌آرایه که شامل آرگومان‌ها است را قبول می‌کند.

پس این دو فراخوانی تقریبا یکی هستند:

func.call(context, ...args);
func.apply(context, args);

آن‌ها فراخوانی یکسانی از func همراه با زمینه و آرگومان‌های داده شده را انجام می‌دهند.

فقط یک تفاوت جزئی در مورد args وجود دارد:

  • سینتکس اسپرد ... به ما اجازه می‌دهد تا args حلقه‌پذیر را به عنوان لیست در call قرار دهیم.
  • متد apply فقط args شبه‌آرایه را قبول می‌کند.

…و برای شیءهایی که هم حلقه‌پذیر و هم شبه‌آرایه هستند، مانند آرایه واقعی، ما می‌توانیم هر یک از آن‌ها را استفاده کنیم اما احتمالا apply سریع‌تر باشد چون بیشتر موتورهای جاوااسکریپت آن را از دورن بهتر بهینه کرده‌اند.

قرار دادن تمام آرگومان‌ها در کنار زمینه در تابعی دیگر را ارسال کردن فراخوانی(call forwarding) می‌گویند.

این ساده‌ترین شکل از آن است:

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

زمانی که یک کد بیرونی این wrapper را فراخوانی کند، نمی‌توان آن را از فراخوانی تابع اصلی func تشخیص داد.

قرض گرفتن یک متد

حالا بیایید یک پیشرفت جزئی دیگر در تابع ترکیب‌سازی ایجاد کنیم:

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

اکنون، این تابع فقط روی دو آرگومان کار می‌کند. اگر این تابع بتواند هر تعداد args را به هم بچسباند بهتر می‌شد.

راه‌حل طبیعی استفاده از متد arr.join است:

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

…متاسفانه این روش کار نخواهد کرد. چون ما در حال فراخوانی hash(arguments) هستیم و شیء arguments هم حلقه‌پذیر است و هم شبه‌آرایه اما یک آرایه واقعی نیست.

پس همانطور که در کد پایین می‌بینیم، فراخوانی join بر روی آن با شکست مواجه می‌شود:

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

hash(1, 2);

اما هنوز یک راه آسان برای استفاده از پیوند دادن آرایه وجود دارد:

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

hash(1, 2);

این ترفند قرض‌گیری متد (method borrowing) نام دارد.

ما متد پیوند دادن را از یک آرایه معمولی (قرض) می‌گیریم ([].join) و برای اجرای آن با زمینه arguments از [].join.call استفاده می‌کنیم.

این چرا کار می‌کند؟

به دلیل اینکه الگوریتم داخلی متد نیتیو (native method) arr.join(glue) بسیار ساده است.

این مراحل تقریبا «بدون تغییر» از مشخصات زبان برداشته شده است:

  1. فرض کنیم که glue آرگومان اول باشد یا اگر آرگومانی وجود نداشت، پس یک کاما ",".
  2. فرض کنیم result یک رشته خالی باشد.
  3. this[0] را به result اضافه کنید.
  4. glue و this[1] را اضافه کنید.
  5. glue و this[2] را اضافه کنید.
  6. …تا زمانی که تعداد this.length المان به هم چسبیدند ادامه دهید.
  7. result را برگردانید.

پس از لحاظ فنی این تابع this را دریافت می‌کند و this[0]، this[1] و بقیه را به هم پیوند می‌زند. این متد از قصد طوری نوشته شده است که هر this شبه‌آرایه را قبول کند (این اتفاقی نیست، بسیاری از متدها از این موضوع پیروی می‌کنند). به همین دلیل است که با this=arguments هم کار می‌کند.

دکوراتورها و ویژگی‌های تابع

به طور کلی اینکه یک تابع یا متد را با تابعی دکور شده جایگزین کنیم مشکلی ایجاد نمی‌کند به جز در موردی کوچک. اگر تابع اصلی ویژگی‌هایی درون خود داشته باشد، مثلا func.calledCount یا هر چیزی، سپس تابع دکور شده آن‌ها را فراهم نمی‌کند. چون یک دربرگیرنده است. پس اگر کسی از دکوراتورها استفاده کرد باید حواسش به این موضوع باشد.

برای نمونه، در مثال بالا اگر تابع slow ویژگی‌ای درون خودش داشت، سپس cachingDecorator(slow) یک دربرگیرنده خواهد بود که آن ویژگی را ندارد.

بعضی از دکوراتورها ممکن است ویژگی‌های خودشان را داشته باشند. مثلا یک دکوراتور می‌تواند تعداد دفعاتی که یک تابع فراخوانی شده و اجرای آن چقدر زمان برده است را محاسبه کند و از طریق ویژگی‌های دربرگیرنده این اطلاعات را در اختیار بگذارد.

گرچه راهی برای ساخت دکوراتورهایی که به ویژگی‌های تابع دسترسی داشته باشد وجود دارد اما این روش به استفاده از یک شیء Proxy خاص برای دربرگرفتن تابع نیاز دارد. ما این موضوع را بعدها در مقاله Proxy and Reflect بررسی می‌کنیم.

خلاصه

دکوراتور یک دربرگیرنده تابع است که رفتار آن را تغییر می‌دهد. کار اصلی هنوز توسط تابع انجام می‌شود.

دکوراتورها می‌توانند به عنوان «خاصیت‌ها» یا «جنبه‌هایی» دیده شوند که می‌توانند به تابع اضافه شوند. ما می‌توانیم یک یا چند خاصیت اضافه کنیم. و همه این‌ها را بدون تغییر کد آن انجام دهیم.

برای پیاده‌سازی cachingDecorator، ما متدهای زیر را مطالعه کردیم:

  • func.call(context, arg1, arg2…) – تابع func را همراه با زمینه و آرگومان‌های داده شده فرا می‌خواند.
  • func.apply(context, args) – با پاس دادن context به عنوان this و شبه‌آرایه args درون لیستی از آرگومان‌ها، تابع func را فراخوانی می‌کند.

ارسال فراخوانی به طور کل معمولا با apply انجام می‌شود:

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

همچنین یک مثال از قرض‌گیری متد دیدیم، زمانی که ما یک متد را از شیءای قرض می‌گیریم و آن را با زمینه‌ای از شیءای دیگر توسط call فراخوانی می‌کنیم. اینکه متدهای آرایه را دریافت کنیم و آن‌ها را روی arguments اعمال کنیم بسیار رایج است. استفاده از شیء پارامترهای رست، راه جایگزین و یک آرایه واقعی است.

دکوراتورهای زیادی در واقعیت وجود دارد. با حل کردن تمرین‌های این فصل نشان دهید که چقدر آن‌ها را یاد گرفته‌اید.

تمارین

اهمیت: 5

یک دکوراتور spy(func) بسازید که باید دربرگیرنده‌ای را برگرداند که تمام فراخوانی‌های تابع را درون ویژگی calls خودش ذخیره کند.

هر فراخوانی به عنوان آرایه‌ای از آرگومان‍‌ها ذخیره می‌شود.

برای مثال:

function work(a, b) {
  alert( a + b ); // یک تابع یا متد داخواه است work تابع
}

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"
}

پی‌نوشت: این دکوراتور بعضی اوقات در انجام یونیت تست (unit-testing) کاربرد دارد. شکل پیشرفته آن sinon.spy در کتابخانه Sinon.JS است.

باز کردن یک sandbox همراه با تست‌ها.

دربرگیرنده که توسط spy(f) برگردانده می‌شود باید تمام آرگومان‌ها را ذخیره و سپس از f.apply برای ارسال کردن فراخوانی استفاده کند.

function spy(func) {

  function wrapper(...args) {
    // wrapper.calls برای ذخیره کردن آرایه «واقعی» درون arguments به جای ...args استفاده از
    wrapper.calls.push(args);
    return func.apply(this, args);
  }

  wrapper.calls = [];

  return wrapper;
}

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 5

یک دکوراتور delay(f, ms) بسازید که هر فراخوانی f را به اندازه ms میلی‌ثانیه به تأخیر می‌اندازد.

برای مثال:

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

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

f1000("test"); // را بعد از 1000 میلی‌ثانیه نشان می‌دهد "test"
f1500("test"); // را بعد از 1500 میلی‌ثانیه نشان می‌دهد "test"

به عبارتی دیگر، delay(f, ms) یک نوع از f که «به اندازه ms تأخیر دارد» را برمی‌گرداند.

در کد بالا، f تایعی است که یک آرگومان دارد اما راه‌حل شما باید تمام آرگومان‌ها و زمینه this را در فراخوانی قرار دهد.

باز کردن یک sandbox همراه با تست‌ها.

راه‌حل:

function delay(f, ms) {

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

}

لطفا به چگونگی استفاده از تابع کمانی در اینجا توجه کنید. همانطور که می‌دانیم، تابع‌های کمانی this و arguments خودشان را ندارند پس f.apply(this, arguments) مقدار this و arguments را از دربرگیرنده می‌گیرند.

اگر ما یک تابع معمولی را قرار دهیم، setTimeout آن را بدون آرگومان‌ها و this=window (در مرورگر) فراخوانی خواهد کرد، پس ما باید کمی بیشتر کد بنویسیم تا آن‌ها از طریق دربرگیرنده رد و بدل کنیم:

function delay(f, ms) {

  // قرار دهیم setTimeout و آرگومان‌ها را از طریق دربرگیرنده درون this متغیرهایی اضافه کردیم تا
  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);
  };

};

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 5

نتیجه دکوراتور debounce(f, md) یک دربرگیرنده است که تا ms میلی‌ثانیه عدم فعالیت وجود نداشته باشد، فراخوانی‌های f را به حالت تعلیق در می‌آورد (فراخوانی انجام نمی‌شود، «مدت زمان آرام‌شدن») سپس f را با آخرین آرگومان‌ها فراخوانی می‌کند.

به عبارتی دیگر، debounce مانند یک منشی است که «تماس‌های تلفنی» را می‌پذیرد و تا زمانی که ms میلی‌ثانیه سکوت برقرار شود صبر می‌کند. و فقط بعد از این مدت اطلاعات آخرین تماس را به «رئیس» منتقل می‌کند (تابع واقعی f را فرا می‌خواند).

برای مثال، ما یک تابع f داشتیم و آن را با f = debounce(f, 1000) جایگزین کردیم.

سپس اگر تابع دربرگرفته شده در زمان‌های 0، 200، 500 میلی‌ثانیه فراخوانی شد، و پس از آن هیچ فراخوانی‌ای وجود نداشت، سپس تابع واقعی f فقط یک بار بعد از 1500 میلی‌ثانیه فراخوانی می‌شود. یعنی اینکه: پس از 1000 میلی‌ثانیه زمان آرام‌شدن از زمان آخرین فراخوانی.

…و این تابع آرگومان‌های آخرین فراخوانی را دریافت می‌کند و بقیه فراخوانی‌ها نادیده گرفته می‌شوند.

اینجا کدی برای آن داریم (از دکوراتور معلق‌کننده در کتابخانه Lodash library استفاده می‌کند):

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

f("a");
setTimeout( () => f("b"), 200);
setTimeout( () => f("c"), 500);
// alert("c") :تابع معلق 1000 میلی‌ثانیه بعد از آخرین فراخوانی صبر می‌کند و سپس اجرا می‌شود

حالا یک مثال عملی. بیایید فرض کنیم که کاربر چیزی تایپ می‌کند و ما می‌خواهیم یک زمانی که وارد کردن تمام شد یک درخواست به سرور ارسال کنیم.

دلیلی برای فرستادن درخواست برای هر کاراکتر تایپ شده وجود ندارد. به جای آن ما می‌خواهیم صبر کنیم و سپس تمام نتیجه را پردازش کنیم.

در یک مرورگر وب، ما می‌توانیم یک کنترل‌کننده رویداد ترتیب دهیم – تابعی که برای هر تغییر در قسمت ورودی فراخوانی می‌شود. در حالت عادی، یک کنترل‌کننده رویداد خیلی فراخوانی می‌شود، برای هر کلید تایپ شده. اما اگر ما آن را به مدت 1000 میلی‌ثانیه debounce(معلق) کنیم، پس از 1000 میلی‌ثانیه بعد از آخرین ورودی، فقط یک بار فراخوانی می‌شود.

در این مثال زنده، کنترل‌کننده نتیجه را درون جعبه بیرون می‌گذارد، امتحانش کنید:

می‌بینید؟ ورودی دوم، تابع معلق را فراخوانی می‌کند پس محتوای آن پس از 1000 میلی‌ثانیه بعد از آخرین ورودی پردازش می‌شود.

پس debounce راهی عالی برای پردازش دنباله‌ای از رویدادها است: چه دنباله‎ای از فشار دادن کلیدها باشد، چه حرکت مَوس(mouse) یا هر چیز دیگری.

این تابع به اندازه زمان داده شده پس از آخرین فراخوانی صبر می‌کند و سپس تابع خودش که می‌تواند نتیجه را پردازش کند را فرا می‌خواند.

تمرین، پیاده‌سازی دکوراتور debounce است.

راهنمایی: اگر درباره آن فکر کنید، فقط چند خط می‌شود :)

باز کردن یک sandbox همراه با تست‌ها.

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

فراخوانی debounce یک دربرگیرنده را برمی‌گرداند. زمانی که فرا خوانده شد، زمان‌بندی می‌کند که تابع اصلی بعد از مدت ms داده شده فراخوانی شود و زمان‌بندی قبلی را لغو می‌کند.

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

اهمیت: 5

یک دکوراتور «جلوگیرنده» throttle(f, ms) بسازید که یک دربرگیرنده را برمی‌گرداند.

زمانی که چند بار فراخوانی شد، فقط یک بار به ازای هر ms میلی‌ثانیه f را فرا می‌خواند.

تفاوت این تابع با معلق‌کننده این است که کاملا یک دکوراتور متفاوت است:

  • debounce تابع را بعد از مدت «آرام‌شدن» اجرا می‌کند. برای پردازش نتیجه نهایی خوب است.
  • throttle هر بار بعد از گذشت ms میلی‌ثانیه تابع را اجرا می‌کند. برای بروزرسانی‌های منظم که نباید زیاد انجام شوند خوب است.

به عبارتی دیگر، throttle مانند یک منشی است که تماس‌های تلفنی را می‌پذیرد اما پس از ms میلی‌ثانیه فقط یک بار مزاحم رئیس می‌شود (تابع واقعی f را فراخوانی می‌کند).

بیایید کاربردی واقعی را بررسی کنیم تا این نیاز و دلیل وجود آن را بهتر متوجه شویم.

برای مثال، ما می‌خواهیم حرکت‌های موس را زیر نظر بگیریم.

در مرورگر می‌توانیم یک تابع را پیاده‌سازی کنیم تا با هر حرکت موس اجرا شود و همانطور که موس تکان می‌خورد موقعیت آن را دریافت کند. در حین استفاده از موس، این تابع معمولا به طور مکرر اجرا می‌شود و می‌تواند چیزی مثل 100 بار در ثانیه باشد (هر 10 میلی‌ثانیه). ما می‌خواهیم زمانی که اشاره‌گر تکان می‌خورد اطلاعاتی را در صفحه وب بروزرسانی کنیم.

…اما بروزرسانی تابع update() در هر حرکت بسیار کوچک خیلی کار سنگینی است. دلیلی منطقی هم برای برورسانی آن زودتر از هر 100 میلی‌ثانیه وجود ندارد.

پس ما آن را درون یک دکوراتور قرار می‌دهیم: به جای تابع اصلی update() از throttle(update, 100) به عنوان تابع اجرایی در هر حرکت موس استفاده می‌کنیم. دکوراتور اکثر مواقع فرا خوانده می‌شود اما فراخوانی را هر 100 میلی‌ثانیه به update() ارسال می‌کند.

از لحاظ بصری، اینگونه به نظر خواهد رسید:

  1. برای اولین حرکت موس تابع دکور شده بلافاصله فراخوانی را به update ارسال می‌کند. این مهم است که کاربر واکنش ما نسبت به حرکت خود به سرعت ببیند.
  2. سپس همانطور که موس حرکت می‌کند، تا قبل از 100ms میلی‌ثانیه چیزی اتفاق نمی‌افتد. تابع دکوراتور فراخوانی‌ها را نادیده می‌گیرد.
  3. زمانی که 100ms تمام می‌شود، یک بروزرسانی بیشتر update با آخرین مختصات اتفاق می‌افتد.
  4. سپس، بالاخره، موس جایی متوقف می‌شود. تابع دکور شده صبر می‌کند تا 100ms تمام شود و سپس update را همراه با آخرین مختصات اجرا می‌کند. پس خیلی مهم است که آخرین مختصات موس پردازش شود.

یک مثال از کد:

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

// منتقل می‌کند f فراخوانی‌ها را هر 1000 میلی‌ثانیه به f1000 تابع
let f1000 = throttle(f, 1000);

f1000(1); // shows 1
f1000(2); // (از فراخوانی جلوگیری می‌کند، هنوز 1000 میلی‌ثانیه نشده است)
f1000(3); // (از فراخوانی جلوگیری می‌کند، هنوز 1000 میلی‌ثانیه نشده است)

// ...زمانی که 1000 میلی‌ثانیه تمام می‌شود
// عدد 3 را نشان می‌دهد، مقدار میانی 2 نادیده گرفته شد...

پی‌نوشت: آرگومان‌ها و زمینه this که به f1000 داده می‌شوند باید به f اصلی منتقل شوند.

باز کردن یک sandbox همراه با تست‌ها.

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;
}

فراخوانی throttle(func, ms) تابع wrapper را برمی‌گرداند.

  1. در حین اولین فراخوانی، تابع wrapper فقط func را اجرا می‌کند و وضعیت آرام‌شدن را تنظیم می‌کند (isThrottled = true).
  2. در این حالت، تمام فراخوانی‌ها در savedArgs/savedThis ذخیره می‌شوند. لطفا در نظر داشته باشید که هم زمینه و هم آرگومان‌ها به یک اندازه مهم هستند و باید به یاد سپرده شوند. ما برای اینکه فراخوانی جدید بسازیم به هر دوی آن‌ها نیاز داریم.
  3. بعد از اینکه ms میلی‌ثانیه طی شد، setTimeout فعال می‌شود. حالت آرام‌شدن حذف می‌شود (isThrottled = false) و اگر ما فراخوانی نادیده‌گرفته‌شده‌ای داشتیم، wrapper همراه با آخرین آرگومان‌ها و زمینه ذخیره شده اجرا می‌شود.

مرحله سوم wrapper را اجرا می‌کند نه func را، چون ما نه تنها نیاز داریم که func را اجرا کنیم بلکه باید دوباره به حالت آرام‌شدن برگردیم و زمان‌بندی را برای تنظیم مجدد آن پیاده‌سازی کنیم.

function throttle(func, ms) {

  let isThrottled = false,
    savedArgs,
    savedThis;

  function wrapper() {

    if (isThrottled) {
      // بخاطر سپردن آخرین آرگومان‌ها برای فراخوانی بعد از آرام‌شدن
      savedArgs = arguments;
      savedThis = this;
      return;
    }

    // در غیر این صورت به حالت آرام‌شدن برو
    func.apply(this, arguments);

    isThrottled = true;

    // بعد از تأخیر isThrottled زمان‌بندی برای تنظیم مجدد
    setTimeout(function() {
      isThrottled = false;
      if (savedArgs) {
        // آخرین آن‌ها را دارند savedThis/savedArgs ،اگر فراخوانی‌ای وجود داشت
        // فراخوانی بازگشتی تابع را اجرا می‌کند و حالت آرام‌شدن را دوباره تنظیم می‌کند
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

باز کردن راه‌حل همراه با تست‌ها درون یک sandbox.

نقشه آموزش