[深刻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