Debounce و Throttle در جاوا اسکریپت — به زبان ساده

۵۸۶ بازدید
آخرین به‌روزرسانی: ۷ شهریور ۱۴۰۲
زمان مطالعه: ۶ دقیقه
دانلود PDF مقاله
Debounce و Throttle در جاوا اسکریپت — به زبان ساده

آیا تاکنون این تجربه را داشته‌اید که صفحه وب در زمان اسکرول کردن، بسیار کُند شود؟ زمانی که از انیمیشن‌های canvas در محیط داشبورد استفاده می‌کنیم، مصرف CPU برای نمایش روان انیمیشن‌ها در هر ثانیه بسیار بالا می‌رود. این وضعیت در صورتی که بخواهید در زمان حرکت ماوس روی این نمودارها یک tooltip نیز نمایش پیدا کند، وخامت بسیار بیشتری پیدا می‌یابد. یکی از راه‌حل‌های این وضعیت، استفاده از Debounce و Throttle در جاوا اسکریپت است که در این راهنما بررسی می‌کنیم. اگر با مفاهیم تابع‌های ناهمگام و طرز کار HOF آشنا باشید، درک مطالبی که در ادامه می‌آید بسیار آسان‌تر خواهد بود.

997696

بررسی یک مثال

فرض کنید می‌خواهید از طریق یک فراخوانی API به دنبال نام یک کشور بگردید. یک کادر ورودی روی صفحه وجود دارد که می‌توانید نام کشور را در آن وارد کنید. زمانی که رشته را در آن وارد کردید، اپلیکیشن اقدام به فراخوانی API کرده و همه کشورهایی که با رشته ورودی تطبیق یابند، بارگذاری می‌کند.

وقتی کاربران موبایل یا افراد تنبل تلاش کنند این قابلیت را امتحان کنند، به صورت زیر خواهد بود:

Debounce و Throttle در جاوا اسکریپت

عبارت روی صفحه به این نکته اشاره دارد که چه تعداد درخواست‌های HTTP به سرور ارسال شده است. توجه کنید که هر بار که یک حرف کیبورد را می‌زنید، حتی زمانی که کلید بک‌اسپیس را ‌می‌زنید عدد این عبارت افزایش می‌یابد. این وضعیت در اپلیکیشن ساده ما شاید نامناسب به نظر نرسد، اما تصور کنید روی اپلیکیشن بزرگی مانند AirBnb کار می‌کنید، در این وضعیت ارسال درخواست‌ها در هر بار که کاربر یک کلید کیبورد را می‌زند یا کار دیگری انجام می‌دهد، انجام می‌شود که موجب کندی عملکرد خواهد شد.

Debounce و Throttle

برای درک مفهوم حقیقی Debounce و Throttle بهتر است ابتدا ایده راه‌حل مشکل فوق را بررسی کنیم. مشکل اصلی این است که درخواست‌های http زیادی ارسال می‌شوند. فرض کنید می‌خواهید به دنبال عبارت Canada بگردید اما صفحه وب به نوعی به جهت ارسال درخواست‌های C، ‌Ca، Can و Cana و غیره فریز شده است. این وضعیت خوبی نیست. بنابراین باید تعداد درخواست‌ها کاهش یابد.

Debounce و Throttle در واقع صرفاً نام‌هایی برای شیوه کاهش این درخواست‌ها هستند. نکته مشترک Debounce و Throttle یک مفهوم ساده است: زمانی که انگشت روی کلید می‌رود تا زمانی که تأیید نشده است، درخواستی ارسال نمی‌شود.

Debounce و Throttle در جاوا اسکریپت

به شکل فوق توجه کنید. در این تصویر می‌بینید که بسته به حالت‌های مختلف چه اتفاقاتی می‌افتد. در حالت Normal هر بار که کلیدی فشرده شود، یک درخواست ارسال می‌شود. در حالت Throttle یک درخواست نخستین بار در طی دوره ارسال می‌شود و دیگر هیچ درخواستی تا پایان دوره ارسال نمی‌شود. هنگامی که دوره زمانی خاتمه یابد، یک درخواست جدید مجدداً ارسال می‌شود. در حالت Debounce یک Callback استفاده می‌شود و در طی چند میلی‌ثانیه فراخوانی می‌شود و تنها درخواستی را ارسال می‌کند که در طی دوره زمانی درخواست دیگری اضافه نشده باشد. برای مثال به زمانی که حرف n در کلمه Canada وارد می‌شود دقت کنید. یک درخواست دیگر اضافه شده است و درخواست قبلی اضافه شده زمانی که a وارد شود نادیده گرفته می‌شود. با این منطق، اگر همه درخواست‌ها درون بازه زمانی اضافه شوند، تنها درخواست آخر اجرا خواهد شد.

بررسی کد حالت Throttle

اینک نوبت به نوشتن کد رسیده است

