JS 基础入门|50 行代码实现 TodoList 待办清单(新手能看懂)

0 阅读10分钟

TodoList 是前端新手入门的经典实战案例,它几乎覆盖了 JavaScript 基础的核心知识点 —— 数组操作、DOM 操作、事件处理,能帮你快速把零散的知识点串联起来。今天我们用不到 50 行核心 JS 代码,从零实现一个功能完整、易理解的待办清单,新手也能跟着一步步写出来。

一、最终效果预览

先直观感受下我们要做的 TodoList 长什么样、能实现什么功能,做到心中有数:

功能清单

  1. ✅ 输入待办事项(支持点击按钮 / 按回车键),新增任务到列表
  2. ✅ 点击任务文本,切换「已完成 / 未完成」状态(文字变灰 + 删除线)
  3. ✅ 点击任务右侧「删除」按钮,移除指定任务
  4. ✅ 进阶优化:刷新页面后任务不丢失(本地存储持久化)
  5. ✅ 细节优化:输入空内容不添加任务、任务列表样式整洁

界面预览

plaintext

┌─────────────────────────────────────┐
│ [输入框:请输入待办事项...] [添加按钮] │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ ● 学习 JS 基础          [删除]       │
│ ● 买菜(已完成)        [删除]       │
│ ● 整理笔记              [删除]       │
└─────────────────────────────────────┘

二、核心知识点预热

在写代码前,先明确本次用到的 3 个核心知识点,提前梳理清楚,避免写代码时一头雾水:

知识点作用本次用到的核心方法 / 属性
数组操作存储所有待办事项,管理数据的增删改push()filter()find()forEach()
DOM 操作把数组里的任务渲染到页面,动态更新 UIgetElementById()createElement()appendChild()innerHTMLdataset
事件处理监听用户操作(点击、回车),触发逻辑addEventListener()、事件委托、e.target
本地存储(进阶)持久化保存数据,刷新页面不丢失localStorage.getItem()localStorage.setItem()JSON.parse()/JSON.stringify()

提示:如果对这些知识点完全陌生也没关系,后面写代码时会逐行讲解,先有个印象即可。

三、分步实现(从 0 到 1 写代码)

我们采用「HTML 结构 → CSS 样式 → JS 逻辑」的顺序实现,所有代码可直接复制到一个 .html 文件中运行,无需额外下载任何东西,新手友好度拉满。

第一步:搭建 HTML 结构(页面骨架)

先写出页面的基础结构,确定有哪些元素需要交互:

html

预览

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>新手版 TodoList</title>
    <!-- CSS 样式会写在这里 -->
</head>
<body>
    <!-- 待办清单容器 -->
    <div class="todo-container">
        <!-- 输入区域:输入框 + 添加按钮 -->
        <div class="todo-input-wrapper">
            <input type="text" id="todo-input" placeholder="请输入待办事项(按回车/点击添加)...">
            <button id="add-todo-btn">添加任务</button>
        </div>
        <!-- 任务列表容器 -->
        <ul class="todo-list" id="todo-list"></ul>
    </div>

    <!-- JS 逻辑会写在这里 -->
    <script>
        // 后续写核心逻辑
    </script>
</body>
</html>

第二步:添加 CSS 样式(让界面更友好)

没有样式的页面会很丑,我们添加简单的样式,重点是「清晰易读」,新手可先复制,后续再慢慢改:

css

/* 全局样式重置(简单版) */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: "微软雅黑", sans-serif;
}

/* 容器样式:居中 + 固定宽度 */
.todo-container {
    width: 500px;
    margin: 50px auto;
    padding: 20px;
    border: 1px solid #eee;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}

/* 输入区域样式 */
.todo-input-wrapper {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
}

#todo-input {
    flex: 1;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 14px;
    outline: none;
}

#todo-input:focus {
    border-color: #42b983; /* 聚焦时变绿色,提升体验 */
}

#add-todo-btn {
    padding: 0 20px;
    background-color: #42b983;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

#add-todo-btn:hover {
    background-color: #359469; /* 鼠标悬浮加深颜色 */
}

/* 任务列表样式 */
.todo-list {
    list-style: none; /* 去掉默认列表圆点 */
}

/* 单个任务项样式 */
.todo-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px;
    border-bottom: 1px solid #f5f5f5;
    margin-bottom: 8px;
    font-size: 14px;
}

/* 已完成任务样式 */
.todo-item.completed {
    color: #999;
    text-decoration: line-through; /* 删除线 */
}

/* 删除按钮样式 */
.delete-btn {
    color: #ff4444;
    border: none;
    background: none;
    cursor: pointer;
    font-size: 12px;
    padding: 4px 8px;
    border-radius: 4px;
}

.delete-btn:hover {
    background-color: #fef0f0; /* 悬浮加背景色 */
}

把这段 CSS 代码放到 <head> 标签内即可。

第三步:编写 JS 核心逻辑(实现交互功能)

这是最核心的部分,我们分 6 个小步骤写,每一步都有明确的目标,新手能跟上:

步骤 1:获取页面元素 + 初始化数据

