首页 > web前端 > js教程 > 平滑用户体验的艺术:去抖动和节流以获得更高性能的用户界面

平滑用户体验的艺术:去抖动和节流以获得更高性能的用户界面

DDD
发布: 2025-01-01 02:05:09
原创
897 人浏览过

Github 代码库

在快节奏的世界中,我们所做的大部分工作都是在网络上完成的,而且速度很快。创建无缝、流畅的用户体验变得更加重要。消费者喜欢运行速度快、没有滞后或延迟的用户界面。实现近乎完美的体验是可能的,尽管很棘手。您听说过事件循环吗?

在 JavaScript 中,事件循环是一个基本概念,它管理代码的执行顺序、收集进程、将指令放入排队的子任务中并高效地运行异步操作。以下是事件循环工作原理的快速分解:

  • 调用堆栈:所有函数在调用时都会添加到此堆栈中,控制流从函数返回时,会从堆栈中弹出
  • 堆:所有变量和对象都从该堆分配内存
  • 队列:消息/指令列表 - 依次执行

此事件循环不断检查调用堆栈。 JavaScript 代码的执行将继续,直到调用堆栈为空。

事件处理是构建 JavaScript 应用程序的一个非常重要的部分。在这样的应用程序中,我们可能需要将多个事件与 UI 组件相关联。

The art of Smooth UX : Debouncing and Throttling for a more performant UI

想象一下...

UI 中有一个按钮,可帮助在表格中填充最新的体育新闻。现在这需要您:

  • 单击按钮(将“单击”事件处理程序与按钮关联。
  • 从 API 获取结果
  • 解析输出(Json)并显示

这 3 个进程以同步方式链接在一起。现在,反复按下按钮意味着多次 API 调用 - 导致 UI 被阻塞相当长的时间 - 看似滞后的用户体验。

这是 Debouncing 和 Throttling 等方法的一个很好的用例。对于此类触发一系列复杂事件的事件 - 我们可以使用此类操作来限制调用 API 的次数,或者一般意义上 - 限制我们处理事件的速率。

什么是去抖与节流?

去抖动:推迟函数的执行,直到自上次事件以来经过指定的冷却时间。

例如:

如果我们对handleOnPressKey()进行反跳2秒,则只有当用户停止按键2秒时才会执行。

设想:

  • 初始按键: 启动 2000ms 定时器来调用handleOnPressKey()。
  • 1000ms内的后续按键:计时器重置;从最近一次按键开始,我们又等待了 2000 毫秒。
  • 2000ms 内没有按键: 计时器完成,调用handleOnPressKey()。

代码片段:

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秒执行一次,即使滚动事件在该时间段内触发多次。

设想:

  • 初始滚动事件:调用handleOnScroll(),并开始2000毫秒的冷却时间。
  • 2000 毫秒内的后续滚动事件: 由于冷却时间处于活动状态,这些事件将被忽略。
  • 2000ms后的滚动事件:再次调用handleOnScroll()。

代码示例:

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 提供支持的搜索功能以及动态建议的下拉菜单。

The art of Smooth UX : Debouncing and Throttling for a more performant UI

让我们快速看一下 HTML 代码,然后再进入更关键的 script.js

我们使用 TailwindCSS 来快速设计样式。您可以在此处查看他们的文档 Tailwind 文档 - 它对于制作快速原型非常有帮助

  • 标题:标题包含页面的标题。
  • 输入字段:用于添加注释的输入字段,使用 Tailwind CSS 样式。
  • 建议下拉菜单:隐藏的下拉菜单,将在用户键入时显示建议。
  • 静态任务列表:显示已添加任务的列表。
  • 脚本:包括用于模糊搜索的 Fuse.js 库和用于自定义 JavaScript 逻辑的 script.js 文件。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event Loop Practice</title>
    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <style>
        /* Tailwind Extensions (Optional for Customizations) */
        body {
            font-family: 'Inter', sans-serif;
        }
    </style>
</head>
<body>



<h3>
  
  
  Why Use Fuse.js?
</h3>

<p>Fuse.js is a lightweight, customizable library for fuzzy searching. It handles typos and partial matches, offers high performance for large datasets, and has an intuitive API. This will help enhance your search functionality with flexible, user-friendly search experiences. Additionally, this provides you with a CDN link, so it can work right of the bat, no imports or local storage required.</p>

<h2>Now let's Code in the Real Deal - The JS</h2>

<h4>
  
  
  1. Task Array and Variables
</h4>



<pre class="brush:php;toolbar:false">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 函数。一旦用户单击“提交”箭头,就会触发此功能。它阻止默认表单提交,检索输入值,清除输入字段,将新任务添加到任务数组,并更新任务列表。

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);
登录后复制
登录后复制

现在我们需要确保任务提交后,它会在任务列表中更新

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);
登录后复制
登录后复制

updateList() 函数通过循环遍历任务数组并为每个任务创建列表项来呈现任务列表。每个列表项都包含一个项目符号点和任务文本。

