流畅用户体验的艺术:防抖和节流,打造更高性能的 UI
Github 代码库
在这个快节奏的世界里,我们所做的大部分工作都是在网络上进行的,而且速度很快。创造无缝、流畅的用户体验变得越来越重要。消费者喜欢运行速度快、没有延迟或延迟的用户界面。实现近乎完美的体验是可能的,尽管有些棘手。你听说过事件循环吗?
在 JavaScript 中,事件循环是一个基本概念,它管理代码的执行顺序、收集进程、将指令放入排队的子任务中并高效运行异步操作。以下是事件循环工作原理的快速分解:
这个事件循环不断检查调用堆栈。JavaScript 代码的执行持续到调用堆栈为空。
事件处理是构建 JavaScript 应用程序的一个非常关键的部分。在这样的应用程序中,我们可能需要将多个事件与一个 UI 组件关联起来。

想象...
您的 UI 中有一个按钮,它可帮助在表格中填充最新的体育新闻。现在这需要您:
这 3 个过程以同步方式串联在一起。现在反复按下按钮将意味着多次 API 调用 - 导致 UI 被阻塞好几秒 - 用户体验似乎很差。
这是防抖和节流等方法的一个很好的用例。对于触发一系列复杂事件的此类事件,我们可以使用这些策略来限制调用 API 的次数,或者从一般意义上讲,限制我们处理事件的速率。
防抖与节流有什么区别?
**去抖动**:推迟执行某个功能,直到自上次事件发生以来经过指定的冷却期。
例如:
如果我们将 `handleOnPressKey()` 去抖动**2 秒**,则只有当用户停止按键**2 秒**时它才会执行。
设想:
**代码片段:**
let debounceTimer; // Timer reference
const handleOnPressKey = () => {
console.log("Key pressed and debounce period elapsed!");
};
const debouncedKeyPress = () => {
// Clear any existing timer
clearTimeout(debounceTimer);
// Start a new debounce timer
debounceTimer = setTimeout(() => {
handleOnPressKey(); // Execute the function after cooldown
}, 2000); // Cooldown period of 2000ms
};
// Attach debouncedKeyPress to keypress events
document.getElementById("input").addEventListener("keypress", debouncedKeyPress);**限制**:确保在指定的时间段内最多调用一次函数,无论事件发生的频率如何。
例如:
如果我们以**2 秒间隔**限制 `handleOnScroll()`,则该函数将每 2 秒最多执行一次,即使滚动事件在此期间触发多次。
设想:
**代码示例:**
let throttleTimer; // Timer reference
const handleOnScroll = () => {
console.log("Scroll event processed!");
};
const throttledScroll = () => {
if (!throttleTimer) {
handleOnScroll(); // Execute the function immediately
throttleTimer = setTimeout(() => {
throttleTimer = null; // Reset timer after cooldown
}, 2000); // Cooldown period of 2000ms
}
};
// Attach throttledScroll to scroll events
document.addEventListener("scroll", throttledScroll);现在让我们构建一些东西
该项目是一款现代待办事项列表应用,旨在探索事件处理中的防抖和节流概念。它具有实时任务添加、由 Fuse.js 提供支持的搜索功能以及动态建议下拉菜单。

