JavaScript实战小项目:原生JS实现待办事项列表(Todo List)

作者:流萤

时间:2026-10-30

浏览:411

本篇教程通过原生JavaScript实现一个完整的Todo List待办事项列表,涵盖结构搭建、添加任务、状态切换、删除任务、本地存储持久化及状态筛选,深入解析核心DOM操作与事件委托机制。

文章目录

待办事项列表(Todo List)是前端开发中最经典的练习项目,它麻雀虽小五脏俱全,集中涵盖了 DOM 元素获取、节点动态创建与挂载、事件监听与事件委托、浏览器本地存储(localStorage)以及数据驱动视图的基本思想。本文将完全脱离前端框架,使用纯原生 HTML、CSS 与 JavaScript,从零构建一个支持任务增删、状态标记、分类筛选与持久化存储的待办事项应用。

一、项目整体架构与设计思路

在编写代码前,明确数据与界面的关系是保证逻辑清晰的前提。传统的 DOM 脚本开发常陷入“直接操作页面元素修改状态”的混乱中,导致数据与显示脱节。本项目采用“状态驱动视图(State-Driven UI)”的轻量模式:界面的每次变化均源于内存中数组数据的更新,随后通过一个统一的渲染函数将最新数据映射到 HTML 视图上。

JavaScript实战小项目:原生JS实现待办事项列表(Todo List)
图 1:Todo List 状态流转与单向渲染架构

每个待办事项被建模为一个标准 JavaScript 对象:

{
  id: 1717200000000, // 唯一时间戳标识符
  text: "学习原生 JavaScript DOM 操作", // 任务具体文本
  completed: false   // 任务完成标记
}

二、HTML结构与基础样式搭建

界面的基本骨架包括四个主要区域:头部输入区、主体任务列表区、底部状态栏以及过滤切换按钮。我们创建基础 HTML 片段与辅助 CSS:

<!-- 容器根节点 -->
<div class="todo-app">
  <header class="todo-header">
    <h2>待办清单</h2>
    <div class="input-group">
      <input type="text" id="todo-input" placeholder="输入待办事项后按回车添加..." autofocus>
      <button id="add-btn">添加</button>
    </div>
  </header>

  <!-- 列表展示区 -->
  <ul id="todo-list" class="todo-list"></ul>

  <!-- 状态筛选与统计区 -->
  <footer class="todo-footer">
    <span id="items-left">0 项未完成</span>
    <div class="filter-group">
      <button class="filter-btn active" data-filter="all">全部</button>
      <button class="filter-btn" data-filter="active">未完成</button>
      <button class="filter-btn" data-filter="completed">已完成</button>
    </div>
    <button id="clear-completed-btn">清除已完成</button>
  </footer>
</div>

三、分步实施核心功能

实现该项目分为五个明确的阶段步骤,从状态定义、输入捕获到事件委托机制的建立,读者应按照逻辑顺序逐步演进。

JavaScript实战小项目:原生JS实现待办事项列表(Todo List)
图 2:Todo List 开发实施核心步骤

第 1 步:DOM 引用与初始状态初始化

首先获取关键节点的引用,并从浏览器的 localStorage 中恢复先前保存的任务。若无历史记录,则初始化为空数组:

// 获取相关 DOM 元素
const todoInput = document.getElementById('todo-input');
const addBtn = document.getElementById('add-btn');
const todoList = document.getElementById('todo-list');
const itemsLeft = document.getElementById('items-left');
const clearCompletedBtn = document.getElementById('clear-completed-btn');
const filterBtns = document.querySelectorAll('.filter-btn');

// 应用核心状态
let todos = JSON.parse(localStorage.getItem('todos_app_data')) || [];
let currentFilter = 'all'; // 筛选状态:'all' | 'active' | 'completed'

第 2 步:处理用户输入与任务添加

任务的添加通常有两种交互入口:点击“添加”按钮或在输入框内按下 Enter 键。添加前必须调用 trim() 去除首尾无意义的空白字符,防止空条目加入:

function addTodo() {
  const text = todoInput.value.trim();
  if (text === '') {
    alert('请输入任务内容!');
    return;
  }

  const newTodo = {
    id: Date.now(), // 以毫秒级时间戳作为唯一标识
    text: text,
    completed: false
  };

  todos.push(newTodo);
  todoInput.value = ''; // 清空输入框
  saveAndRender();
}

// 绑定添加按钮点击事件与回车按键事件
addBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    addTodo();
  }
});

第 3 步:深入理解并使用“事件委托”管理列表交互

列表中的任务是动态增减的。如果在每次创建 <li> 时都分别为其内部的复选框和删除按钮绑定 addEventListener,会带来两个严重问题:频繁绑定增加内存占用,且后续动态添加的项必须重新绑定。最标准、最优雅的做法是在父容器 <ul> 上进行事件委托(Event Delegation)。

JavaScript实战小项目:原生JS实现待办事项列表(Todo List)
图 3:事件冒泡与事件委托分发机制解析

