每日源码分析 - lodash(debounce.js和throttle.js)
本系列使用 lodash 4.17.4
前言
本文件引用了isObject函數(shù)
import isObject from './isObject.js' 判斷變量是否是廣義的對象(對象、數(shù)組、函數(shù)), 不包括null
正文
import isObject from './isObject.js'/*** Creates a debounced function that delays invoking `func` until after `wait`* milliseconds have elapsed since the last time the debounced function was* invoked. The debounced function comes with a `cancel` method to cancel* delayed `func` invocations and a `flush` method to immediately invoke them.* Provide `options` to indicate whether `func` should be invoked on the* leading and/or trailing edge of the `wait` timeout. The `func` is invoked* with the last arguments provided to the debounced function. Subsequent* calls to the debounced function return the result of the last `func`* invocation.** **Note:** If `leading` and `trailing` options are `true`, `func` is* invoked on the trailing edge of the timeout only if the debounced function* is invoked more than once during the `wait` timeout.** If `wait` is `0` and `leading` is `false`, `func` invocation is deferred* until the next tick, similar to `setTimeout` with a timeout of `0`.** See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)* for details over the differences between `debounce` and `throttle`.** @since 0.1.0* @category Function* @param {Function} func The function to debounce.* @param {number} [wait=0] The number of milliseconds to delay.* @param {Object} [options={}] The options object.* @param {boolean} [options.leading=false]* Specify invoking on the leading edge of the timeout.* @param {number} [options.maxWait]* The maximum time `func` is allowed to be delayed before it's invoked.* @param {boolean} [options.trailing=true]* Specify invoking on the trailing edge of the timeout.* @returns {Function} Returns the new debounced function.* @example** // Avoid costly calculations while the window size is in flux.* jQuery(window).on('resize', debounce(calculateLayout, 150))** // Invoke `sendMail` when clicked, debouncing subsequent calls.* jQuery(element).on('click', debounce(sendMail, 300, {* 'leading': true,* 'trailing': false* }))** // Ensure `batchLog` is invoked once after 1 second of debounced calls.* const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })* const source = new EventSource('/stream')* jQuery(source).on('message', debounced)** // Cancel the trailing debounced invocation.* jQuery(window).on('popstate', debounced.cancel)** // Check for pending invocations.* const status = debounced.pending() ? "Pending..." : "Ready"*/ function debounce(func, wait, options) {let lastArgs,lastThis,maxWait,result,timerId,lastCallTimelet lastInvokeTime = 0let leading = falselet maxing = falselet trailing = trueif (typeof func != 'function') {throw new TypeError('Expected a function')}wait = +wait || 0if (isObject(options)) {leading = !!options.leadingmaxing = 'maxWait' in optionsmaxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWaittrailing = 'trailing' in options ? !!options.trailing : trailing}function invokeFunc(time) {const args = lastArgsconst thisArg = lastThislastArgs = lastThis = undefinedlastInvokeTime = timeresult = func.apply(thisArg, args)return result}function leadingEdge(time) {// Reset any `maxWait` timer.lastInvokeTime = time// Start the timer for the trailing edge.timerId = setTimeout(timerExpired, wait)// Invoke the leading edge.return leading ? invokeFunc(time) : result}function remainingWait(time) {const timeSinceLastCall = time - lastCallTimeconst timeSinceLastInvoke = time - lastInvokeTimeconst timeWaiting = wait - timeSinceLastCallreturn maxing? Math.min(timeWaiting, maxWait - timeSinceLastInvoke): timeWaiting}function shouldInvoke(time) {const timeSinceLastCall = time - lastCallTimeconst 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 = setTimeout(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 = undefinedreturn result}function cancel() {if (timerId !== undefined) {clearTimeout(timerId)}lastInvokeTime = 0lastArgs = 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 = argslastThis = thislastCallTime = timeif (isInvoking) {if (timerId === undefined) {return leadingEdge(lastCallTime)}if (maxing) {// Handle invocations in a tight loop.timerId = setTimeout(timerExpired, wait)return invokeFunc(lastCallTime)}}if (timerId === undefined) {timerId = setTimeout(timerExpired, wait)}return result}debounced.cancel = canceldebounced.flush = flushdebounced.pending = pendingreturn debounced }export default debounce復(fù)制代碼使用方式
函數(shù)防抖(debounce)
函數(shù)防抖(debounce)和函數(shù)節(jié)流(throttle)相信有一定前端基礎(chǔ)的應(yīng)該都知道,不過還是簡單說一下
防抖(debounce)就是把多個(gè)順序的調(diào)用合并到一起(只執(zhí)行一次),這在某些情況下對性能會(huì)有極大的優(yōu)化(后面使用場景會(huì)說幾個(gè))。
圖片來自css-tricks
在lodash的options中提供了一個(gè)leading屬性,這個(gè)屬性讓其在開始的時(shí)候觸發(fā)。
圖片來自css-tricks
// debounce函數(shù)的簡單使用 var log = function() {console.log("log after stop moving"); } document.addEventListener('mousemove', debounce(log, 500)) 復(fù)制代碼函數(shù)節(jié)流(throttle)
使用throttle時(shí),只允許一個(gè)函數(shù)在 X 毫秒內(nèi)執(zhí)行一次。
比如你設(shè)置了400ms,那么即使你在這400ms里面調(diào)用了100次,也只有一次執(zhí)行。跟 debounce 主要的不同在于,throttle 保證 X 毫秒內(nèi)至少執(zhí)行一次。
在lodash的實(shí)現(xiàn)中,throttle主要借助了debounce來實(shí)現(xiàn)。
// throttle函數(shù)的簡單使用 var log = function() {console.log("log every 500ms"); } document.addEventListener('mousemove', throttle(log, 500)) 復(fù)制代碼使用場景
我盡量總結(jié)一下debounce和throttle函數(shù)實(shí)際的應(yīng)用場景
防抖(debounce)
1. 自動(dòng)補(bǔ)全(autocomplete)性能優(yōu)化
自動(dòng)補(bǔ)全很多地方都有,基本無一例外都是通過發(fā)出異步請求將當(dāng)前內(nèi)容作為參數(shù)傳給服務(wù)器,然后服務(wù)器回傳備選項(xiàng)。那么問題來了,如果我每輸入一個(gè)字符都要發(fā)出個(gè)異步請求,那么異步請求的個(gè)數(shù)會(huì)不會(huì)太多了呢?因?yàn)閷?shí)際上用戶可能只需要輸入完后給出的備選項(xiàng)。
這時(shí)候就可以使用防抖,比如當(dāng)輸入框input事件觸發(fā)隔了1000ms的時(shí)候我再發(fā)起異步請求。
2. 原生事件性能優(yōu)化
想象一下,我有個(gè)使用js進(jìn)行自適應(yīng)的元素,那么很自然,我需要考慮我瀏覽器窗口發(fā)生resize事件的時(shí)候我要去重新計(jì)算它的位置。現(xiàn)在問題來了,我們看看resize一次觸發(fā)多少次。
window.addEventListener('resize', function() {console.log('resize') }) 復(fù)制代碼至少在我電腦上,稍微改變一下就會(huì)觸發(fā)幾次resize事件,而用js去自適應(yīng)的話會(huì)有較多的DOM操作,我們都知道DOM操作很浪費(fèi)時(shí)間,所以對于resize事件我們是不是可以用debounce讓它最后再計(jì)算位置?當(dāng)然如果你覺得最后才去計(jì)算位置或者一些屬性會(huì)不太即時(shí),你可以繼續(xù)往下看看函數(shù)節(jié)流(throttle)
節(jié)流(throttle)
和防抖一樣,節(jié)流也可以用于原生事件的優(yōu)化。我們看下面幾個(gè)例子
圖片懶加載
圖片懶加載(lazyload)可能很多人都知道,如果我們?yōu)g覽一個(gè)圖片很多的網(wǎng)站的話,我們不希望所有的圖片在一開始就加載了,一是浪費(fèi)流量,可能用戶不關(guān)心下面的圖片呢。二是性能,那么多圖片一起下載,性能爆炸。那么一般我們都會(huì)讓圖片懶加載,讓一個(gè)圖片一開始在頁面中的標(biāo)簽為
<img src="#" data-src="我是真正的src"> 復(fù)制代碼當(dāng)我屏幕滾動(dòng)到能顯示這個(gè)img標(biāo)簽的位置時(shí),我用data-src去替換src的內(nèi)容,變?yōu)?/p> <img src="我是真正的src" data-src="我是真正的src"> 復(fù)制代碼
大家都知道如果直接改變src的話瀏覽器也會(huì)直接發(fā)出一個(gè)請求,在紅寶書(JS高程)里面的跨域部分還提了一下用img標(biāo)簽的src做跨域。這時(shí)候圖片才會(huì)顯示出來。
關(guān)于怎么判斷一個(gè)元素出現(xiàn)在屏幕中的,大家可以去看看這個(gè)函數(shù)getBoundingClientRect(),這里就不擴(kuò)展的講了
好的,那么問題來了,我既然要檢測元素是否在瀏覽器內(nèi),那我肯定得在scroll事件上綁定檢測函數(shù)吧。scroll函數(shù)和resize函數(shù)一樣,滑動(dòng)一下事件觸發(fā)幾十上百次,讀者可以自己試一下。
document.addEventListener('scroll', function() {console.log('scroll') }) 復(fù)制代碼好的,你的檢測元素是否在瀏覽器內(nèi)的函數(shù)每次要檢查所有的img標(biāo)簽(至少是所有沒有替換src的),而且滑一次要執(zhí)行幾十次,你懂我的意思。
throttle正是你的救星,你可以讓檢測函數(shù)每300ms運(yùn)行一次。
拖動(dòng)和拉伸
你以為你只需要防備resize和scroll么,太天真了,看下面幾個(gè)例子。
或者想做類似原生窗口調(diào)整大小的效果 那么你一定會(huì)需要mousedown、mouseup、mousemove事件,前兩個(gè)用于拖動(dòng)的開始和結(jié)束時(shí)的狀態(tài)變化(比如你要加個(gè)標(biāo)識標(biāo)識開始拖動(dòng)了)。mousemove則是用來調(diào)整元素的位置或者寬高。那么同樣的我們來看看mousemove事件的觸發(fā)頻率。 document.addEventListener('mousemove', function() {console.log('mousemove') }) 復(fù)制代碼我相信你現(xiàn)在已經(jīng)知道它比scroll還恐怖而且可以讓性能瞬間爆炸。那么這時(shí)候我們就可以用函數(shù)節(jié)流讓它300ms觸發(fā)一次位置計(jì)算。
源碼分析
debounce.js
這個(gè)文件的核心和入口是debounced函數(shù),我們先看看它
function debounced(...args) {const time = Date.now()const isInvoking = shouldInvoke(time)lastArgs = args // 記錄最后一次調(diào)用傳入的參數(shù)lastThis = this // 記錄最后一次調(diào)用的thislastCallTime = time // 記錄最后一次調(diào)用的時(shí)間if (isInvoking) {if (timerId === undefined) {return leadingEdge(lastCallTime)}if (maxing) {// Handle invocations in a tight loop.timerId = setTimeout(timerExpired, wait)return invokeFunc(lastCallTime)}}if (timerId === undefined) {timerId = setTimeout(timerExpired, wait)}return result } 復(fù)制代碼這里面很多變量,用閉包存下的一些值
其實(shí)就是保存最后一次調(diào)用的上下文(lastThis, lastAargs, lastCallTime)還有定時(shí)器的Id之類的。然后下面是執(zhí)行部分, 由于maxing是和throttle有關(guān)的,為了理解方便這里暫時(shí)不看它。
// isInvoking可以暫時(shí)理解為第一次或者當(dāng)上一次觸發(fā)時(shí)間超過設(shè)置wait的時(shí)候?yàn)檎?/span>if (isInvoking) {// 第一次觸發(fā)的時(shí)候沒有加timerif (timerId === undefined) {// 和上文說的leading有關(guān)return leadingEdge(lastCallTime)}//if (maxing) {// // Handle invocations in a tight loop.// timerId = setTimeout(timerExpired, wait)// return invokeFunc(lastCallTime)//}}// 第一次觸發(fā)的時(shí)候添加定時(shí)器if (timerId === undefined) {timerId = setTimeout(timerExpired, wait)} 復(fù)制代碼接下來我們看看這個(gè)timerExpired的內(nèi)容
function timerExpired() {const time = Date.now()// 這里的這個(gè)判斷基本只用作判斷timeSinceLastCall是否超過設(shè)置的waitif (shouldInvoke(time)) {// 實(shí)際調(diào)用函數(shù)部分return trailingEdge(time)}// 如果timeSinceLastCall還沒超過設(shè)置的wait,重置定時(shí)器之后再進(jìn)一遍timerExpiredtimerId = setTimeout(timerExpired, remainingWait(time))} 復(fù)制代碼trailingEdge函數(shù)其實(shí)就是執(zhí)行一下invokeFunc然后清空一下定時(shí)器還有一些上下文,這樣下次再執(zhí)行debounce過的函數(shù)的時(shí)候就能夠繼續(xù)下一輪了,沒什么值得說的
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 = undefinedreturn result} 復(fù)制代碼總結(jié)一下其實(shí)就是下面這些東西,不過提供了一些配置和可復(fù)用性(throttle部分)所以代碼就復(fù)雜了些。
// debounce簡單實(shí)現(xiàn) var debounce = function(wait, func){var timerIdreturn function(){var thisArg = this, args = argumentsclearTimeout(last)timerId = setTimeout(function(){func.apply(thisArg, args)}, wait)} } 復(fù)制代碼throttle.js
function throttle(func, wait, options) {let leading = truelet trailing = trueif (typeof func != 'function') {throw new TypeError('Expected a function')}if (isObject(options)) {leading = 'leading' in options ? !!options.leading : leadingtrailing = 'trailing' in options ? !!options.trailing : trailing}return debounce(func, wait, {'leading': leading,'maxWait': wait,'trailing': trailing}) } 復(fù)制代碼其實(shí)基本用的都是debounce.js里面的內(nèi)容,只是多了個(gè)maxWait參數(shù),還記得之前分析debounce的時(shí)候被我們注釋的部分么。
if (isInvoking) {if (timerId === undefined) {return leadingEdge(lastCallTime)}// **看這里**,如果有maxWait那么maxing就為真if (maxing) {// Handle invocations in a tight loop.timerId = setTimeout(timerExpired, wait)return invokeFunc(lastCallTime)}}if (timerId === undefined) {timerId = setTimeout(timerExpired, wait)} 復(fù)制代碼可以看到remainingWait和shouldInvoke中也都對maxing進(jìn)行了判斷
總結(jié)一下其實(shí)就是下面這樣 // throttle的簡單實(shí)現(xiàn),定時(shí)器都沒用 var throttle = function(wait, func){var last = 0return function(){var time = +new Date()if (time - last > wait){func.apply(this, arguments)last = curr }} } 復(fù)制代碼本文章來源于午安煎餅計(jì)劃Web組 - 梁王
總結(jié)
以上是生活随笔為你收集整理的每日源码分析 - lodash(debounce.js和throttle.js)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [codeVS1204] 单词背诵
- 下一篇: [Usaco2007 Oct] Supe