用原生JavaScript实现待办事项列表:基础语法综合实战

作者:交互手记

时间:2026-10-30

浏览:218

通过构建经典待办事项(To-Do List)应用,系统串联原生JavaScript基础语法、DOM增删查改、事件冒泡与委托机制以及localStorage持久化存储方案。

文章目录

待办事项列表(To-Do List)是检验前端基本功最直观的小型项目。本文不依赖任何第三方类库或打包工具,使用纯原生 HTML、CSS 和 JavaScript 构建一个具备增删、切换状态与本地数据持久化功能的待办应用。通过这个实战案例,可以一次性串联变量声明、数组操作、DOM 查询与节点创建、事件委托机制以及浏览器存储 API。

一、整体架构与开发流程

实现一个结构清晰的待办应用,推荐将数据状态与界面渲染解耦。传统的做法是直接在按钮点击时往列表中插入 HTML 字符串,这种方式一旦遇到稍复杂的交互就容易导致状态混乱。更为稳健的逻辑是:让用户操作去修改内存中的数据列表,再根据最新数据统一绘制页面。

用原生JavaScript实现待办事项列表:基础语法综合实战
图1:待办事项应用的分层设计与四个核心开发阶段

二、构建页面结构与样式基石

HTML 负责表达功能结构,CSS 提供基础视觉区分。我们只需要一个表单(包含文本输入框和提交按钮)以及一个用于挂载条目的 <ul> 容器。

使用 <form> 包裹输入框的优势在于:用户输入完成后按下键盘回车键(Enter),表单会自动触发 submit 事件,无需再手动编写回车监听代码。关键骨架如下:

<!-- HTML 骨架 -->
<div class="todo-app">
  <header>
    <h1>我的待办清单</h1>
    <form id="todo-form">
      <input 
        type="text" 
        id="todo-input" 
        placeholder="写下你要做的事情..." 
        autocomplete="off"
        required 
      />
      <button type="submit" id="add-btn">添加</button>
    </form>
  </header>

  <ul id="todo-list" class="todo-list">
    <!-- 动态由 JS 渲染待办条目 -->
  </ul>

  <footer class="todo-footer">
    <span id="todo-count">未完成:0 项</span>
  </footer>
</div>

在样式上,核心在于为完成状态赋予视觉反馈。当某个待办项被标记为完成时,为其添加 completed 类名,通过 text-decoration: line-through 展示删除线,并降低文字颜色透明度。

三、核心数据结构设计与 DOM 渲染

在编写 JavaScript 前,首先明确单个待办项的数据结构。我们使用数组容纳待办对象,每个对象包含三项基础属性:

  • id:时间戳或自增整数,作为唯一标识,用于准确查找和更新特定项;
  • text:用户输入的文本描述;
  • completed:布尔值,记录完成状态(true 或 false)。

很多初学者在此容易陷入一个误区:每次添加条目时,直接调用 ul.innerHTML += newHtml。这种做法看似省事,实则每次都会销毁整个列表内的已有 DOM 节点并重新构造,不仅丢失原有未提交的临时状态(如焦点),还会产生不必要的性能损耗。正确的做法是基于数组遍历,使用 document.createElement 或一次性组装 DocumentFragment 完成追加,或者在数据变化时统一定向渲染。

四、交互设计与事件委托机制

待办列表具有动态增删的特性:条目在页面加载时可能并不存在,而是由用户后续不断点击添加生成的。如果每生成一个 <li> 节点就为其内部的复选框与删除按钮单独调用一次 addEventListener,会带来两个明显缺陷:

  1. 内存占用随条目增多而线性上升;
  2. 每处新增逻辑中都必须记得编写绑定代码,容易遗漏。

利用浏览器的事件冒泡机制,我们可以采用事件委托(Event Delegation)模式:只在父级容器 <ul id="todo-list"> 上绑定一次监听器,通过检查事件对象的 event.target,判断用户点击的是复选框还是删除按钮,进而执行对应操作。

用原生JavaScript实现待办事项列表:基础语法综合实战
图2:给每个子项绑定监听器与父容器事件委托的机制对比

五、状态持久化与 LocalStorage 读写流

纯内存数组在网页刷新或关闭标签页后会立即重置。为了让用户记录的数据得到保存,需要借助浏览器提供的 localStorage API。该 API 仅支持存储键值字符串,因此在保存与恢复时需要进行 JSON 转换:

  • 保存到本地: localStorage.setItem('my_todos', JSON.stringify(todos))
  • 从本地还原: const data = JSON.parse(localStorage.getItem('my_todos') || '[]')
用原生JavaScript实现待办事项列表:基础语法综合实战
图3:待办数据在内存、持久化存储与视图之间的流动模型

六、完整原生代码实现

下面给出完整的代码组织,将逻辑按关注点分为“状态初始化”、“持久化助手”、“渲染引擎”和“事件监听”四个清晰区块:

