بازگشت به درس

دکوراتور جلوگیرنده

اهمیت: 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.