先拿到需要操作的 DOM 元素,再定义数组存储任务数据:

js

// 1. 获取页面元素(DOM 操作第一步:找到要操作的元素)
const todoInput = document.getElementById('todo-input'); // 输入框
const addTodoBtn = document.getElementById('add-todo-btn'); // 添加按钮
const todoList = document.getElementById('todo-list'); // 任务列表容器

// 2. 初始化任务数组:从本地存储读取,没有则为空数组(进阶优化)
let todos = JSON.parse(localStorage.getItem('todos')) || [];

步骤 2:编写「添加任务」函数

实现输入内容、点击按钮新增任务的逻辑:

js

// 3. 添加任务函数
function addTodo() {
    // ① 获取输入框内容,去除首尾空格(避免添加空任务)
    const todoText = todoInput.value.trim();
    
    // ② 空内容直接返回,不执行后续逻辑
    if (!todoText) {
        alert('请输入待办事项内容!'); // 提示用户,更友好
        return;
    }
    
    // ③ 向数组中添加新任务(对象形式:id唯一标识、text内容、completed完成状态)
    todos.push({
        id: Date.now(), // 用时间戳做唯一ID,避免重复
        text: todoText,
        completed: false // 默认未完成
    });
    
    // ④ 清空输入框,方便下次输入
    todoInput.value = '';
    
    // ⑤ 保存到本地存储(进阶优化)
    saveTodosToLocalStorage();
    
    // ⑥ 重新渲染任务列表(让新增的任务显示在页面上)
    renderTodoList();
}

步骤 3:编写「渲染任务列表」函数

把数组里的任务动态渲染到页面上:

js

// 4. 渲染任务列表函数(把数组数据显示到页面)
function renderTodoList() {
    // ① 先清空列表容器(避免重复渲染)
    todoList.innerHTML = '';
    
    // ② 遍历任务数组,为每个任务创建li元素
    todos.forEach(todo => {
        // 创建li标签
        const li = document.createElement('li');
        
        // 给li添加类名:基础样式 + 完成状态样式
        li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
        
        // 把任务ID存到li的自定义属性中(方便后续查找对应任务)
        li.dataset.todoId = todo.id;
        
        // 拼接li的内容:任务文本 + 删除按钮
        li.innerHTML = `
            <span class="todo-text">${todo.text}</span>
            <button class="delete-btn">删除</button>
        `;
        
        // 把li添加到任务列表容器中
        todoList.appendChild(li);
    });
}

步骤 4:编写「删除 / 切换完成状态」函数

实现点击删除按钮删除任务、点击文本切换完成状态的逻辑:

js

// 5. 处理任务点击(删除/切换完成状态):用事件委托,只给父元素绑定一次事件
function handleTodoClick(e) {
    // ① 获取点击的目标元素
    const target = e.target;
    // ② 获取当前点击元素对应的任务li
    const todoItem = target.closest('.todo-item');
    if (!todoItem) return; // 不是任务项,直接返回
    
    // ③ 获取任务ID(从自定义属性中取)
    const todoId = Number(todoItem.dataset.todoId);
    
    // 情况1:点击了删除按钮
    if (target.classList.contains('delete-btn')) {
        // 过滤掉要删除的任务(保留ID不等于当前ID的任务)
        todos = todos.filter(todo => todo.id !== todoId);
    }
    
    // 情况2:点击了任务文本(切换完成状态)
    if (target.classList.contains('todo-text')) {
        // 找到对应任务,切换completed状态
        const todo = todos.find(todo => todo.id === todoId);
        if (todo) {
            todo.completed = !todo.completed;
        }
    }
    
    // ④ 保存到本地存储
    saveTodosToLocalStorage();
    
    // ⑤ 重新渲染列表
    renderTodoList();
}

// 辅助函数:保存任务到本地存储
function saveTodosToLocalStorage() {
    localStorage.setItem('todos', JSON.stringify(todos));
}

步骤 5:绑定事件(让函数生效)

给按钮、输入框、列表绑定事件,触发上面写的函数:

js

// 6. 绑定事件
// ① 点击添加按钮,触发添加任务
addTodoBtn.addEventListener('click', addTodo);

// ② 输入框按回车键,触发添加任务(优化体验)
todoInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
        addTodo();
    }
});

// ③ 任务列表绑定点击事件(事件委托)
todoList.addEventListener('click', handleTodoClick);

步骤 6:初始化渲染(页面加载时显示任务)

页面打开时,自动渲染本地存储中的任务:

js

// 7. 页面加载完成后,初始化渲染任务列表
renderTodoList();

完整 JS 代码汇总

把上面的 JS 代码整合到 <script> 标签内,完整代码如下:

js

// 1. 获取页面元素
const todoInput = document.getElementById('todo-input');
const addTodoBtn = document.getElementById('add-todo-btn');
const todoList = document.getElementById('todo-list');

// 2. 初始化任务数组
let todos = JSON.parse(localStorage.getItem('todos')) || [];

