JS高级——节流函数
1、什么是节流
-
在某个时间内(比如500ms),某个函数只能被触发一次;
过程:
-
当事件触发时,相应的函数按照一定的频率去执行
2、节流的应用场景
-
监听页面的滚动事件;
-
鼠标移动事件;
-
用户频繁点击按钮操作;
-
游戏中的一些设计;
总之,依然是密集的事件触发,但是这次密集事件触发的过程,不会等待最后一次才进行函数调用,而是会按照一定的频率进行调用;
3、节流函数的实现
下面我们根据需求场景,一步一步深入实现节流函数
3.1 节流基本功能
节流函数的默认实现思路我们采用时间戳的方式来完成:
-
我们使用一个last来记录上一次执行的时间
-
每次准备执行前,获取一下当前的时间now:
now - last > interval
-
那么函数执行,并且将now赋值给last即可
-
代码实现 :
//throttle.js
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @returns
*/
function throttle(fn, interval) {
// 1.记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 真正执行的函数
const _throttle = function () {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长时间需要去触发函数
const remainTime = interval - (nowTime - lastTime)
//第一次会执行,原因是nowTime刚开始是一个很大的数字,结果为负数
//若最后一次没能满足条件,不会执行
if (remainTime <= 0) {
// 2.3.真正触发函数
fn()
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
代码调用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button id="cancel">取消</button>
<script src="./throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
let counter = 0
//输入触发事件
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`)
}
inputEl.oninput = throttle(inputChange, 1000)
</script>
</body>
</html>
3.2 绑定this和参数
我们知道在oninput事件触发时会有参数传递,并且触发的函数中this是指向当前的元素节点的
-
目前我们fn的执行是一个独立函数调用,它里面的this是window
-
我们需要将其修改为对应的节点对象,而返回的function中的this指向的是节点对象;
-
-
目前我们的fn在执行时是没有传递任何的参数的,它需要将触发事件时传递的参数传递给fn
-
而我们返回的function中的arguments正是我们需要的参数;
-
所以我们的代码可以进行如下的优化:
//throttle.js
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @returns
*/
function throttle(fn, interval) {
// 1.记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 真正执行的函数
const _throttle = function (...args) {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长时间需要去触发函数
const remainTime = interval - (nowTime - lastTime)
//第一次会执行,原因是nowTime刚开始是一个很大的数字,结果为负数
//若最后一次没能满足条件,不会执行
if (remainTime <= 0) {
// 2.3.真正触发函数
fn.apply(this, args)
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
代码调用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button id="cancel">取消</button>
<script src="./throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
let counter = 0
//输入触发事件
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event)
}
inputEl.oninput = throttle(inputChange, 1000)
</script>
</body>
</html>
3.3 节流函数第一次触发,优化立即执行
目前我们的函数触发第一次是都是立即执行的,但是某些场景是用户开始输入时的第一次是不立即执行的,我们可以如何优化呢?
-
我们可以让用户多传入一个参数:leading
-
leading为true时,或者不传(默认为true),第一次触发会立即执行
-
leading为false时,第一次不会立即执行
-
我们可以根据是否传入leading进行不同的处理方式:
//throttle.js
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @param {*} options 可选参数: leading第一次是否执行
* @returns
*/
function throttle(fn, interval, options = {leading: true }) {
// 1.记录上一次的开始时间
let lastTime = 0
const {leading} = options
// 2.事件触发时, 真正执行的函数
const _throttle = function (...args) {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
//当lastTime为0时且第一次不执行,此时 remainTime = interval - (nowTime - lastTime) = interval > 0
if (!lastTime && !leading) lastTime = nowTime//决定第一次是否执行函数
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
// 2.3.真正触发函数
fn.apply(this, args)
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
代码调用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button id="cancel">取消</button>
<script src="./throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
let counter = 0
//输入触发事件
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event)
}
inputEl.oninput = throttle(inputChange, 2000, { leading: false })
</script>
</body>
</html>
3.4 优化节流函数最后一次执行
默认情况下,我们的防抖函数最后一次是不会执行的
-
因为没有达到最终的时间,也就是条件
now - last > interval
满足不了的 -
但是,如果我们希望它最后一次是可以执行的,那么我们可以让其传入对应的参数来控制
我们来看一下代码如何实现:
-
我们增加了判断语句:
-
当最后一次需要执行且没有定时器时,只需要创建一个定时器,并且还有多久触发由remainTime决定
-
-
如果固定的频率中执行了回调函数
-
因为需要执行回调函数,所以如果存在定时器则需要取消;
-
并且将timer赋值为null,这样的话可以开启下一次定时器;
-
-
如果定时器最后执行了,那么timer需要赋值为null
-
因为下一次重新开启时,只有定时器为null,才能进行下一次的定时操作;
-
//throttle.js
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
* @returns
*/
function throttle(fn, interval, options = {leading: true, trailing: false,}) {
const { leading, trailing } = options;
// 1.记录上一次的开始时间
let lastTime = 0;
let timer = null;
// 2.事件触发时, 真正执行的函数
const _throttle = function (...args) {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime();
//当lastTime为0时且第一次不执行
if (!lastTime && !leading) lastTime = nowTime;
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime);
if (remainTime <= 0) {
if (timer) {
//执行函数时,需要取消定时器
clearTimeout(timer);
timer = null;
}
// 2.3.真正触发函数
fn.apply(this, args);
// 2.4.保留上次触发的时间
lastTime = nowTime;
return;
}
//当最后一次需要执行且没有定时器时,只需要创建一个定时器,还有多久触发由remainTime决定
if (trailing && !timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; //执行后重置
lastTime = !leading ? 0 : new Date().getTime(); //第一次不执行时为0,第一次执行时为定时器执行时的时间
}, remainTime);
}
};
return _throttle;
}
代码调用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button id="cancel">取消</button>
<script src="./throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
let counter = 0
//输入触发事件
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event)
}
inputEl.oninput = throttle(inputChange, 2000, { leading: true , trailing: true})
</script>
</body>
</html>
3.5 当我们触发函数时,在未到执行时间,可以取消函数执行
与防抖函数的取消功能是类似的:
//throttle.js
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
* @returns
*/
function throttle(fn, interval, options = {leading: true,trailing: false}) {
// 1.记录上一次的开始时间
const {leading,trailing} = options
let lastTime = 0
let timer = null
// 2.事件触发时, 真正执行的函数
const _throttle = function (...args) {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.3.真正触发函数
fn.apply(this, args)
// 2.4.保留上次触发的时间
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
lastTime = !leading ? 0 : new Date().getTime()
}, remainTime)
}
}
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}
代码调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button id="cancel">取消</button>
<script src="./throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
let counter = 0
//输入触发事件
const throttleChange = throttle(inputChange, 2000, { leading: false, trailing: true })
inputEl.oninput = throttleChange
// 取消功能
cancelBtn.onclick = function () {
throttleChange.cancel()
}
</script>
</body>
</html>
3.6 当触发的函数有返回值时,获取在节流函数中执行的结果
与防抖函数类似,有两种方法:
-
方法一:通过回调函数
-
方法二:通过Promise的resolve
(1)给throttle函数的options,多添加一个参数,参数为一个回调函数:
//throttle.js
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
* @returns
*/
function throttle(fn, interval, options = {leading: true,trailing: false}) {
// 1.记录上一次的开始时间
const { leading,trailing,resultCallback} = options
let lastTime = 0
let timer = null
// 2.事件触发时, 真正执行的函数
const _throttle = function (...args) {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.3.真正触发函数
const result = fn.apply(this, args)
if (resultCallback && typeof resultCallback === 'function')
resultCallback(result) //函数回调返回返回值
// 2.4.保留上次触发的时间
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
const result = fn.apply(this, args)
if (resultCallback && typeof resultCallback === 'function')
resultCallback(result) //函数回调返回返回值
timer = null
lastTime = !leading ? 0 : new Date().getTime()
}, remainTime)
}
}
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}
代码调用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button id="cancel">取消</button>
<script src="./throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
let counter = 0
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event)
// 返回值
return "aaaaaaaaaaaa"
}
// 方法一 通过添加一个函数参数来获取
const throttleChange = throttle(inputChange, 1000,{
leading: false,
trailing: true,
resultCallback: function (res) {
console.log("resultCallback:", res)
}
})
inputEl.oninput = throttleChange
// 取消功能
cancelBtn.onclick = function () {
throttleChange.cancel()
}
</script>
</body>
</html>
(2)使用Promise来返回执行结果:
/**
* @param {*} fn 要执行的函数
* @param {*} interval 时间间隔
* @param {*} options 可选参数: leading第一次是否执行, trailing 最后一次是否执行
* @returns
*/
function throttle(fn, interval, options = {leading: true,trailing: false}) {
// 1.记录上一次的开始时间
const { leading,trailing} = options
let lastTime = 0
let timer = null
// 2.事件触发时, 真正执行的函数
const _throttle = function (...args) {
return new Promise((resolve, reject) => { //promise方式返回返回值
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
if (!lastTime && !leading) lastTime = nowTime
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
const remainTime = interval - (nowTime - lastTime)
if (remainTime <= 0) {
if (timer) {
clearTimeout(timer)
timer = null
}
// 2.3.真正触发函数
const result = fn.apply(this, args)
resolve(result)
// 2.4.保留上次触发的时间
lastTime = nowTime
return
}
if (trailing && !timer) {
timer = setTimeout(() => {
const result = fn.apply(this, args)
resolve(result)
timer = null
lastTime = !leading ? 0 : new Date().getTime()
}, remainTime)
}
})
}
_throttle.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
lastTime = 0
}
return _throttle
}
代码调用:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button id="cancel">取消</button>
<script src="./throttle.js"></script>
<script>
const inputEl = document.querySelector("input")
const cancelBtn = document.querySelector("#cancel")
let counter = 0
const inputChange = function (event) {
console.log(`发送了第${++counter}次网络请求`, this, event)
// 返回值
return "aaaaaaaaaaaa"
}
// 方法二 返回一个promise
const throttleChange = throttle(inputChange, 3000, {
leading: false,
trailing: true
})
const tempCallback = function (...args) {
throttleChange.apply(this, args).then(res => {//此时this绑定的是input对象
console.log("Promise的返回值结果:", res)
})
}
inputEl.oninput = tempCallback
// 取消功能
cancelBtn.onclick = function () {
throttleChange.cancel()
}
</script>
</body>
</html>
更多推荐
所有评论(0)