【手写篇】你会用定时器、时间戳、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前端实验室 🛠️「精选资源|实战经验|技术洞见」

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
uhakadotcom9 小时前
视频直播与视频点播:基础知识与应用场景
后端·面试·架构
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
快速开始使用 n8n
后端·面试·github
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github