现在我们需要确保列表在第一次加载页面后得到更新。我们还希望在页面加载时初始化 Fuse.js - 并将任务数组与其关联。请记住,我们希望在下拉列表中呈现来自此任务数组的建议。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event Loop Practice</title>
    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <style>
        /* Tailwind Extensions (Optional for Customizations) */
        body {
            font-family: 'Inter', sans-serif;
        }
    </style>
</head>
<body>



<h3>
  
  
  Why Use Fuse.js?
</h3>

<p>Fuse.js is a lightweight, customizable library for fuzzy searching. It handles typos and partial matches, offers high performance for large datasets, and has an intuitive API. This will help enhance your search functionality with flexible, user-friendly search experiences. Additionally, this provides you with a CDN link, so it can work right of the bat, no imports or local storage required.</p>

<h2>Now let's Code in the Real Deal - The JS</h2>

<h4>
  
  
  1. Task Array and Variables
</h4>



<pre class="brush:php;toolbar:false">const tasks = new Array (
    "Complete Blog on Throttling + Debouncing",
    "Make a list of 2025 Resolutions",
);
let fuse = undefined;
let debounceTimer;
let throttleTimer;
登录后复制
登录后复制
const onSubmit = (event) => {
    //Prevent default
    event.preventDefault();

    const text = document.getElementById("input").value.trim();
    document.getElementById("input").value = "";
    tasks.push(text);
    updateList();
}
登录后复制

现在我们需要确保在每个“输入”中我们都会搜索列表以在下拉列表中显示建议。这有 3 个部分:

  • 编写搜索逻辑:searchTasks()
  • 在每个输入上填充下拉列表:updateDropdown()
  • 关联要在每个输入上调用的 updateDropdown() (至少现在是这样:-) ->直到我们实现去抖动/节流逻辑)
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);
    })
}
登录后复制
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);
登录后复制

到目前为止:每次您输入内容时,下拉列表都会更新 - 在更庞大的 UI 中,我们不希望出现这种体验

在庞大的 UI 中每次击键时更新下拉列表可能会导致性能问题,从而导致延迟和糟糕的用户体验。频繁的更新可能会压垮事件循环,导致处理其他任务的延迟。

我们现在将了解如何使用去抖或节流来帮助管理更新频率,确保更流畅的性能和更灵敏的界面。

以下是我们如何在笔记制作项目中实现这两种技术的方法。

去抖:

去抖动确保函数仅在自上次调用以来经过指定时间后才被调用。这对于搜索输入字段等场景非常有用,在这些场景中我们希望等待用户完成输入后再进行 API 调用。

代码片段:

//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);
}
登录后复制

说明

  • 输入事件监听器附加到输入字段。
  • clearTimeout 函数会清除任何现有的反跳计时器。
  • setTimeout 函数设置一个新的去抖定时器,时间为 1 秒。如果在此期间没有检测到输入,则使用输入值调用 searchTasks 函数。

限制(在同一用例中)- 使用两种方法之一

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";
}
登录后复制

说明

  • let lastCall = 0;: 初始化一个变量来跟踪最后一次调用 searchTasks 的时间。
  • document.getElementById("input").addEventListener("input", (event) => { ... });: 将输入事件侦听器附加到输入字段。
  • const now = Date.now();: 获取当前时间(以毫秒为单位)。
  • const delay = 1000;: 将油门延迟设置为 1 秒。
  • if (now - lastCall >= delay) { ... }:检查自上次调用以来是否已经过去了足够的时间。
    • const query = event.target.value.trim();: 检索修剪后的输入值。
    • searchTasks(query);: 使用输入值调用 searchTasks 函数。
    • lastCall = now;:将lastCall时间更新为当前时间。

但是,请注意:限流并不是最适合这种场景,因为它将函数执行的频率限制在固定的时间间隔内,这可能无法为实时搜索建议提供最佳的用户体验。用户期望在输入时立即获得反馈,而限制可能会导致明显的延迟。

更好的节流用例

限流更适合您想要控制事件处理速率以避免性能问题的场景。以下是一些示例:

  • 窗口大小调整:当用户调整浏览器窗口大小时,您可能想要更新布局或执行计算。限制可确保这些更新以受控的速率发生,从而防止过多的函数调用。
  • 滚动:处理滚动事件时,例如加载更多内容或根据滚动位置更新 UI,节流有助于管理更新频率,确保流畅的性能。
  • API 速率限制:进行 API 调用时,限制可以通过控制请求频率来帮助您保持在速率限制内。

通过在这些场景中使用限制,您可以提高性能并确保更流畅的用户体验。

在此处查找完整代码

编码快乐!


请留下反馈!

我希望您觉得这个博客有帮助!您的反馈对我来说非常宝贵,所以请在下面的评论中留下您的想法和建议。

请随时在 LinkedIn 上与我联系以获取更多见解和更新。让我们保持联系,继续共同学习和成长!

以上是平滑用户体验的艺术:去抖动和节流以获得更高性能的用户界面的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板