import axios from 'axios

1import axios from 'axios';
2const search = async (city) =>
3  await axios.get(`https://restcountries.eu/rest/v2/name/${city}`)

search تابعی است که نام شهر را با استفاده از API به همین نام ارسال می‌کند. از API واقعی restful برای تست این مثال در عمل استفاده کرده‌ایم. تنها کاری که باید انجام دهیم این است که کادر City را با هر نامی که دوست دارید، پر کنید. ابتدا کد را بررسی می‌کنیم:

1const throttle = (delay, fn) => {
2  let inThrottle = false;
3
4  return args => {
5    if (inThrottle) {
6      return;
7    }
8
9    inThrottle = true;
10    fn(args);
11    setTimeout(() => {
12      inThrottle = false;
13    }, delay);
14  };
15};

تابع بی‌نام یک HOF (تابع مرتبه بالاتر) است که تابع دیگری را بازگشت می‌دهد. زمانی که این تابع برای نخستین بار فراخوانی می‌شود، inThrottle مقدار false می‌گیرد. سپس زمانی که تابع بازگشتی دوباره فراخوانی می‌شود inThrottle مقدار true می‌گیرد و تابع callback یعنی fn اجرا می‌شود. در ادامه inThrottle دوباره در طی زمان delay میلی‌ثانیه مقدار false می‌گیرد. در این زمان در عین این که inThrottle مقدار true دارد، هیچ کدام از توابع بازگشتی از throttle نمی‌توانند اجرا شوند.

Debounce و Throttle در جاوا اسکریپت

چنان که در تصویر فوق می‌بینید، حتی اگر انگشت خود را روی دکمه نگه دارید، امکان اجرای تابع را نمی‌دهد مگر این که inThrottle مقدار false بگیرد. سپس متد جدید برای درخواست کردن به صورت زیر درمی‌آید:

const sendRequestThrottle = throttle(500, search);
<input type="text" onChange={sendRequestThrottle} />

اما یک نکته هست که باید مراقب آن باشیم. متد HOF به نام throttle نباید در کامپوننت React قرار گیرد. زمانی که متغیرهای حالت که در کامپوننت A هستند، تغییر می‌یابند، A رندر مجدد می‌شود و. همه چیز از نو انتساب می‌یابد. اگر throttle هم رندر مجدد شود inThrottle دوباره مقدار false خواهد گرفت.

بررسی کد Debounce

اینک به بررسی Debounce می‌پردازیم. این کد کاملاً مشابه کد Throttle است و بخش غالب‌ آن را به سادگی درک می‌کنید.

1const debounce = (delay, fn) => {
2  let inDebounce = null;
3  return args => {
4    clearTimeout(inDebounce);
5    inDebounce = setTimeout(() => fn(args), delay);
6  }
7}

می‌بینید که بسیار ساده است. تنها تفاوت با Throttle در این است که Debounce بررسی نمی‌کند، آیا inDebounce مقدار true دارد یا نه. اگر Callback درون دوره زمانی مورد نظر اجرا شده باشد، setTimeout قبلی لغو می‌شود و بی‌درنگ اجرا شده و یک setTimeout جدید ایجاد می‌کند. بنابراین اگر به فشردن کیبورد به صورت سریع ادامه بدهید، callback هرگز اجرا نخواهد شد

Debounce و Throttle در جاوا اسکریپت

اکنون می‌توانید فراخوانی‌های Debounce را بهتر درک کنید. تنها کاری که باید انجام دهید این است که callback خود را در DOM ثبت کنید:

const sendRequestDebounce = debounce(500, search);
<input type="text" onChange={sendRequestDebounce} />

Throttle و Debounce در Lodash

در اغلب موارد نیازی به ایجاد یک Throttle یا Debounce وجود ندارد، زیرا کتابخانه‌های سبک زیای وجود دارند که این قابلیت‌ها را عرضه می‌کنند. یک نمونه از آن کتابخانه Lodash است:

