首頁 > 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
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板