// 3. 添加任务函数
function addTodo() {
    const todoText = todoInput.value.trim();
    if (!todoText) {
        alert('请输入待办事项内容!');
        return;
    }
    todos.push({
        id: Date.now(),
        text: todoText,
        completed: false
    });
    todoInput.value = '';
    saveTodosToLocalStorage();
    renderTodoList();
}

// 4. 渲染任务列表函数
function renderTodoList() {
    todoList.innerHTML = '';
    todos.forEach(todo => {
        const li = document.createElement('li');
        li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
        li.dataset.todoId = todo.id;
        li.innerHTML = `
            <span class="todo-text">${todo.text}</span>
            <button class="delete-btn">删除</button>
        `;
        todoList.appendChild(li);
    });
}

// 5. 处理任务点击函数
function handleTodoClick(e) {
    const target = e.target;
    const todoItem = target.closest('.todo-item');
    if (!todoItem) return;
    const todoId = Number(todoItem.dataset.todoId);

    if (target.classList.contains('delete-btn')) {
        todos = todos.filter(todo => todo.id !== todoId);
    }

    if (target.classList.contains('todo-text')) {
        const todo = todos.find(todo => todo.id === todoId);
        if (todo) {
            todo.completed = !todo.completed;
        }
    }

    saveTodosToLocalStorage();
    renderTodoList();
}

// 6. 保存到本地存储辅助函数
function saveTodosToLocalStorage() {
    localStorage.setItem('todos', JSON.stringify(todos));
}

// 7. 绑定事件
addTodoBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') addTodo();
});
todoList.addEventListener('click', handleTodoClick);

// 8. 初始化渲染
renderTodoList();

四、代码核心讲解(新手必看)

写完代码后,重点理解这几个关键逻辑,才算真正掌握:

1. 为什么用「事件委托」?

我们没有给每个删除按钮 / 任务文本单独绑定事件,而是给父元素 todoList 绑定一次事件,原因是:

  • 减少事件绑定数量,提升性能;
  • 动态创建的任务项(新增的 li)也能触发事件,不用重新绑定。

核心原理:点击子元素时,事件会冒泡到父元素,通过 e.target 判断点击的是哪个子元素,再执行对应逻辑。

2. 为什么用 Date.now() 做任务 ID?

Date.now() 返回当前时间的毫秒数(13 位数字),每一刻的数值都不同,能保证每个任务的 ID 唯一,避免删除 / 修改时搞错任务。

3. 本地存储为什么要转 JSON?

localStorage 只能存储字符串类型,而我们的任务是数组(里面是对象),所以需要:

  • JSON.stringify(todos):把数组转成 JSON 字符串,存入本地存储;
  • JSON.parse(xxx):把本地存储的字符串转回数组,方便操作。

4. 为什么要先清空列表再渲染?

如果不执行 todoList.innerHTML = '',每次新增 / 修改任务时,数组会遍历并重复添加所有任务,导致页面出现重复的任务项。

五、运行测试(验证功能)

把所有代码整合到一个 .html 文件后,用浏览器打开,依次测试以下功能:

  1. 输入空内容,点击「添加任务」→ 弹出提示,不添加任务;
  2. 输入「学习 JS」,点击添加 → 列表出现该任务;
  3. 输入「买菜」,按回车键 → 列表新增该任务;
  4. 点击「学习 JS」文本 → 文字变灰加删除线(已完成);
  5. 再次点击「学习 JS」→ 恢复未完成状态;
  6. 点击「买菜」右侧的删除按钮 → 该任务消失;
  7. 刷新页面 → 剩余的任务还在(本地存储生效)。

六、新手拓展优化方向(可选)

如果想进一步提升,可以尝试实现这些功能,巩固知识点:

  1. 添加「清空所有任务」按钮,点击后清空数组并重新渲染;
  2. 给任务添加「编辑」功能,点击后可修改任务文本;
  3. 区分「已完成」和「未完成」任务,分栏显示;
  4. 添加任务优先级(高 / 中 / 低),不同优先级显示不同颜色;
  5. 优化样式,适配手机端(响应式布局)。

七、常见问题排查(新手避坑)

  1. 点击按钮没反应?→ 检查元素 ID 是否和 JS 中getElementById的参数一致;
  2. 新增任务后页面不显示?→ 检查是否调用了renderTodoList()函数;
  3. 刷新页面任务丢失?→ 检查是否调用了saveTodosToLocalStorage(),且JSON.stringify/JSON.parse没有写错;
  4. 删除任务没效果?→ 检查todoId的类型(Number / 字符串)是否和数组中todo.id一致。

总结

  1. 本次 TodoList 的核心是「数据驱动视图」:所有操作先修改数组(数据),再重新渲染页面(视图),这是前端开发的核心思想;
  2. 事件委托、本地存储、数组常用方法(push/filter/find/forEach)是本次的核心知识点,也是 JS 基础的高频考点;
  3. 新手学习的关键是「先跑通,再理解」:先复制代码看到效果,再逐行拆解逻辑,最后自己从头写一遍,遇到问题再回头看讲解。

这个 TodoList 核心 JS 代码不到 50 行,覆盖了新手入门必须掌握的知识点,吃透它,你的 JS 基础会扎实很多。