1function debounce(func, wait, options) {
2  let lastArgs,
3    lastThis,
4    maxWait,
5    result,
6    timerId,
7    lastCallTime
8
9  let lastInvokeTime = 0
10  let leading = false
11  let maxing = false
12  let trailing = true
13
14  // Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
15  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
16
17  if (typeof func !== 'function') {
18    throw new TypeError('Expected a function')
19  }
20  wait = +wait || 0
21  if (isObject(options)) {
22    leading = !!options.leading
23    maxing = 'maxWait' in options
24    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
25    trailing = 'trailing' in options ? !!options.trailing : trailing
26  }
27
28  function invokeFunc(time) {
29    const args = lastArgs
30    const thisArg = lastThis
31
32    lastArgs = lastThis = undefined
33    lastInvokeTime = time
34    result = func.apply(thisArg, args)
35    return result
36  }
37
38  function startTimer(pendingFunc, wait) {
39    if (useRAF) {
40      root.cancelAnimationFrame(timerId)
41      return root.requestAnimationFrame(pendingFunc)
42    }
43    return setTimeout(pendingFunc, wait)
44  }
45
46  function cancelTimer(id) {
47    if (useRAF) {
48      return root.cancelAnimationFrame(id)
49    }
50    clearTimeout(id)
51  }
52
53  function leadingEdge(time) {
54    // Reset any `maxWait` timer.
55    lastInvokeTime = time
56    // Start the timer for the trailing edge.
57    timerId = startTimer(timerExpired, wait)
58    // Invoke the leading edge.
59    return leading ? invokeFunc(time) : result
60  }
61
62  function remainingWait(time) {
63    const timeSinceLastCall = time - lastCallTime
64    const timeSinceLastInvoke = time - lastInvokeTime
65    const timeWaiting = wait - timeSinceLastCall
66
67    return maxing
68      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
69      : timeWaiting
70  }
71
72  function shouldInvoke(time) {
73    const timeSinceLastCall = time - lastCallTime
74    const timeSinceLastInvoke = time - lastInvokeTime
75
76    // Either this is the first call, activity has stopped and we're at the
77    // trailing edge, the system time has gone backwards and we're treating
78    // it as the trailing edge, or we've hit the `maxWait` limit.
79    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
80      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
81  }
82
83  function timerExpired() {
84    const time = Date.now()
85    if (shouldInvoke(time)) {
86      return trailingEdge(time)
87    }
88    // Restart the timer.
89    timerId = startTimer(timerExpired, remainingWait(time))
90  }
91
92  function trailingEdge(time) {
93    timerId = undefined
94
95    // Only invoke if we have `lastArgs` which means `func` has been
96    // debounced at least once.
97    if (trailing && lastArgs) {
98      return invokeFunc(time)
99    }
100    lastArgs = lastThis = undefined
101    return result
102  }
103
104  function cancel() {
105    if (timerId !== undefined) {
106      cancelTimer(timerId)
107    }
108    lastInvokeTime = 0
109    lastArgs = lastCallTime = lastThis = timerId = undefined
110  }
111
112  function flush() {
113    return timerId === undefined ? result : trailingEdge(Date.now())
114  }
115
116  function pending() {
117    return timerId !== undefined
118  }
119
120  function debounced(...args) {
121    const time = Date.now()
122    const isInvoking = shouldInvoke(time)
123
124    lastArgs = args
125    lastThis = this
126    lastCallTime = time
127
128    if (isInvoking) {
129      if (timerId === undefined) {
130        return leadingEdge(lastCallTime)
131      }
132      if (maxing) {
133        // Handle invocations in a tight loop.
134        timerId = startTimer(timerExpired, wait)
135        return invokeFunc(lastCallTime)
136      }
137    }
138    if (timerId === undefined) {
139      timerId = startTimer(timerExpired, wait)
140    }
141    return result
142  }
143  debounced.cancel = cancel
144  debounced.flush = flush
145  debounced.pending = pending
146  return debounced
147}

کد آن کاملاً طولانی است، اما کافی است به تابع debounced که یک HOF است توجه کنید:

1function debounce() {
2  let lastCallTime;
3  ...
4  function debounced(...args) {
5    ...
6  }
7  return debounced;
8}

Debounce یک تابع جدید به نام debounced بازگشت می‌دهد که در عمل اجرا خواهد شد. درون این debounced از استفاده می‌کنیم. بنابراین شاید متوجه شوید که debounce در Lodash اقدام به مقایسه زمان قبلی (که تابع قبلاً فراخوانی شده) با زمان جاری (که تابع فعلاً فراخوانی شده) می‌کند.

برای این که این راهبرد کمی ساده‌تر شود، کد می‌تواند مانند زیر باشد:

1const throttled = function(delay, fn) {
2    let lastCall = 0;
3    return (...args) => {
4        let context = this;
5        let current = new Date().getTime();
6        
7        if (current - lastCall < delay) {
8            return;
9        }
10        lastCall = current;
11        
12        return fn.apply(context, ...args);
13    };
14};

البته شما می‌توانید در صورت علاقه، خودتان این کارکردها را بسازید، اما به خاطر داشته باشید که نکته مهم این است که اجرای تابع را تا نقطه بعدی به تأخیر بیندازید.

سخن پایانی

Debounce و Throttle هر دو به دلیل نیاز به ایجاد تأخیر در اجرای تابع به دلیل کاهش تعداد ارسال درخواست‌های HTTP از سوی کاربر ساخته شده‌اند. امروزه این دو روش‌های مهم برای بهبود وب محسوب می‌شوند. می‌تواند از هر کدام از آن‌ها در کارهای مختلف که نیاز به ایجاد تأخیر دارند، مانند رویداد اسکرول کردن استفاده کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۲ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
better-programming
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *