摘要:舉例舉例通過(guò)拖拽瀏覽器窗口,可以觸發(fā)很多次事件。不支持,所以不能在服務(wù)端用于文件系統(tǒng)事件。總結(jié)將一系列迅速觸發(fā)的事件例如敲擊鍵盤合并成一個(gè)多帶帶的事件。確保一個(gè)持續(xù)的操作流以每毫秒執(zhí)行一次的速度執(zhí)行。
Debounce 和 Throttle 是兩個(gè)很相似但是又不同的技術(shù),都可以控制一個(gè)函數(shù)在一段時(shí)間內(nèi)執(zhí)行的次數(shù)。
當(dāng)我們?cè)诓僮?DOM 事件的時(shí)候,為函數(shù)添加 debounce 或者 throttle 就會(huì)尤為有用。為什么?因?yàn)槲覀冊(cè)谑录秃瘮?shù)執(zhí)行之間加了一個(gè)我們自己的控制層。記住,我們是不去控制這些 DOM 事件觸發(fā)的頻率的,因?yàn)檫@個(gè)可能會(huì)有變化。
下面我們以滾動(dòng)事件舉例:
當(dāng)使用觸控板、鼠標(biāo)滾輪,或者直接拽動(dòng)滾動(dòng)條,每秒都可以輕易觸發(fā)至少30次事件,而且在觸屏的移動(dòng)端,甚至?xí)_(dá)到每秒100次,面對(duì)這樣高的執(zhí)行頻率,你的滾動(dòng)事件處理程序能否很好地應(yīng)對(duì)?
在2011年,Twitter 網(wǎng)站提出了一個(gè) issue:當(dāng)向下滾動(dòng) Twitter 信息流的時(shí)候,整個(gè)頁(yè)面的響應(yīng)速度都會(huì)變慢。 John Resig 基于該問(wèn)題發(fā)表了一篇博客,文中指出,直接在 scroll 事件里掛載一些計(jì)算量大的函數(shù)是件多么不明智的行為。
John 當(dāng)時(shí)提出的解決方案是在 onScroll event 的外部設(shè)置一個(gè)每 250ms 執(zhí)行一次的循環(huán)。這樣處理程序就與事件解耦了。使用這樣一個(gè)簡(jiǎn)單的技術(shù)就可以避免破壞用戶體驗(yàn)。
::: tip 譯者注
文中的核心代碼如下
var outerPane = $details.find(".details-pane-outer"), didScroll = false; $(window).scroll(function() { didScroll = true; }); setInterval(function() { if ( didScroll ) { didScroll = false; // Check your page position and then // Load in more results } }, 250);
:::
如今,處理事件的方式稍微復(fù)雜了一些。下面我們結(jié)合用例,一一介紹 Debounce、 Throttle 和requestAnimationFrame。
DebounceDebounce 允許我們將多個(gè)連續(xù)的調(diào)用合并成一個(gè)。
想象一個(gè)進(jìn)電梯的場(chǎng)景,你走進(jìn)了電梯,門剛要關(guān)上,這時(shí)另一個(gè)人想要進(jìn)來(lái),于是電梯沒(méi)有移動(dòng)樓層(處理函數(shù)),而是將門打開讓那個(gè)人進(jìn)來(lái)。這時(shí)又有一個(gè)人要進(jìn)來(lái),就又會(huì)上演剛才那一幕。也就是說(shuō),電梯延遲了它的函數(shù)(移動(dòng)樓層)執(zhí)行,但是優(yōu)化了資源。
在下面的例子中,嘗試快速點(diǎn)擊按鈕或者在上面滑動(dòng):
你可以看到連續(xù)快速事件是怎樣被一個(gè)多帶帶的 debounce 事件所替代的。但是如果事件觸發(fā)時(shí)間間隔較長(zhǎng),就不會(huì)發(fā)生 debounce。
Leading 邊緣 (或者 "immediate")在上面的例子中,你會(huì)發(fā)現(xiàn) debounce 事件會(huì)等到快速事件停止發(fā)生后才會(huì)觸發(fā)函數(shù)執(zhí)行。為什么不在每次一開始就立即觸發(fā)函數(shù)執(zhí)行呢,這樣它的表現(xiàn)就和原始的沒(méi)有去抖的處理器一樣了。直到快速調(diào)用出現(xiàn)停頓的時(shí)候,才會(huì)再次觸發(fā)。
下面是使用 leading 標(biāo)識(shí)符的例子:
在 underscore.js 中,該選項(xiàng)叫作 immediate ,而不是 leading。
自己試一下:
Debounce 的實(shí)現(xiàn)Debounce 的概念和實(shí)現(xiàn)最早是由 John Hann 在2009年提出來(lái)的。
不久之后,Ben Alman 就寫了一個(gè) jQuery 插件(現(xiàn)在已經(jīng)不再維護(hù)了),一年之后 Jeremy Ashkenas 把它添加進(jìn)了 underscore.js。再后來(lái)被添加進(jìn) Lodash。
這三個(gè)實(shí)現(xiàn)在內(nèi)部有一點(diǎn)不同,但是接口幾乎是相同的。
曾經(jīng)有一段時(shí)間,underscore 采取了 Lodash 里面的 debounce/throttle 實(shí)現(xiàn),但是后來(lái)我在2013年發(fā)現(xiàn)了 _.debounce 函數(shù)的一個(gè) bug。從那時(shí)起,這兩種實(shí)現(xiàn)就出現(xiàn)分化了。
Lodash 為 _.debounce 和 _.throttle 添加了更多的特性。最初的 immediate 標(biāo)識(shí)符被 leading 和 trailing 所替代。你可以選擇一個(gè)選項(xiàng),也可以兩個(gè)都要。默認(rèn)情況下 trailing 是被開啟的。
新的 maxWait 選項(xiàng)(目前只存在于Lodash)在本文中沒(méi)有提及,但是它也是一個(gè)很有用的選項(xiàng)。實(shí)際上,throttle 函數(shù)就是使用 _.debounce 帶著 maxWait 的選項(xiàng)來(lái)定義的,你可以在這里查看源碼。
Debounce 舉例通過(guò)拖拽瀏覽器窗口,可以觸發(fā)很多次 resize 事件。
例子如下:
可以看到,我們?cè)?resize 事件上使用的是默認(rèn)的 trailing 選項(xiàng),因?yàn)槲覀冎恍枰P(guān)心用戶停止調(diào)整瀏覽器后的最終結(jié)果就可以了。
為什么要在用戶還在輸入的時(shí)候每隔 50ms 就發(fā)送一次 Ajax請(qǐng)求?_.debounce 可以幫助我們避免額外的開銷,只有當(dāng)用戶停止輸入了再發(fā)送請(qǐng)求。
這里沒(méi)有必要設(shè)置 leading,我們是想要等到最后一個(gè)字符輸入完再執(zhí)行函數(shù)的。
還有一個(gè)類似的使用場(chǎng)景就是表單校驗(yàn),當(dāng)用戶輸入完再進(jìn)行校驗(yàn)、提示信息等。
說(shuō)了這么多,你可能已經(jīng)想自己來(lái)寫 debounce/throttle 函數(shù)了,或者是從網(wǎng)上隨便一篇博客上拷貝一份下來(lái)。但是我給你的建議是直接使用 underscore 或者 Lodash。 如果你只是需要 _.debounce 和 _.throttle 函數(shù),可以使用 Lodash custom builder 來(lái)輸出一個(gè)自定義的壓縮后為 2KB 的庫(kù)??梢允褂孟铝忻顏?lái)進(jìn)行構(gòu)建:
npm i -g lodash-cli lodash include = debounce, throttle
也就是說(shuō),最好是使用模塊化的形式,通過(guò) webpack/browserify/rollup 來(lái)引用,如 lodash/throttle 和 lodash/debounce 或 lodash.throttle 和 lodash.debounce 。
使用 _.debounce 函數(shù)的一個(gè)常見(jiàn)錯(cuò)誤就是多次調(diào)用它:
// 錯(cuò)誤 $(window).on("scroll", function() { _.debounce(doSomething, 300); }); // 正確 $(window).on("scroll", _.debounce(doSomething, 200));
為 debounced 函數(shù)創(chuàng)建一個(gè)變量可以讓我們調(diào)用私有函數(shù) debounced_version.cancel(),如果有需要,lodash 和 underscore.js 都可以供你使用。
var debounced_version = _.debounce(doSomething, 200); $(window).on("scroll", debounced_version); // 如果你需要的話 debounced_version.cancel();Throttle
使用 _.throttle 則不允許函數(shù)每 X 毫秒的執(zhí)行次數(shù)超過(guò)一次。
Throttle 和 debounce 最主要的區(qū)別就是 throttle 保證函數(shù)每 X 毫秒至少執(zhí)行一次。
和 debounce 一樣, throttle 也用在了 Ben 的插件、underscore.js 和 lodash里面。
Throttling 舉例這是一個(gè)非常常見(jiàn)的例子。用戶在一個(gè)無(wú)限滾動(dòng)的頁(yè)面里向下滾動(dòng),你需要知道當(dāng)前滾動(dòng)的位置距離底部還有多遠(yuǎn),如果接近底部了,我們就得通過(guò) Ajax 請(qǐng)求獲取更多的內(nèi)容,將其添加到頁(yè)面里。
此時(shí)我們之前的 _.debounce 就派不上作用了。使用 debounce 只有當(dāng)用戶停止?jié)L動(dòng)時(shí)才能觸發(fā),而我們需要的是在用戶滾動(dòng)到底部之前就開始獲取內(nèi)容。
使用 _.throttle 就能確保實(shí)時(shí)檢查距離底部還有多遠(yuǎn)。
requestAnimationFrame (rAF)requestAnimationFrame 是另一種限制函數(shù)執(zhí)行速度的方法。
它可以被看做 _.throttle(dosomething, 16)。但它有著更高的保真度,因?yàn)樗菫g覽器的原生 API,有著更好的精度。
我們可以使用 rAF API,作為 throttle 函數(shù)的替代,考慮下面的優(yōu)缺點(diǎn):
優(yōu)點(diǎn)目標(biāo)是 60fps(每幀 16ms),但是會(huì)在瀏覽器內(nèi)部決定如何安排渲染的最佳時(shí)機(jī)。
非常簡(jiǎn)單,而且是標(biāo)準(zhǔn) API,在未來(lái)也不會(huì)改變。更少的維護(hù)成本。
缺點(diǎn)rAFs 的開始/取消由我們自己來(lái)管理,而不像 .debounce 和 .throttle 是在內(nèi)部管理的。
如果瀏覽器的 tab 頁(yè)面不活躍了,它就不會(huì)再執(zhí)行。
雖然所有的現(xiàn)代瀏覽器都提供了 rAF, 但是 IE9、Opera Mini 和一些老的安卓版本還不支持。如果需要,現(xiàn)在還是要使用 polyfill 。
Node.js 不支持 rAF,所以不能在服務(wù)端用于 throttle 文件系統(tǒng)事件。
根據(jù)經(jīng)驗(yàn),如果你的 JavaScript 函數(shù)是在繪制或者直接改變屬性,所有涉及到元素位置重新計(jì)算的,我會(huì)建議使用 requestAnimationFrame,
如果是處理 Ajax 請(qǐng)求,或者決定是否添加/刪除某個(gè) class(可能會(huì)觸發(fā)一個(gè) CSS 動(dòng)畫),我會(huì)考慮 _.debounce 和 _.throttle,這里可以設(shè)置更低一些的執(zhí)行速度(例如 200ms,而不是16ms)。
這時(shí)你可能會(huì)想,為什不把 rAF 集成到 underscore 或 lodash 里呢,那他倆都是拒絕的,因?yàn)檫@只是一個(gè)特殊的使用場(chǎng)景,而且已經(jīng)足夠簡(jiǎn)單,可以被直接調(diào)用。
rAF 舉例受這篇文章的啟發(fā),在這里我會(huì)舉一個(gè)滾動(dòng)的例子,在這篇文章中有每個(gè)步驟的邏輯解釋。
我做了一個(gè)對(duì)比實(shí)驗(yàn),一邊是 rAF,一邊是 16ms 間隔的 _.throttle。它們性能很相似,但是 rAF 可能會(huì)在更復(fù)雜的場(chǎng)景下性能更高一些。
See the Pen Scroll comparison requestAnimationFrame vs throttle