[深刻01] 執行上下文
[深刻02] 原型鏈
[深刻03] 繼承
[深刻04] 事件循環
[深刻05] 柯里化 偏函數 函數記憶
[深刻06] 隱式轉換 和 運算符
[深刻07] 瀏覽器緩存機制(http緩存機制)
[深刻08] 前端安全
[深刻09] 深淺拷貝
[深刻10] Debounce Throttle
[深刻11] 前端路由
[深刻12] 前端模塊化
[深刻13] 觀察者模式 發佈訂閱模式 雙向數據綁定
[深刻14] canvas
[深刻15] webSocket
[深刻16] webpack
[深刻17] http 和 https
[深刻18] CSS-interview
[react] Hookshtml
[部署01] Nginx
[部署02] Docker 部署vue項目
[部署03] gitlab-CI前端
[源碼-webpack01-前置知識] AST抽象語法樹
[源碼-webpack02-前置知識] Tapable
[源碼-webpack03] 手寫webpack - compiler簡單編譯流程vue
版本一 (基礎版本)
/**
* @param {function} fn 須要debounce防抖函數處理的函數
* @param {number} delay 定時器延時的時間
*/
function debounce(fn, delay) {
let timer = null
// 該變量常駐內存,能夠記住上一次的狀態
// 只有在外層函數失去引用時,該變量纔會清除
// 緩存定時器id
return (...args) => {
// 返回一個閉包
// 注意參數:好比事件對象 event 可以獲取到
if (timer) {
// timer存在,就清除定時器
// 清除定時器,則定時器對應的回調函數也就不會執行
clearTimeout(timer)
}
// 清除定時器後,從新計時
timer = setTimeout(() => {
fn.call(this, ...args)
// this須要定時器回調函數時才能肯定,this指向調用時所在的對象,大多數狀況都指向window
}, delay)
}
}
複製代碼
其實就是手動清除最後一次的timer
版本二 (升級版本)
/**
* @param {function} fn 須要debounce防抖函數處理的函數
* @param {number} delay 定時器延時的時間
* @param {boolean} immediate 是否當即執行
*/
function debounce(fn, delay, immediate) {
let timer = null
return (...args) => { // 這裏能夠拿到事件對象
if (immediate && !timer) {
// 若是當即執行標誌位是 true,而且timer不存在
// 即第一次觸發的狀況
// 之後的觸發因爲timer存在,則再也不進入執行
// 注意:timer是setTimeout()執行返回的值,不是setTimeout()的回調執行時才返回,是當即返回的
// 注意:因此第二次觸發時,timer就已經有值了,不是setTimeout()的回調執行時才返回
fn.call(this, ...args)
}
if (timer) {
clearTimeout(timer)
// timer存在,就清除定時器
// 清除定時器,則定時器對應的回調函數也就不會執行
}
timer = setTimeout(() => {
console.log(args, 'args')
console.log(this, 'this')
fn.call(this, ...args)
// 注意:有一個特殊狀況
// 好比:只點擊一次,在上面的immediate&&!timer判斷中會當即執行一次,而後在delay後,定時器中也會觸發一次
// --------------------
// if (!immediate) {
// fn.call(this, ...args)
// }
// immediate = false
// 註釋的操做能夠只在點擊一次沒有再點擊的狀況只執行一次
// 可是:一次性屢次點擊,第二次不會觸發,只有再停頓達到delay後,再次點擊纔會正常的達到debounce的效果
// --------------------
}, delay)
// 手動取消執行debounce函數
debounce.cancel = function () {
clearTimeout(timer)
}
}
}
複製代碼
版本三 (變動需求)
需求:第一次當即執行,而後等到中止觸發delay毫秒後,才能夠從新觸發
/**
* @param {function} fn 須要debounce防抖函數處理的函數
* @param {number} delay 定時器延時的時間
* @param {boolean} immediate 是否當即執行
*/
function debounce(fn, delay, immediate) {
let timer
return (...args) => {
if (timer) {
clearTimeout(timer)
}
if(!immediate) {
// 不當即執行的狀況
// 和最初的版本同樣
timer = setTimeout(() => {
fn.call(this, ...args)
}, delay)
} else {
// 當即執行
const cacheTimer = timer // 緩存timer
// 緩存timer, 由於下面timer會當即改變,若是直接用timer判斷,fn不會執行
// 當即執行的狀況下,第一次:cacheTimer => false
// 當即執行的狀況下,第二次:cacheTimer => true,由於直到delay毫秒後,timer纔會被修改,cacheTimer 變爲false
timer = setTimeout(() => {
timer = null
// delay後,timer重新改成null,則知足條件!cacheTimer,則fn會再次執行
}, delay)
if(!cacheTimer) {
// 緩存了timer,因此當即執行的狀況,第一次緩存的timer時false,會當即執行fn
fn.call(this, ...args)
}
}
}
}
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div class="div">點擊</div>
<script>
const dom = document.getElementsByClassName('div')[0]
const fn = () => {
console.log(11111111111)
}
dom.addEventListener('click', debounce(fn, 1000, true), false)
// document.addEventListener('click', (() => debounce(fn, 1000))(), false)
// 注意:這裏debounce(fn, 1000)會當即執行,返回閉包函數
// 注意:閉包函數纔是在每次點擊的時候觸發
function debounce(fn, delay, immediate) {
let timer = null
return (...args) => { // 這裏能夠拿到事件對象
if (immediate && !timer) {
// 若是當即執行標誌位是 true,而且timer不存在
// 即第一次觸發的狀況
// 之後的觸發因爲timer存在,則再也不進入執行
console.log('第一次當即執行')
fn.call(this, ...args)
}
if (timer) {
clearTimeout(timer)
// timer存在,就清除定時器
// 清除定時器,則定時器對應的回調函數也就不會執行
}
timer = setTimeout(() => {
console.log(args, 'args')
console.log(this, 'this')
fn.call(this, ...args)
}, delay)
}
}
</script>
</body>
</html>
複製代碼
function App() {
const fn = () => {
console.log('fn')
}
const debounce = (fn, delay, immediate) => {
let timer = null
return (...args) => {
if (immediate && !timer) {
fn.call(this, ...args)
}
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.call(this, ...args)
}, delay)
debounce.cancel = function () { // 手動取消debounce
clearTimeout(timer)
}
}
}
const cancleDebounce = () => {
debounce.cancel()
}
return (
<div className="App">
<div onClick={debounce(fn, 3000, true)}>點擊2</div>
<div onClick={cancleDebounce}>取消執行</div>
</div>
);
}
複製代碼
小結:Debounce須要考慮第一次執行,手動取消執行,事件對象event等參數的傳遞問題 |
function throttle(fn, delay) {
let isRun = true // 標誌位
return (...args) => {
if (!isRun) { // false則跳出函數,再也不向下執行
return
}
isRun = false // 當即改成false,則下次不會再執行到定位器,直到定時器執行完,isRun爲true,纔有機會執行到定時器
let timer = setTimeout(() => {
fn.call(this, ...args)
isRun = true
clearTimeout(timer) // 執行完全部操做後,清除定時器
}, delay)
}
}
複製代碼
function throttle(fn, delay) {
let previous = 0 // 緩存上一次的時間戳
return (...args) => {
const now = + new Date()
// (+)一元加運算符:能夠把任意類型的數據轉換成(數值),結果只能是(數值)和(NaN)兩種
// 獲取如今的時間戳,即距離1970.1.1 00:00:00的毫秒數字
// 注意:單位是毫秒數,和定時器的第二個參數吻合,也是毫秒數
if (now - previous > delay) {
// 第一次:now - previous > delay是true,因此當即執行一次
// 而後 previous = now
// 第二次:第二次能進來的條件就是差值毫秒數超過delay毫秒
// 這樣頻繁的點擊時,就能按照固定的頻率執行,固然是下降了頻率
fn.call(this, ...args)
previous = now // 注意:執行完記得同步時間
}
}
}
複製代碼
前置知識:
- leading:是頭部,領導的意思
- trailing: 是尾部的意思
- remaining:剩餘的意思 (remain:剩餘)
options.leading => 布爾值,表示是否執行事件剛開始的那次回調,false表示不執行開始時的回調
options.trailing => 布爾值,表示是否執行事件結束時的那次回調,false表示不執行結束時的回調
_.throttle = function(func, wait, options) {
// func:throttle函數觸發時須要執行的函數
// wait:定時器的延遲時間
// options:配置對象,有 leading 和 trailing 屬性
var timeout, context, args, result;
// timeout:定時器ID
// context:上下文環境,用來固定this
// args:傳入func的參數
// result:func函數執行的返回值,由於func是可能存在返回值的,因此須要考慮到返回值的賦值
var previous = 0;
// 記錄上一次事件觸發的時間戳,用來緩存每一次的 now
// 第一次是:0
// 之後就是:上一次的時間戳
if (!options) options = {};
// 配置對象不存在,就設置爲空對象
var later = function() { // later是定時器的回調函數
previous = options.leading === false ? 0 : _.now();
timeout = null; // 從新賦值爲null,用於條件判斷,和下面的操做同樣
result = func.apply(context, args);
if (!timeout) context = args = null;
// timer必然爲null,上面從新賦值了,重置context, args
};
var throttled = function() {
var now = _.now();
// 獲取當前時間的時間戳
if (!previous && options.leading === false) previous = now;
// 若是previous不存在,而且第一次回調不須要執行的話,previous = now
// previous
// 第一次是:previous = 0
// 以後都是:previous是上次的時間戳
// options.leading === false
// 注意:這裏是三等,即類型不同的話都是false
// 因此:leading是undefined時,undefined === false 結果是 fale,由於類型都不同
var remaining = wait - (now - previous);
// remaining:表示距離下次觸發 func 還需等待的時間
// remaining的值的取值狀況,下面有分析
context = this;
// 固定this指向
args = arguments;
// 獲取func的實參
if (remaining <= 0 || remaining > wait) {
// remaining <= 0 的全部狀況以下:
// 狀況1:
// 第一次觸發,而且(不傳options或傳入的options.leading === true)即須要當即執行第一次回調
// remaining = wait - (now - 0) => remaining = wait - now 必然小於0
// 狀況2:
// now - previous > wait,即間隔的時間已經大於了傳入定時器的時間
// remaining > wait 的狀況以下:
// 說明 now < previous 正常狀況時絕對不會出現的,除非修改了電腦的本地時間,能夠直接不考慮
if (timeout) {
// 定時器ID存在,就清除定時器
clearTimeout(timeout);
timeout = null;
// 清除定時器後,將timeout設置爲null,這樣就不會再次進入這個if語句
// 注意:好比 var xx = clearTimeout(aa),這裏clearTimeout()不會把xx變成null,xx不會改變,可是aa不會執行
}
previous = now;
// 立刻緩存now,在執行func以前
result = func.apply(context, args);
// 執行func
if (!timeout) context = args = null;
// 定時器ID不存在,就重置context和args
// 注意:這裏timeout不是必定爲null的
// 1. 若是進入了上面的if語句,就會被重置爲null
// 2. 果如沒有進入上面的if語句,則有多是有值的
} else if (!timeout && options.trailing !== false) {
// 定時器ID不存在 而且 最後一次回調須要觸發時進入
// later是回調
timeout = setTimeout(later, remaining);
}
return result;
// 返回func的返回值
};
throttled.cancel = function() { // 取消函數
clearTimeout(timeout); // 清除定時器
// 如下都是重置一切參數
previous = 0;
timeout = context = args = null;
};
return throttled;
};
----------------------------------------------------------
總結整個流程:
window.onscroll = _.throttle(fn, 1000);
window.onscroll = _.throttle(fn, 1000, {leading: false});
window.onscroll = _.throttle(fn, 1000, {trailing: false});
以點擊觸發_.throttle(fn, 1000)爲例:
1. 第一次點擊
(1)now賦值
(2)不會執行previous = now
(3)remaining = wait - now => remain < 0
(4)進入if (remaining <= 0 || remaining > wait) 中
(5)previous = now;
(6)執行 func.apply(context, args)
(7)context = args = null
2. 第二次點擊 - 迅速的
(1)now賦值
(2)進入if (!timeout && options.trailing !== false) 中
(3)timeout = setTimeout(later, remaining);
// 特別注意:這時timerout有值了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 而 timeout = null的賦值一共有兩處!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// (1)if (remaining <= 0 || remaining > wait) 這個if中修改!!!!!!!!!!!!!!!!!
// (2)if (!timeout && options.trailing !== false)這個if的定時器回調中修改!!!!!!!!!!
// 而(2)中的定時器回調須要在remaining毫秒後纔會修改!!!!!!!!!!!!!!!!!!!!!!!
(4)previous = _.now(); 而後 timeout = null; 在而後 result = func.apply(context, args);
(5)context = args = null;
3. 第三次點擊 - 迅速的
- 由於在timeout存在,remaining毫秒還未到時,不會進入任何條件語句中執行任何代碼
- 直到定時器時間到後,修改了timeout = null,previous被從新修改後就再作判斷
複製代碼
Debounce: juejin.im/post/5c270a…
Throttle: juejin.im/post/5be24d…
分析underscore-throttle1:juejin.im/post/5cedd3…
分析underscore-throttle2:github.com/lessfish/un…
underscore源碼地址:github.com/jashkenas/u…
juejin.im/post/5d0a53…react