共计 10388 个字符,预计需要花费 26 分钟才能阅读完成。
导航
[[深刻 01] 执行上下文](https://juejin.im/post/684490…
[[深刻 02] 原型链](https://juejin.im/post/684490…
[[深刻 03] 继承](https://juejin.im/post/684490…
[[深刻 04] 事件循环](https://juejin.im/post/684490…
[[深刻 05] 柯里化 偏函数 函数记忆](https://juejin.im/post/684490…
[[深刻 06] 隐式转换 和 运算符](https://juejin.im/post/684490…
[[深刻 07] 浏览器缓存机制(http 缓存机制)](https://juejin.im/post/684490…
[[深刻 08] 前端平安](https://juejin.im/post/684490…
[[深刻 09] 深浅拷贝](https://juejin.im/post/684490…
[[深刻 10] Debounce Throttle](https://juejin.im/post/684490…
[[深刻 11] 前端路由](https://juejin.im/post/684490…
[[深刻 12] 前端模块化](https://juejin.im/post/684490…
[[深刻 13] 观察者模式 公布订阅模式 双向数据绑定](https://juejin.im/post/684490…
[[深刻 14] canvas](https://juejin.im/post/684490…
[[深刻 15] webSocket](https://juejin.im/post/684490…
[[深刻 16] webpack](https://juejin.im/post/684490…
[[深刻 17] http 和 https](https://juejin.im/post/684490…
[[深刻 18] CSS-interview](https://juejin.im/post/684490…
[[深刻 19] 手写 Promise](https://juejin.im/post/684490…
[[深刻 20] 手写函数](https://juejin.im/post/684490…
[[react] Hooks](https://juejin.im/post/684490…
[[部署 01] Nginx](https://juejin.im/post/684490…
[[部署 02] Docker 部署 vue 我的项目](https://juejin.im/post/684490…
[[部署 03] gitlab-CI](https://juejin.im/post/684490…
[[源码 -webpack01- 前置常识] AST 形象语法树](https://juejin.im/post/684490…
[[源码 -webpack02- 前置常识] Tapable](https://juejin.im/post/684490…
[[源码 -webpack03] 手写 webpack – compiler 简略编译流程](https://juejin.im/post/684490…
[[源码] Redux React-Redux01](https://juejin.im/post/684490…
[[源码] axios ](https://juejin.im/post/684490…
[[源码] vuex ](https://juejin.im/post/684490…
[[源码 -vue01] data 响应式 和 初始化渲染 ](https://juejin.im/post/684490…
Debounce 防抖函数
- <font color=red> 特点:延时执行,如果在延时的工夫内屡次触发,则从新计时 </font>
- 过程:当事件 A 产生时,设置一个定时器,a 秒后触发 A 的回调函数,如果在 a 秒内有新的同一事件产生,则革除定时器,并从新开始计时(即又在 a 秒后触发 A 的回调,留神:上次的 A 的回调并未触发,而是定时器被革除了,定时器中 A 的回调就不会被执行)
版本一 (根底版本)
- <font color=red> 长处:能够传参,比方点击时,点击事件提供的 event 对象 </font>
-
<font color=red> 毛病:</font>
- <font color=red> 第一次触发是不须要延时的,版本一的第一次也是须要定时器的 delay 工夫后才会执行 </font>
- <font color=red> 不能手动勾销 debounce 的执行,在 delay 工夫未到时的最初一次的执行 </font>
版本一 (根底版本) /** * @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) } }
版本二 (降级版本)
- <font color=red> 解决问题:解决第一次点击不能立刻触发的问题 </font>
- <font color=red> 解决问题:在 delay 工夫没有到时,手动的勾销 debounce 的执行 </font>
-
实现的后果:
- 第一次点击立刻触发
- 如果从第一次点击开始,始终不间断频繁点击(未超过 delay 工夫),而后进行点击不再点击,会触发两次,第一次是立刻执行的,第二次是 debounce 延时执行的
- 能够手动勾销 debounce 的执行
其实就是手动革除最初一次的 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)
// 解决:// timer = 1
// return
}
if (timer) {clearTimeout(timer)
// timer 存在,就革除定时器
// 革除定时器,则定时器对应的回调函数也就不会执行
}
timer = setTimeout(() => {console.log(args, 'args')
console.log(this, 'this')
fn.call(this, ...args)
// 留神:有一个非凡状况
// 比方:只点击一次,在下面的 immediate&&!timer 判断中会立刻执行一次,而后在 delay 后,定时器中也会触发一次
// 如何解决执行两次:在下面的 immediate&&!timer 判断中立刻执行一次 fn 后,将 timer=1,同时 return,将不再往下执行,同时 timer 存在
// --------------------
// if (!immediate) {// fn.call(this, ...args)
// }
// immediate = false
// 正文的操作能够只在点击一次没有再点击的状况只执行一次
// 然而:一次性屡次点击,第二次不会触发,只有再进展达到 delay 后,再次点击才会失常的达到 debounce 的成果
// --------------------
}, delay)
// 手动勾销执行 debounce 函数
debounce.cancel = function () {clearTimeout(timer)
}
}
}
<font color=red> 版本三 (变更需要)</font>
-
<font color=red> 需要:第一次立刻执行,而后等到进行触发 delay 毫秒后,才能够从新触发 </font>
版本三 (变更需要) 需要:第一次立刻执行,而后等到进行触发 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) } } } }
案例 1
<!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>
案例二 – react 中
-
手动勾销
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> ); }
在实在我的项目中的使用
- <font color=red> 如视频监听断流的回调,会不停的执行监听函数,当视频当断流时,就不再执行监听函数了,此时能够用 debounce,就能解决监听到断流后须要解决的事件,比方提醒断流 </font>
- input 框的查问后果,不须要输出每个字符都去查问后果,而是应用 debounce 函数去解决查问后端接口
-
小结:Debounce 须要思考第一次执行,手动勾销执行,事件对象 event 等参数的传递问题
Throttle
- <font color=red> 特点:每隔一段时间,只执行一次 </font>
- 在工夫 a 内,只会执行一次函数,屡次触发也只会触发一次
版本一(根底版本)
-
原理:设置一个标记位为 true,在闭包中判断标记位,false 则 turn;接着把示意为改为 false,第二次就间接返回了,不会执行定时器,定时器执行完,标记位改为 true,则又能够进入闭包执行定时器;同时定时器执行完,要革除定时器
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) } }
版本二(利用工夫戳)
-
<font color=red> 原理:比拟两次点击的工夫戳差值(单位是毫秒),大于 delay 毫秒则执行 fn</font>
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 // 留神:执行完记得同步工夫 } } }
在实在我的项目中的使用
- 浏览器窗口的 resize
- 滚动条的滚动监听函数须要触发的回调
- 上拉加载更多
<font color=red>underscore 中的 Throttle</font>
前置常识:- 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:https://juejin.im/post/684490…
Throttle:https://juejin.im/post/684490…
剖析 underscore-throttle1:https://juejin.im/post/684490…
剖析 underscore-throttle2:https://github.com/lessfish/u…
underscore 源码地址:https://github.com/jashkenas/…
https://juejin.im/post/684490…