【手写篇】你会用定时器、时间戳、RAF、事件绑定机制实现节流防抖吗???

🧑‍💻 写在开头

点赞 + 收藏 === 学会🤣🤣🤣

一、基本概念&简单实现

防抖【回城】

合并多次触发的事件为一次执行。常用于输入框的实时搜索。

1.应用

用于限制在短时间内频繁触发某个事件的执行次数。这种情况通常出现在用户在一个连续操作中多次触发同一事件,例如快速连续点击按钮、滚动页面时频繁触发事件等。

2.原理

防抖的基本思想是在事件触发后,设定一个等待时间(也称为延迟时间),只有在等待时间内没有再次触发该事件,才执行事件的回调函数。如果在等待时间内再次触发了该事件,就会重新计时等待时间。这样可以有效地阻止事件频繁触发,减少不必要的处理开销,提升前端性能和用户体验。

在实现防抖时,可以通过使用定时器(setTimeout)和清除定时器(clearTimeout)来实现。每次触发事件时,先清除之前设置的定时器,再设置一个新的定时器,从而达到防抖的效果。

3.手写

  • 进入函数清除定时器
  • debounce返回一个函数
  • 清除函数后重置定时器
  • 可以给函数传递参数
js 复制代码
// 设置防抖
function debounce(callback, delay) {
  let timer = null;
  // debounce返回一个函数,进入先清空定时器,然后重置定时器
  return function () {
    // 清除定时器
    clearTimeout(timer);
    // 重置定时器
    timer = setTimeout(() => {
      callback(delay);
    }, delay);
  };
}

节流【技能释放】

限制函数的执行频率。在一段时间内只执行一次

1.应用

  • 页面滚动事件:当用户滚动页面时,会频繁触发滚动事件。使用节流可以限制滚动事件的触发频率,避免过多的滚动事件处理,从而提高页面性能。
  • 窗口大小调整事件:当用户调整浏览器窗口大小时,会频繁触发窗口大小调整事件。使用节流可以确保调整事件在一定时间间隔内只触发一次,避免过多的计算和布局更新。
  • 输入框输入事件:当用户在输入框中输入内容时,每输入一个字符都会触发输入事件。使用节流可以控制输入事件的触发频率,减少输入事件的处理次数,特别适用于实时搜索功能。
  • 鼠标移动事件:当鼠标在页面上移动时,会频繁触发鼠标移动事件。使用节流可以限制鼠标移动事件的触发次数,避免过多的计算和渲染。
  • 按钮点击事件:当用户频繁点击按钮时,使用节流可以确保按钮点击事件在一定时间间隔内只触发一次,避免多次点击导致的重复操作。
  • 自动完成(Autocomplete)功能:在输入框中实现自动完成功能时,用户每输入一个字符就会触发一次搜索请求。使用节流可以限制搜索请求的触发频率,避免频繁的搜索请求。
  • 拖拽操作:当用户进行拖拽操作时,拖拽事件会持续触发。使用节流可以限制拖拽事件的触发频率,避免频繁的拖拽事件处理。

2.原理

不同于防抖,节流是确保在一定时间间隔内,事件最多触发一次,而不是在等待时间结束时执行一次。

节流函数的基本思想是,在事件触发后立即执行一次事件处理函数,并且在接下来的一段时间内阻止再次触发事件。只有在经过指定的时间间隔后,才允许再次触发事件,再次执行事件处理函数。

3.手写

  • 先判断是否可以执行
  • 阻止执行
  • 执行函数
  • 延迟时间设置为可执行
js 复制代码
function throttle(callback) {
  let canRun = true;
  return function () {
    // 已经有一个执行了,返回
    if (!canRun) return;
    // 阻止执行
    canRun = false;
    // 执行函数
    callback("我节流了");
    // 设置间隔时间
    setTimeout(() => {
      canRun = true;
    }, 1000);
  };
}

二、定时器实现我看烦了,其他方式呢?

节流

时间戳方式实现节流