// 1. 获取关键 DOM 节点
const todoForm = document.getElementById('todo-form');
const todoInput = document.getElementById('todo-input');
const todoList = document.getElementById('todo-list');
const todoCount = document.getElementById('todo-count');

// 存储键名常量
const STORAGE_KEY = 'practical_todos_data';

// 2. 状态初始化:优先读取本地缓存,无数据则退化为空数组
let todos = loadTodos();

function loadTodos() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? JSON.parse(raw) : [];
  } catch (err) {
    console.error('读取本地存储失败,重置为空数组:', err);
    return [];
  }
}

function saveTodos() {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
  } catch (err) {
    console.warn('保存到 localStorage 失败:', err);
  }
}

// 3. 视图渲染函数:负责把 todos 数组完整映射为 DOM 节点
function render() {
  // 清空现有内容
  todoList.innerHTML = '';

  if (todos.length === 0) {
    const emptyNotice = document.createElement('li');
    emptyNotice.className = 'todo-empty';
    emptyNotice.textContent = '暂无待办事项,轻松一下吧!';
    todoList.appendChild(emptyNotice);
  } else {
    // 遍历生成每个待办项
    todos.forEach(todo => {
      const li = document.createElement('li');
      li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
      li.dataset.id = todo.id;

      // 复选框
      const checkbox = document.createElement('input');
      checkbox.type = 'checkbox';
      checkbox.className = 'toggle-status';
      checkbox.checked = todo.completed;

      // 文本容器
      const span = document.createElement('span');
      span.className = 'todo-text';
      span.textContent = todo.text; // 避免直接 innerHTML 拼接,防止 XSS

      // 删除按钮
      const deleteBtn = document.createElement('button');
      deleteBtn.className = 'delete-btn';
      deleteBtn.textContent = '删除';
      deleteBtn.setAttribute('aria-label', `删除待办:${todo.text}`);

      li.append(checkbox, span, deleteBtn);
      todoList.appendChild(li);
    });
  }

  // 更新未完成计数
  const activeCount = todos.filter(t => !t.completed).length;
  todoCount.textContent = `未完成:${activeCount} 项`;
}

// 4. 事件监听一:添加待办(利用表单 submit 事件)
todoForm.addEventListener('submit', (e) => {
  e.preventDefault(); // 阻止页面默认刷新行为
  
  const text = todoInput.value.trim();
  if (!text) return;

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

  todos.push(newTodo);
  saveTodos();
  render();

  todoInput.value = '';
  todoInput.focus();
});

// 5. 事件监听二:利用事件委托集中处理勾选与删除
todoList.addEventListener('click', (e) => {
  const target = e.target;
  const parentLi = target.closest('li.todo-item');
  if (!parentLi) return;

  const targetId = Number(parentLi.dataset.id);

  // 点击了状态切换复选框
  if (target.classList.contains('toggle-status')) {
    const targetTodo = todos.find(item => item.id === targetId);
    if (targetTodo) {
      targetTodo.completed = target.checked;
      saveTodos();
      render();
    }
  }

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

// 6. 首屏首次执行渲染
render();

七、容易混淆的要点与避坑指南

在编写原生应用的过程中,有几个细节往往会成为逻辑漏洞的来源,需要特别留意:

  • XSS 防护与 textContent: 展示用户输入的文本时,始终使用 node.textContent = text 或对特殊字符做转义,切忌直接采用模板字符串拼进 innerHTML(例如 <span>${todo.text}</span>),否则一旦用户输入包含 <img src=x onerror=alert(1)>,就会引发跨站脚本注入。
  • 表单的 preventDefault: 若没有在 submit 回调中调用 e.preventDefault(),页面会在点击按钮时向当前地址发起 GET 请求并刷新页面,导致数据看似“一闪而过”没有被添加。
  • dataset 属性的类型转换: 存放在 HTML 属性 dataset.id 中的值全部为字符串。如果你的原始数据中 id 是数字类型(如 Date.now()),在做全等对比(item.id === targetId)时务必记得用 Number(parentLi.dataset.id) 做显式类型转换,否则比较结果将始终为 false。
  • localStorage 的存储异常: 在无痕浏览模式或存储空间满载时,调用 localStorage.setItem 可能抛出 QuotaExceededError 异常,使用 try...catch 包裹能够有效避免程序崩溃。

八、核心知识体系全景回顾

通过这个实战项目,我们可以把看似散落的原生 JavaScript 基础语法凝聚成一个具备实战价值的知识闭环:

用原生JavaScript实现待办事项列表:基础语法综合实战
图4:从原生 JavaScript 基础语法到状态驱动的知识体系架构

综上所述,即便脱离庞大的现代前端开发框架,依靠清晰的“状态驱动渲染”逻辑和扎实的原生 API 使用,我们同样能写出结构优美、高效且健壮的应用程序。理解这套底层运行机理,不仅能够夯实编程基本功,更是未来深入掌握各种主流前端框架不可或缺的基石。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章