事件委托的具体实现代码如下。利用 e.target 识别点击目标,再通过 closest('li') 获取对应的数据 ID:

todoList.addEventListener('click', (e) => {
  const target = e.target;
  const li = target.closest('li');
  if (!li) return;

  const id = Number(li.dataset.id);

  // 点击了完成复选框
  if (target.classList.contains('toggle-checkbox')) {
    const item = todos.find(t => t.id === id);
    if (item) {
      item.completed = target.checked;
      saveAndRender();
    }
  }

  // 点击了删除按钮
  if (target.classList.contains('delete-btn')) {
    todos = todos.filter(t => t.id !== id);
    saveAndRender();
  }
});

第 4 步:持久化存储与视图渲染逻辑

saveAndRender() 函数是整个系统的数据同步核心。它负责将当前 todos 序列化存入 localStorage,随后根据选中的过滤器更新 DOM:

function saveAndRender() {
  // 1. 持久化存储到浏览器
  localStorage.setItem('todos_app_data', JSON.stringify(todos));

  // 2. 依据筛选条件筛选待展示项
  let filteredTodos = todos;
  if (currentFilter === 'active') {
    filteredTodos = todos.filter(t => !t.completed);
  } else if (currentFilter === 'completed') {
    filteredTodos = todos.filter(t => t.completed);
  }

  // 3. 构建并挂载 HTML 片段
  todoList.innerHTML = '';
  filteredTodos.forEach(item => {
    const li = document.createElement('li');
    li.className = `todo-item ${item.completed ? 'completed' : ''}`;
    li.dataset.id = item.id;

    // 安全拼接:避免直接写入未转义字符串
    const checkbox = document.createElement('input');
    checkbox.type = 'checkbox';
    checkbox.className = 'toggle-checkbox';
    checkbox.checked = item.completed;

    const span = document.createElement('span');
    span.className = 'todo-text';
    span.textContent = item.text;

    const deleteBtn = document.createElement('button');
    deleteBtn.className = 'delete-btn';
    deleteBtn.textContent = '删除';

    li.appendChild(checkbox);
    li.appendChild(span);
    li.appendChild(deleteBtn);

    todoList.appendChild(li);
  });

  // 4. 更新未完成任务计数
  const activeCount = todos.filter(t => !t.completed).length;
  itemsLeft.textContent = `${activeCount} 项未完成`;
}
安全提醒:在向页面渲染文本内容时,请务必使用 element.textContent 或做字符转义,切勿使用直接字符串插值赋值给 innerHTML(例如 <span>${item.text}</span>),否则容易遭受 XSS(跨站脚本攻击)。

第 5 步:状态过滤与已完成任务清理

通过底部的过滤按钮,用户可以在“全部”、“未完成”和“已完成”三者间自由切换。同时提供一键清空所有已完成任务的便捷操作:

// 状态过滤事件绑定
filterBtns.forEach(btn => {
  btn.addEventListener('click', () => {
    filterBtns.forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    currentFilter = btn.dataset.filter;
    saveAndRender();
  });
});

// 清除所有已完成项
clearCompletedBtn.addEventListener('click', () => {
  todos = todos.filter(t => !t.completed);
  saveAndRender();
});

// 首次加载初始化渲染
saveAndRender();

四、常见易错问题与排查

初学者在实现该项目时,最容易在以下三个环节出现逻辑故障或表现异常:

  • ID 类型不匹配导致查找失败:在 HTML 属性中,通过 dataset.id 取出的值永远是字符串类型(String)。而在我们定义对象时,id 使用的是数值类型的 Date.now()。若直接使用严格相等比较 t.id === li.dataset.id 会永远返回 false。必须使用 Number(li.dataset.id) 显式转换后再比较。
  • localStorage 存取未做 JSON 序列化:localStorage 只能保存字符串数据。如果直接执行 localStorage.setItem('key', todos),存储进去的将是无意义的 [object Object] 字符串。读取和写入时必须分别配合 JSON.stringify() 与 JSON.parse() 使用,并给解析失败或初次加载提供兜底空数组 || []。
  • 输入空任务引发的脏数据:未进行 trim() 校验时,用户连续敲击空格也能成功创建空白条目,破坏列表排版与统计真实性。

五、知识体系与技术要点总结

通过实现这样一个原生的 Todo List 项目,我们打通了现代前端框架(如 React/Vue)底层所依赖的核心知识链路。回顾全文,各模块的技术要点与核心职责如下表图所示:

JavaScript实战小项目:原生JS实现待办事项列表(Todo List)
图 4:项目技能图谱与前端思维矩阵总结

掌握了原生 JavaScript 的 DOM 事件委托机制与数据驱动视图的思想后,在后续接触现代框架(如 Vue 的响应式数据、React 的不可变更新)时,便能看透框架在底层所封装的核心行为,从而写出性能更高、更加严谨的前端代码。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章