原理: 通过记录上一次函数执行的时间戳,计算当前时间与上次执行时间的间隔是否大于设定的延迟时间,以决定是否执行函数。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Throttle with Timestamp</title>
</head>
<body>
    <button id="throttle-btn">Click me</button>
    <script>
        function throttle(func, delay) {
            let lastTime = 0; // 记录上次执行时间
            return function(...args) {
                const now = Date.now(); // 获取当前时间
                if (now - lastTime >= delay) { // 判断时间间隔是否大于延迟时间
                    lastTime = now; // 更新上次执行时间
                    func.apply(this, args); // 执行函数
                }
            };
        }

        const handleClick = throttle(() => {
            console.log('点击了Button');
        }, 2000);

        document.getElementById('throttle-btn').addEventListener('click', handleClick);
    </script>
</body>
</html>

请求动画帧方式实现节流

原理: 利用 requestAnimationFrame 使得在浏览器每次重绘之前只执行一次函数,从而限制函数的执行频率,坏处是延迟时间无法自行控制,这里效果不是很好看出来,我们可以用时间戳简单模式下重绘的延时。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Throttle with requestAnimationFrame and setTimeout</title>
    <style>
        #throttle-area {
            width: 100%;
            height: 200px;
            border: 1px solid black;
            background-color: lightgray;
            display: flex;
            justify-content: center;
            align-items: center;
            font-size: 20px;
        }
    </style>
</head>

<body>
    <div id="throttle-area">矩形中移动指针</div>

    <script>
        function throttle(func, delay) {
            let ticking = false;
            let lastExecution = 0;

            return function (...args) {
                const now = Date.now();

                if (!ticking) {
                    requestAnimationFrame(() => {
                        if (now - lastExecution >= delay) { // 控制延迟时间
                            func.apply(this, args); // 执行函数
                            lastExecution = now; // 更新上次执行时间
                        }
                        ticking = false; // 重置标记
                    });
                    ticking = true; // 设置标记
                }
            };
        }

        const handleMouseMove = throttle((event) => {
            const throttleArea = document.getElementById('throttle-area');
            throttleArea.style.backgroundColor = `rgb(${event.clientX % 255}, ${event.clientY % 255}, 150)`;
            throttleArea.textContent = `指针位置: (${event.clientX}, ${event.clientY})`;
            console.log(`指针位置: (${event.clientX}, ${event.clientY})`);
        }, 500); // 控制延迟时间为500毫秒

        document.getElementById('throttle-area').addEventListener('mousemove', handleMouseMove);
    </script>
</body>

</html>

防抖

立即执行一次+防抖

原理: 在函数首次调用时立即执行,用户停止输入后再次执行函数。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Debounce with Immediate Execution and Stop Typing</title>
</head>
<body>
    <input type="text" id="debounce-input" placeholder="Type something...">
    <script>
        function debounce(func, delay) {
            let timer; // 定时器变量
            return function(...args) {
                const context = this; // 保存函数执行上下文
                clearTimeout(timer); // 清除定时器
                const callNow = !timer; // 是否立即执行的标志
                timer = setTimeout(function() { // 设置延迟定时器
                    timer = null; // 定时器置空
                    if (!callNow) { // 如果不是立即执行
                        func.apply(context, args); // 执行函数
                    }
                }, delay);
                if (callNow) { // 如果是立即执行
                    func.apply(context, args); // 执行函数
                }
            };
        }

        const handleInput = debounce((event) => { // 创建防抖函数
            console.log('Input:', event.target.value); // 打印输入内容
        }, 500); // 设置延迟时间为500毫秒

        document.getElementById('debounce-input').addEventListener('input', handleInput); // 监听输入事件
    </script>
</body>
</html>

用promise方式实现防抖

利用Promise来处理防抖逻辑,通过异步的方式实现函数的延迟执行。

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Debounce with Promise</title>
</head>