在讨论更关键的“script.js”之前,我们先快速看一下 HTML 代码
我们使用 TailwindCSS 进行快速样式设置。您可以在此处查看其文档 Tailwind 文档 - 它对于快速制作原型非常有帮助
Event Loop Practice
Make Notes
send
Static Task List
为什么使用 Fuse.js?
Fuse.js 是一个轻量级、可自定义的模糊搜索库。它可以处理拼写错误和部分匹配,为大型数据集提供高性能,并具有直观的 API。这将有助于通过灵活、用户友好的搜索体验增强您的搜索功能。此外,它还为您提供了 CDN 链接,因此它可以立即工作,无需导入或本地存储。
现在让我们开始编写真正的代码 - JS
1. 任务数组和变量
const tasks = new Array (
"Complete Blog on Throttling + Debouncing",
"Make a list of 2025 Resolutions",
);
let fuse = undefined;
let debounceTimer;
let throttleTimer;本节初始化任务数组并声明 Fuse.js、防抖定时器和节流阀定时器的变量。为了这个项目,我们已经对一些任务进行了硬编码
现在让我们构建“onSubmit”函数。一旦用户单击“提交箭头”,就会触发此函数。它会阻止默认的表单提交、检索输入值、清除输入字段、将新任务添加到任务数组并更新任务列表。
const onSubmit = (event) => {
//Prevent default
event.preventDefault();
const text = document.getElementById("input").value.trim();
document.getElementById("input").value = "";
tasks.push(text);
updateList();
}现在我们需要确保一旦任务提交,它就会在任务列表中更新
const updateList = () => {
const lists = document.getElementById("taskList");
lists.innerHTML = "";
//Loop through all elements in tasks
tasks.forEach(task => {
const taskElement = document.createElement("li");
taskElement.classList.add("flex", "items-center", "space-x-2");
//Add Bullet Point Element
const bullet = document.createElement("span");
bullet.classList.add("h-2", "w-2", "bg-blue-500", "rounded-full");
//Add Span Tag
const taskText = document.createElement("span");
taskText.textContent = task;
taskElement.appendChild(bullet);
taskElement.appendChild(taskText);
lists.appendChild(taskElement);
})
}`updateList()` 函数通过循环遍历任务数组并为每个任务创建列表项来呈现任务列表。每个列表项都包含一个要点和任务文本。
现在我们需要确保列表在页面首次加载后得到更新。我们还希望在页面加载时初始化 Fuse.js - 并将“tasks”数组与其关联。请记住,我们希望在下拉列表中从此“tasks”数组呈现建议。
const init = () => {
console.log("Initializing...");
//Update and render the list
updateList();
//Initialize Fuse with the updated array
try{
fuse = new Fuse(tasks, {
includeScore: true,
threshold: 0.3 //For sensitivity
})
} catch(e) {
console.log("Error initializing Fuse:"+ fuse);
}
}document.addEventListener("DOMContentLoaded", init);现在我们需要确保每次输入时,我们都会搜索列表以在下拉列表中显示建议。这包括 3 个部分:
//Utility function to search within already entered values
const searchTasks = (query) => {
const result = fuse.search(query);
const filteredTasks = result.map(result => result.item)
updateDropdown(filteredTasks);
}const updateDropdown = (tasks) => {
const dropdown = document.getElementById("dropdown");
dropdown.innerHTML = "";
if(tasks.length === 0) {
dropdown.style.display = "none";
return;
}
tasks.forEach(task => {
const listItem = document.createElement("li");
listItem.textContent = task;
listItem.addEventListener("click", () => {
document.getElementById("input").value = task;
dropdown.style.display = "none";
})
dropdown.appendChild(listItem);
});
dropdown.style.display = "block";
}document.getElementById("submitButton").addEventListener("input", () => {
searchTasks(event.target.value)
});到目前为止:每次输入内容时,下拉列表都会更新 - 在更庞大的用户界面中,我们不希望有这种体验
在庞大的用户界面中,每次按键时更新下拉列表可能会导致性能问题,造成延迟和糟糕的用户体验。频繁更新可能会使事件循环不堪重负,从而导致处理其他任务的延迟。
我们现在将看到如何使用 Debouncing 或 throttling 来帮助管理更新频率,确保更流畅的性能和更灵敏的界面。
下面说明了我们如何在笔记制作项目中实现这两种技术。
防抖动:
防抖功能可确保仅在自上次调用以来经过指定的时间后才调用函数。这对于搜索输入字段等场景非常有用,在这些场景中,我们希望等待用户完成输入后再进行 API 调用。
**代码片段**:
document.getElementById("input").addEventListener("input", (event) => {
// Implement Debouncing - wait for 1 second of no input
clearTimeout(debounceTimer); //debounceTimer is already declared in the beginning
debounceTimer = setTimeout(() => {
const query = event.target.value;
searchTasks(query); // Call search function with the input value
}, 1000);
});**解释**:
节流(在相同用例中)- 使用以下两种方法之一
let lastCall = 0; // To track the last time searchTasks was called
document.getElementById("input").addEventListener("input", (event) => {
const now = Date.now();
const delay = 1000; // Throttle delay (1 second)
// If enough time has passed since the last call, run the search
if (now - lastCall >= delay) {
const query = event.target.value.trim();
searchTasks(query); // Call search function with the input value
lastCall = now; // Update last call time
}
});**解释**:
但请注意:节流并不适合这种情况,因为它将函数执行频率限制为固定间隔,这可能无法为实时搜索建议提供最佳用户体验。用户希望在输入时立即得到反馈,而节流可能会带来明显的延迟。
节流的更好用例
节流更适合于您想要控制事件处理速率以避免性能问题的场景。以下是一些示例:
通过在这些场景中使用限制,您可以提高性能并确保更流畅的用户体验。
完整代码请见此处
祝您编码愉快!
请留下反馈!
希望您觉得本博客对您有帮助!您的反馈对我来说非常宝贵,因此请在下面的评论中留下您的想法和建议。
欢迎随时在 LinkedIn 上与我联系,获取更多见解和更新。让我们保持联系,继续共同学习和成长!