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

آیا تاکنون این تجربه را داشتهاید که صفحه وب در زمان اسکرول کردن، بسیار کُند شود؟ زمانی که از انیمیشنهای canvas در محیط داشبورد استفاده میکنیم، مصرف CPU برای نمایش روان انیمیشنها در هر ثانیه بسیار بالا میرود. این وضعیت در صورتی که بخواهید در زمان حرکت ماوس روی این نمودارها یک tooltip نیز نمایش پیدا کند، وخامت بسیار بیشتری پیدا مییابد. یکی از راهحلهای این وضعیت، استفاده از Debounce و Throttle در جاوا اسکریپت است که در این راهنما بررسی میکنیم. اگر با مفاهیم تابعهای ناهمگام و طرز کار HOF آشنا باشید، درک مطالبی که در ادامه میآید بسیار آسانتر خواهد بود.
بررسی یک مثال
فرض کنید میخواهید از طریق یک فراخوانی API به دنبال نام یک کشور بگردید. یک کادر ورودی روی صفحه وجود دارد که میتوانید نام کشور را در آن وارد کنید. زمانی که رشته را در آن وارد کردید، اپلیکیشن اقدام به فراخوانی API کرده و همه کشورهایی که با رشته ورودی تطبیق یابند، بارگذاری میکند.
وقتی کاربران موبایل یا افراد تنبل تلاش کنند این قابلیت را امتحان کنند، به صورت زیر خواهد بود:
عبارت روی صفحه به این نکته اشاره دارد که چه تعداد درخواستهای HTTP به سرور ارسال شده است. توجه کنید که هر بار که یک حرف کیبورد را میزنید، حتی زمانی که کلید بکاسپیس را میزنید عدد این عبارت افزایش مییابد. این وضعیت در اپلیکیشن ساده ما شاید نامناسب به نظر نرسد، اما تصور کنید روی اپلیکیشن بزرگی مانند AirBnb کار میکنید، در این وضعیت ارسال درخواستها در هر بار که کاربر یک کلید کیبورد را میزند یا کار دیگری انجام میدهد، انجام میشود که موجب کندی عملکرد خواهد شد.
Debounce و Throttle
برای درک مفهوم حقیقی Debounce و Throttle بهتر است ابتدا ایده راهحل مشکل فوق را بررسی کنیم. مشکل اصلی این است که درخواستهای http زیادی ارسال میشوند. فرض کنید میخواهید به دنبال عبارت Canada بگردید اما صفحه وب به نوعی به جهت ارسال درخواستهای C، Ca، Can و Cana و غیره فریز شده است. این وضعیت خوبی نیست. بنابراین باید تعداد درخواستها کاهش یابد.
Debounce و Throttle در واقع صرفاً نامهایی برای شیوه کاهش این درخواستها هستند. نکته مشترک Debounce و Throttle یک مفهوم ساده است: زمانی که انگشت روی کلید میرود تا زمانی که تأیید نشده است، درخواستی ارسال نمیشود.
به شکل فوق توجه کنید. در این تصویر میبینید که بسته به حالتهای مختلف چه اتفاقاتی میافتد. در حالت Normal هر بار که کلیدی فشرده شود، یک درخواست ارسال میشود. در حالت Throttle یک درخواست نخستین بار در طی دوره ارسال میشود و دیگر هیچ درخواستی تا پایان دوره ارسال نمیشود. هنگامی که دوره زمانی خاتمه یابد، یک درخواست جدید مجدداً ارسال میشود. در حالت Debounce یک Callback استفاده میشود و در طی چند میلیثانیه فراخوانی میشود و تنها درخواستی را ارسال میکند که در طی دوره زمانی درخواست دیگری اضافه نشده باشد. برای مثال به زمانی که حرف n در کلمه Canada وارد میشود دقت کنید. یک درخواست دیگر اضافه شده است و درخواست قبلی اضافه شده زمانی که a وارد شود نادیده گرفته میشود. با این منطق، اگر همه درخواستها درون بازه زمانی اضافه شوند، تنها درخواست آخر اجرا خواهد شد.
بررسی کد حالت Throttle
اینک نوبت به نوشتن کد رسیده است
import axios from 'axios
import axios from 'axios'; const search = async (city) => await axios.get(`https://restcountries.eu/rest/v2/name/${city}`)
search تابعی است که نام شهر را با استفاده از API به همین نام ارسال میکند. از API واقعی restful برای تست این مثال در عمل استفاده کردهایم. تنها کاری که باید انجام دهیم این است که کادر City را با هر نامی که دوست دارید، پر کنید. ابتدا کد را بررسی میکنیم:
const throttle = (delay, fn) => { let inThrottle = false; return args => { if (inThrottle) { return; } inThrottle = true; fn(args); setTimeout(() => { inThrottle = false; }, delay); }; };
تابع بینام یک HOF (تابع مرتبه بالاتر) است که تابع دیگری را بازگشت میدهد. زمانی که این تابع برای نخستین بار فراخوانی میشود، inThrottle مقدار false میگیرد. سپس زمانی که تابع بازگشتی دوباره فراخوانی میشود inThrottle مقدار true میگیرد و تابع callback یعنی fn اجرا میشود. در ادامه inThrottle دوباره در طی زمان delay میلیثانیه مقدار false میگیرد. در این زمان در عین این که inThrottle مقدار true دارد، هیچ کدام از توابع بازگشتی از throttle نمیتوانند اجرا شوند.
چنان که در تصویر فوق میبینید، حتی اگر انگشت خود را روی دکمه نگه دارید، امکان اجرای تابع را نمیدهد مگر این که inThrottle مقدار false بگیرد. سپس متد جدید برای درخواست کردن به صورت زیر درمیآید:
const sendRequestThrottle = throttle(500, search); <input type="text" onChange={sendRequestThrottle} />
اما یک نکته هست که باید مراقب آن باشیم. متد HOF به نام throttle نباید در کامپوننت React قرار گیرد. زمانی که متغیرهای حالت که در کامپوننت A هستند، تغییر مییابند، A رندر مجدد میشود و. همه چیز از نو انتساب مییابد. اگر throttle هم رندر مجدد شود inThrottle دوباره مقدار false خواهد گرفت.
بررسی کد Debounce
اینک به بررسی Debounce میپردازیم. این کد کاملاً مشابه کد Throttle است و بخش غالب آن را به سادگی درک میکنید.
const debounce = (delay, fn) => { let inDebounce = null; return args => { clearTimeout(inDebounce); inDebounce = setTimeout(() => fn(args), delay); } }
میبینید که بسیار ساده است. تنها تفاوت با Throttle در این است که Debounce بررسی نمیکند، آیا inDebounce مقدار true دارد یا نه. اگر Callback درون دوره زمانی مورد نظر اجرا شده باشد، setTimeout قبلی لغو میشود و بیدرنگ اجرا شده و یک setTimeout جدید ایجاد میکند. بنابراین اگر به فشردن کیبورد به صورت سریع ادامه بدهید، callback هرگز اجرا نخواهد شد
اکنون میتوانید فراخوانیهای Debounce را بهتر درک کنید. تنها کاری که باید انجام دهید این است که callback خود را در DOM ثبت کنید:
const sendRequestDebounce = debounce(500, search); <input type="text" onChange={sendRequestDebounce} />
Throttle و Debounce در Lodash
در اغلب موارد نیازی به ایجاد یک Throttle یا Debounce وجود ندارد، زیرا کتابخانههای سبک زیای وجود دارند که این قابلیتها را عرضه میکنند. یک نمونه از آن کتابخانه Lodash است:
function debounce(func, wait, options) { let lastArgs, lastThis, maxWait, result, timerId, lastCallTime let lastInvokeTime = 0 let leading = false let maxing = false let trailing = true // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function') if (typeof func !== 'function') { throw new TypeError('Expected a function') } wait = +wait || 0 if (isObject(options)) { leading = !!options.leading maxing = 'maxWait' in options maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait trailing = 'trailing' in options ? !!options.trailing : trailing } function invokeFunc(time) { const args = lastArgs const thisArg = lastThis lastArgs = lastThis = undefined lastInvokeTime = time result = func.apply(thisArg, args) return result } function startTimer(pendingFunc, wait) { if (useRAF) { root.cancelAnimationFrame(timerId) return root.requestAnimationFrame(pendingFunc) } return setTimeout(pendingFunc, wait) } function cancelTimer(id) { if (useRAF) { return root.cancelAnimationFrame(id) } clearTimeout(id) } function leadingEdge(time) { // Reset any `maxWait` timer. lastInvokeTime = time // Start the timer for the trailing edge. timerId = startTimer(timerExpired, wait) // Invoke the leading edge. return leading ? invokeFunc(time) : result } function remainingWait(time) { const timeSinceLastCall = time - lastCallTime const timeSinceLastInvoke = time - lastInvokeTime const timeWaiting = wait - timeSinceLastCall return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting } function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime const timeSinceLastInvoke = time - lastInvokeTime // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)) } function timerExpired() { const time = Date.now() if (shouldInvoke(time)) { return trailingEdge(time) } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time)) } function trailingEdge(time) { timerId = undefined // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) { return invokeFunc(time) } lastArgs = lastThis = undefined return result } function cancel() { if (timerId !== undefined) { cancelTimer(timerId) } lastInvokeTime = 0 lastArgs = lastCallTime = lastThis = timerId = undefined } function flush() { return timerId === undefined ? result : trailingEdge(Date.now()) } function pending() { return timerId !== undefined } function debounced(...args) { const time = Date.now() const isInvoking = shouldInvoke(time) lastArgs = args lastThis = this lastCallTime = time if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime) } if (maxing) { // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait) return invokeFunc(lastCallTime) } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait) } return result } debounced.cancel = cancel debounced.flush = flush debounced.pending = pending return debounced }
کد آن کاملاً طولانی است، اما کافی است به تابع debounced که یک HOF است توجه کنید:
function debounce() { let lastCallTime; ... function debounced(...args) { ... } return debounced; }
Debounce یک تابع جدید به نام debounced بازگشت میدهد که در عمل اجرا خواهد شد. درون این debounced از استفاده میکنیم. بنابراین شاید متوجه شوید که debounce در Lodash اقدام به مقایسه زمان قبلی (که تابع قبلاً فراخوانی شده) با زمان جاری (که تابع فعلاً فراخوانی شده) میکند.
برای این که این راهبرد کمی سادهتر شود، کد میتواند مانند زیر باشد:
const throttled = function(delay, fn) { let lastCall = 0; return (...args) => { let context = this; let current = new Date().getTime(); if (current - lastCall < delay) { return; } lastCall = current; return fn.apply(context, ...args); }; };
البته شما میتوانید در صورت علاقه، خودتان این کارکردها را بسازید، اما به خاطر داشته باشید که نکته مهم این است که اجرای تابع را تا نقطه بعدی به تأخیر بیندازید.
سخن پایانی
Debounce و Throttle هر دو به دلیل نیاز به ایجاد تأخیر در اجرای تابع به دلیل کاهش تعداد ارسال درخواستهای HTTP از سوی کاربر ساخته شدهاند. امروزه این دو روشهای مهم برای بهبود وب محسوب میشوند. میتواند از هر کدام از آنها در کارهای مختلف که نیاز به ایجاد تأخیر دارند، مانند رویداد اسکرول کردن استفاده کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوا اسکریپت)
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
- طراحی تایمر معکوس با HTML ،CSS و JavaScript — به زبان ساده
==