<body>
    <input type="text" id="debounce-input-promise" placeholder="Type something...">
    <script>
        function debounce(func, delay) {
            let timer;
            return function (...args) {
                return new Promise((resolve) => {
                    if (timer) clearTimeout(timer); // 清除定时器
                    timer = setTimeout(() => {
                        resolve(func.apply(this, args)); // 延迟结束后执行函数
                    }, delay);
                });
            };
        }

        const handleInput = debounce((event) => {
            console.log('Input:', event.target.value);
        }, 1000);

        document.getElementById('debounce-input-promise').addEventListener('input', handleInput);
    </script>
</body>

</html>

用事件监听器实现防抖

原理: 利用事件监听器的绑定和解绑机制,避免频繁触发事件处理函数。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Debounce with Event Listener Unbinding</title>
</head>
<body>
    <input type="text" id="debounce-event-input" placeholder="Type something...">
    <script>
        function debounceEvent(func, delay) {
            let timer; // 定时器变量
            return function(event) {
                if (timer) { // 如果定时器存在,说明上次触发事件后还未执行函数
                    event.target.removeEventListener(event.type, arguments.callee); // 解绑事件
                    clearTimeout(timer); // 清除定时器
                }
                timer = setTimeout(() => { // 设置新的定时器
                    func.apply(this, arguments); // 执行函数
                    event.target.addEventListener(event.type, arguments.callee); // 重新绑定事件
                    timer = null; // 定时器置空
                }, delay);
            };
        }

        const handleInput = debounceEvent((event) => { // 创建防抖函数
            console.log('Input:', event.target.value); // 打印输入内容
        }, 1000); // 设置延迟时间为1000毫秒

        document.getElementById('debounce-event-input').addEventListener('input', handleInput); // 监听输入事件
    </script>
</body>
</html>

三、浏览器的事件循环

  1. 执行全局同步代码: 从全局作用域开始执行 JavaScript 代码,将同步任务依次压入执行栈中执行。
  2. 执行微任务: 在当前宏任务执行结束后,会检查微任务队列是否有任务,如果有则依次执行所有微任务,直到微任务队列为空。微任务的执行顺序是先进先出。
  3. 执行宏任务: 如果微任务队列为空,事件循环会从宏任务队列中取出一个任务,放入执行栈中执行。执行完当前宏任务后,会再次检查微任务队列,重复上述微任务执行的步骤。
  4. 渲染页面: 在执行完所有任务后,如果页面需要重绘或重排(比如有 DOM 变化或 CSS 样式变化),浏览器会进行页面渲染。
  5. 等待新任务: 一轮事件循环结束后,会等待新的任务进入任务队列,继续下一轮事件循环。

推荐阅读

开源电子书

在线链接

工程化系列

本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等

面试手写系列

react实现原理系列

其他

🍋 写在最后

如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!

感兴趣的同学可以关注下我的公众号ObjectX前端实验室

🌟 少走弯路 | ObjectX前端实验室 🛠️「精选资源|实战经验|技术洞见」

相关推荐
GDAL2 分钟前
vue3入门教程:ref函数
前端·vue.js·elementui
GISer_Jing11 分钟前
Vue3状态管理——Pinia
前端·javascript·vue.js
好开心3326 分钟前
axios的使用
开发语言·前端·javascript·前端框架·html
Domain-zhuo35 分钟前
Git常用命令
前端·git·gitee·github·gitea·gitcode
开发者每周简报42 分钟前
求职市场变化
人工智能·面试·职场和发展
菜根Sec1 小时前
XSS跨站脚本攻击漏洞练习
前端·xss
m0_748257181 小时前
Spring Boot FileUpLoad and Interceptor(文件上传和拦截器,Web入门知识)
前端·spring boot·后端
桃园码工1 小时前
15_HTML5 表单属性 --[HTML5 API 学习之旅]
前端·html5·表单属性
百万蹄蹄向前冲2 小时前
2024不一样的VUE3期末考查
前端·javascript·程序员
Anlici2 小时前
three.js建立3D模型展示地球+高亮
前端·数据可视化·canvas