第7题:节流函数的作用是什么?应用场景有哪些 如何实现
节流概念
节流跟防抖,它们既有相似之处但又有所不同,很容易混淆。这里通过两个比喻来加深理解,先来说说节流。
节流的概念可以想象一下水坝,你建了水坝在河道中,不能让水流动不了,你只能让水流慢些。换言之,你不能让用户的方法都不执行。(个人比较喜欢这个比喻,因为它很形象的说出了跟防抖的区别。)
节流函数的作用
节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。
举例说明:小明的妈妈和小明约定好,如果小明在周考中取得满分,那么当月可以带他去游乐场玩,但是一个月最多只能去一次。
这其实就是一个节流的例子,在一个月的时间内,去游乐场最多只能触发一次。即使这个时间周期内,小明取得多次满分。
节流应用场景
1.按钮点击事件
2.拖拽事件
3.onScoll
4.计算鼠标移动的距离(mousemove)
节流函数实现
利用时间戳实现
<html> <head></head> <body> <span class="token keyword">function</span> <span class="token function">throttle</span> <span class="token punctuation">(</span>func <span class="token punctuation">,</span> delay <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> lastTime <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">throttled</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> context <span class="token operator">=</span> <span class="token keyword">this</span> <span class="token punctuation">;</span> <span class="token keyword">var</span> args <span class="token operator">=</span> arguments <span class="token punctuation">;</span> <span class="token keyword">var</span> nowTime <span class="token operator">=</span> Date <span class="token punctuation">.</span> <span class="token function">now</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>nowTime <span class="token operator">></span> lastTime <span class="token operator">+</span> delay <span class="token punctuation">)</span> <span class="token punctuation">{</span> func <span class="token punctuation">.</span> <span class="token function">apply</span> <span class="token punctuation">(</span>context <span class="token punctuation">,</span> args <span class="token punctuation">)</span> <span class="token punctuation">;</span> lastTime <span class="token operator">=</span> nowTime <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment">//防抖函数最终返回的是一个函数</span> <span class="token keyword">return</span> throttled <span class="token punctuation">;</span> <span class="token punctuation">}</span> </body> </html>
利用定时器实现
<html> <head></head> <body> <span class="token keyword">function</span> <span class="token function">throttle</span> <span class="token punctuation">(</span>func <span class="token punctuation">,</span> delay <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> timeout <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">;</span> <span class="token keyword">function</span> <span class="token function">throttled</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">var</span> context <span class="token operator">=</span> <span class="token keyword">this</span> <span class="token punctuation">;</span> <span class="token keyword">var</span> args <span class="token operator">=</span> arguments <span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span> <span class="token operator">!</span>timeout <span class="token punctuation">)</span> <span class="token punctuation">{</span> timeout <span class="token operator">=</span> <span class="token function">setTimeout</span> <span class="token punctuation">(</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> func <span class="token punctuation">.</span> <span class="token function">apply</span> <span class="token punctuation">(</span>context <span class="token punctuation">,</span> args <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token function">clearTimeout</span> <span class="token punctuation">(</span>timeout <span class="token punctuation">)</span> <span class="token punctuation">;</span> timeout <span class="token operator">=</span> <span class="token keyword">null</span> <span class="token punctuation">}</span> <span class="token punctuation">,</span> delay <span class="token punctuation">)</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> throttled <span class="token punctuation">;</span> <span class="token punctuation">}</span> </body> </html>
时间戳和定时器的方式都没有考虑最后一次执行的问题,比如有个按钮点击事件,设置的间隔时间是 1S,在第 0.5S,1.8S,2.2S 点击,那么只有 0.5S 和 1.8S 的两次点击能够触发函数执行,而最后一次的 2.2S 会被忽略。
组合实现,允许设置第一次或者最后一次是否触发函数执行
function throttle (func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function () { previous = options.leading === false ? 0 : Date.now() || new Date().getTime(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function () { var now = Date.now() || new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 判断是否设置了定时器和 trailing timeout = setTimeout(later, remaining); } return result; }; throttled.cancel = function () { clearTimeout(timeout); previous = 0; timeout = context = args = null; }; return throttled; }
使用很简单:
btn.onclick = throttle(handle, 1000, {leading:true, trailing: true});
以判断页面是否滚动到底部为例,普通的做法就是监听 window 对象的 scroll 事件,然后再函数体中写入判断是否滚动到底部的逻辑:
<html> <head></head> <body> <span class="pl-en">$</span>( <span class="pl-c1">window</span>). <span class="pl-en">on</span>( <span class="pl-s"><span class="pl-pds">'</span>scroll<span class="pl-pds">'</span></span>, <span class="pl-k">function</span>() { <span class="pl-c">// 判断是否滚动到底部的逻辑</span> <span class="pl-k">let</span> pageHeight <span class="pl-k">=</span> <span class="pl-en">$</span>( <span class="pl-s"><span class="pl-pds">'</span>body<span class="pl-pds">'</span></span>). <span class="pl-c1">height</span>(), scrollTop <span class="pl-k">=</span> <span class="pl-en">$</span>( <span class="pl-c1">window</span>). <span class="pl-en">scrollTop</span>(), winHeight <span class="pl-k">=</span> <span class="pl-en">$</span>( <span class="pl-c1">window</span>). <span class="pl-c1">height</span>(), thresold <span class="pl-k">=</span> pageHeight <span class="pl-k">-</span> scrollTop <span class="pl-k">-</span> winHeight; <span class="pl-k">if</span> (thresold <span class="pl-k">></span> <span class="pl-k">-</span> <span class="pl-c1">100</span> <span class="pl-k">&&</span> thresold <span class="pl-k"><=</span> <span class="pl-c1">20</span>) { <span class="pl-en">console</span>. <span class="pl-c1">log</span>( <span class="pl-s"><span class="pl-pds">'</span>end<span class="pl-pds">'</span></span>); } }); </body> </html>
如下图:
这样做的一个缺点就是比较消耗性能,因为当在滚动的时候,浏览器会无时不刻地在计算判断是否滚动到底部的逻辑,而在实际的场景中是不需要这么做的,在实际场景中可能是这样的:在滚动过程中,每隔一段时间在去计算这个判断逻辑。而函数节流所做的工作就是每隔一段时间去执行一次原本需要无时不刻地在执行的函数,所以在滚动事件中引入函数的节流是一个非常好的实践:
<html> <head></head> <body> <span class="pl-c">// 添加节流函数</span> <span class="pl-k">function</span> <span class="pl-en">throttle</span>( <span class="pl-smi">fn</span>, <span class="pl-smi">interval</span> <span class="pl-k">=</span> <span class="pl-c1">300</span>) { <span class="pl-k">let</span> canRun <span class="pl-k">=</span> <span class="pl-c1">true</span>; <span class="pl-k">return</span> <span class="pl-k">function</span>() { <span class="pl-k">if</span> ( <span class="pl-k">!</span>canRun) <span class="pl-k">return</span>; canRun <span class="pl-k">=</span> <span class="pl-c1">false</span>; <span class="pl-c1">setTimeout</span>(() <span class="pl-k">=></span> { <span class="pl-smi">fn</span>. <span class="pl-c1">apply</span>( <span class="pl-c1">this</span>, <span class="pl-c1">arguments</span>); canRun <span class="pl-k">=</span> <span class="pl-c1">true</span>; }, interval); }; } <span class="pl-en">$</span>( <span class="pl-c1">window</span>). <span class="pl-en">on</span>( <span class="pl-s"><span class="pl-pds">'</span>scroll<span class="pl-pds">'</span></span>, <span class="pl-en">throttle</span>( <span class="pl-k">function</span>() { <span class="pl-c">// 判断是否滚动到底部的逻辑</span> <span class="pl-k">let</span> pageHeight <span class="pl-k">=</span> <span class="pl-en">$</span>( <span class="pl-s"><span class="pl-pds">'</span>body<span class="pl-pds">'</span></span>). <span class="pl-c1">height</span>(), scrollTop <span class="pl-k">=</span> <span class="pl-en">$</span>( <span class="pl-c1">window</span>). <span class="pl-en">scrollTop</span>(), winHeight <span class="pl-k">=</span> <span class="pl-en">$</span>( <span class="pl-c1">window</span>). <span class="pl-c1">height</span>(), thresold <span class="pl-k">=</span> pageHeight <span class="pl-k">-</span> scrollTop <span class="pl-k">-</span> winHeight; <span class="pl-k">if</span> (thresold <span class="pl-k">></span> <span class="pl-k">-</span> <span class="pl-c1">100</span> <span class="pl-k">&&</span> thresold <span class="pl-k"><=</span> <span class="pl-c1">20</span>) { <span class="pl-en">console</span>. <span class="pl-c1">log</span>( <span class="pl-s"><span class="pl-pds">'</span>end<span class="pl-pds">'</span></span>); } }) ); </body> </html>
如图:
加上函数节流之后,当页面再滚动的时候,每隔 300ms 才会去执行一次判断逻辑。
简单来说,函数的节流就是通过闭包保存一个标记(canRun=true),在函数的开头判断这个标记是否为 true,如果为 true 的话就继续执行函数,否则则 return 掉,判断完标记后立即把这个标记设为 false,然后把外部传入的函数的执行包在一个 setTimeout 中,最后在 setTimeout 执行完毕后再把标记设置为 true(这里很关键),表示可以执行下一次的循环了。当 setTimeout 还未执行的时候,canRun 这个标记始终为 false,在开头的判断中被 return 掉。
防抖和节流都是为了避免事件的频繁触发,有些事件无论是防抖还是节流都做到性能上的优化,要根据业务需求选择合适的方案。
具体参考我之前写的文章:《第 6 题:什么是防抖和节流 有什么区别 如何实现》
结语
我的回答如有不对的地方,麻烦务必指出来,我及时改正,以免误导别人,让我们共同进步吧!
码云笔记 » 第7题:节流函数的作用是什么?应用场景有哪些 如何实现