原生 JavaScript 实现动态任务筛选:事件委托、状态管理与实时搜索
原生 JavaScript 实现动态任务筛选:事件委托、状态管理与实时搜索
在前端项目中,任务管理、订单后台和数据列表经常需要提供多条件筛选功能。例如,用户可以根据任务状态切换列表,同时通过搜索框快速定位目标记录。
这类功能不一定需要引入 Vue 或 React。对于结构相对简单的页面,使用原生 JavaScript、DOM API 和事件委托,就可以构建可维护的交互组件。
本文以一个任务管理面板为例,实现状态筛选、实时搜索和结果数量统计。
一、设计组件结构
首先创建一个任务管理区域,包含五个状态筛选按钮、搜索输入框和任务列表。
<section class="task-panel" data-task-app>
<h2>任务管理面板</h2>
<div class="task-toolbar" role="group" aria-label="任务状态筛选">
<button
type="button"
id="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/36e141169c80f56ddb0e450d14c6202c950a269d"
data-filter="all"
aria-pressed="true">
全部任务
</button>
<button
type="button"
class="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/122aeaf76134cd9d0f862b62537496bee4dbae67"
data-filter="pending"
aria-pressed="false">
待处理
</button>
<button
type="button"
id="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/0770149d2f4d156370408074c6ed6cb0ea70f84e"
data-filter="running"
aria-pressed="false">
进行中
</button>
<button
type="button"
class="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/08934b0f4d25e34814c103518328f2d0bf9fde8c"
data-filter="paused"
aria-pressed="false">
已暂停
</button>
<button
type="button"
id="https://github.com/tiktokwuyu-ux/TIKTOK_baike/commit/e5447e0c2476c4b79e2f9ff8d6f998e386bdae9a"
data-filter="done"
aria-pressed="false">
已完成
</button>
</div>
<input
type="search"
data-search
placeholder="搜索任务名称"
aria-label="搜索任务名称"
>
<p data-result-count aria-live="polite"></p>
<ul class="task-list" data-task-list>
<li data-status="pending">检查用户提交的数据</li>
<li data-status="running">同步远程服务配置</li>
<li data-status="paused">整理历史任务记录</li>
<li data-status="done">完成数据库结构检查</li>
<li data-status="running">处理接口响应数据</li>
<li data-status="pending">验证前端页面兼容性</li>
</ul>
</section>
整个组件使用 data-* 属性描述交互行为。
data-filter 表示筛选按钮对应的状态,data-status 表示任务本身的状态。
这种设计能够将业务状态与样式选择器分离,减少后续修改 CSS 时对 JavaScript 逻辑的影响。
二、使用统一状态对象管理筛选条件
筛选功能通常包含多个条件。为了避免不同事件处理函数分别修改 DOM,可以先定义一个统一状态对象。
const app = document.querySelector("[data-task-app]");
const state = {
filter: "all",
keyword: ""
};
const taskList = app.querySelector("[data-task-list]");
const resultCount = app.querySelector("[data-result-count]");
const filterButtons = app.querySelectorAll("[data-filter]");
const searchInput = app.querySelector("[data-search]");
其中,filter 负责保存当前选中的任务状态,keyword 保存用户输入的搜索关键词。
当其中任意一个条件发生变化时,都交给同一个渲染函数处理。
这样可以避免不同筛选条件之间出现状态不一致的问题。
三、实现统一的筛选函数
接下来遍历任务列表,根据状态和关键词判断每条任务是否应该显示。
function renderTasks() {
const tasks = taskList.querySelectorAll("[data-status]");
let visibleCount = 0;
for (const task of tasks) {
const matchesStatus =
state.filter === "all" ||
task.dataset.status === state.filter;
const matchesKeyword = task.textContent
.toLocaleLowerCase("zh-CN")
.includes(state.keyword);
const visible = matchesStatus && matchesKeyword;
task.hidden = !visible;
if (visible) {
visibleCount++;
}
}
resultCount.textContent = `当前显示 ${visibleCount} 项任务`;
for (const button of filterButtons) {
const selected = button.dataset.filter === state.filter;
button.setAttribute("aria-pressed", String(selected));
}
}
这里有两个关键设计。
第一,使用 hidden 属性控制任务显示状态,不需要为每条任务反复设置内联样式。
第二,状态和关键词采用逻辑与关系。只有同时满足当前状态和搜索条件,任务才会显示。
当用户切换状态时,原来的搜索关键词仍然保留。
四、通过事件委托处理筛选按钮
传统做法通常为每个按钮分别绑定一个点击事件。
当按钮数量增加,或者页面需要动态插入新的筛选项时,这种方式维护起来比较麻烦。
使用事件委托,可以直接在父容器监听点击操作。
app.addEventListener("click", (event) => {
const button = event.target.closest("button[data-filter]");
if (!button || !app.contains(button)) {
return;
}
state.filter = button.dataset.filter;
renderTasks();
});
closest() 会从事件目标开始,向上查找符合选择器的元素。
这意味着即使按钮内部包含图标或其他子元素,点击事件仍然可以正确定位到对应按钮。
相比于在多个按钮中重复编写事件处理函数,这种方式能够将交互逻辑集中到一个位置。
五、使用防抖优化搜索输入
用户输入关键词时,input 事件可能在短时间内连续触发。
如果每次输入都执行复杂的数据过滤,可能造成不必要的计算。
可以通过防抖机制控制执行频率。
function debounce(callback, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
callback(...args);
}, delay);
};
}
const handleSearch = debounce((value) => {
state.keyword = value
.trim()
.toLocaleLowerCase("zh-CN");
renderTasks();
}, 200);
searchInput.addEventListener("input", (event) => {
handleSearch(event.target.value);
});
这里的 200 表示输入停止约 200 毫秒后再执行搜索。
实际项目中,防抖延迟应根据列表规模、输入体验和计算成本调整。
需要注意,防抖更适合搜索建议和非即时性筛选。如果操作需要立即反馈,则不一定适合增加延迟。
六、补充基础 CSS
为了让筛选状态更加直观,可以根据 aria-pressed 属性设置按钮样式。
.task-panel {
max-width: 760px;
margin: 0 auto;
padding: 24px;
}
.task-toolbar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.task-toolbar button {
padding: 8px 14px;
border: 1px solid #d0d7de;
border-radius: 6px;
background: #fff;
cursor: pointer;
}
.task-toolbar button[aria-pressed="true"] {
background: #0969da;
border-color: #0969da;
color: #fff;
}
[data-search] {
width: 100%;
padding: 10px;
box-sizing: border-box;
}
.task-list {
padding-left: 20px;
}
.task-list li {
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.task-list li[hidden] {
display: none;
}
使用属性选择器可以直接关联组件当前状态,无需在 JavaScript 中额外维护一套样式类名。
同时,aria-pressed 还能为辅助技术提供按钮选中状态。
七、处理动态添加的任务
当业务系统通过接口获取新数据时,任务列表可能随时发生变化。
如果组件采用事件委托,就不需要重新绑定筛选按钮的监听器。
例如,可以定义一个统一的任务插入函数:
function appendTask(task) {
const item = document.createElement("li");
item.dataset.status = task.status;
item.textContent = task.title;
taskList.appendChild(item);
renderTasks();
}
这里使用 textContent 设置任务标题,而不是直接拼接 innerHTML。
这种方式能够避免把用户输入当作 HTML 解析,降低动态内容带来的注入风险。
新增任务后调用 renderTasks(),就能自动应用当前的搜索和状态筛选条件。
八、性能与可维护性建议
当任务数量较少时,直接遍历 DOM 元素通常能够满足性能要求。
但如果数据量达到数千甚至数万条,频繁访问 DOM 可能增加渲染成本。
实际开发中,可以根据业务规模逐步优化:
- 维护独立数据模型:将任务数据保存在 JavaScript 对象中,DOM 只负责展示。
- 使用分页或虚拟列表:避免一次性渲染大量节点。
- 减少重复查询:缓存生命周期内稳定的 DOM 引用。
- 统一状态更新入口:所有筛选行为最终都经过同一个渲染流程。
- 处理异步请求竞争:远程筛选时可结合
AbortController取消过期请求。
这些优化应根据实际性能测试结果决定,不需要在简单组件中提前引入复杂架构。
九、总结
使用原生 JavaScript 构建动态筛选组件,核心不是监听多少个按钮,而是如何合理组织状态、事件和渲染逻辑。
通过 data-* 属性描述业务状态、事件委托处理交互、统一渲染函数管理页面,以及防抖减少重复计算,可以建立一个结构清晰、容易扩展的任务管理组件。
对于后台管理系统、数据筛选页面和轻量级业务工具,这套方案能够在不引入额外框架的情况下满足常见交互需求。
浙公网安备 33010602011771号