ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

原生JavaScript实现TodoMVC:夯实前端基础的关键实践

2026/8/10 14:04:49 拓冰建站 浏览量
原生JavaScript实现TodoMVC:夯实前端基础的关键实践

1. 为什么选择原生JavaScript实现TodoMVC

在框架盛行的今天,坚持用原生JavaScript实现TodoMVC看似复古,实则是一次绝佳的基础能力训练。我见过太多开发者能熟练使用Vue/React的API,却对事件委托、DOM操作优化这些底层原理一知半解。通过这个项目,你将掌握:

  • 纯手工管理DOM节点生命周期
  • 无虚拟DOM情况下的高效渲染策略
  • 事件系统的精确控制能力
  • 模块化组织代码的原始方法

提示:完成原生版本后再对比Vue实现,你会对现代框架解决的问题有更深刻的理解。我在团队面试中常让候选人做这个练习,能清晰看出其基础功底。

2. 项目结构与核心模块设计

2.1 基础HTML架构

采用经典TodoMVC的HTML结构,但去除所有框架痕迹。关键点在于:

<section class="todoapp"> <header class="header"> <h1>todos</h1> <input class="new-todo" autofocus> </header> <section class="main"> <ul class="todo-list"></ul> </section> <footer class="footer"></footer> </section>

特别注意:

  • 避免使用id选择器,保持CSS作用域纯净
  • 为动态元素预留空的容器(如todo-list)
  • 使用语义化class命名(不要用js-前缀)

2.2 状态管理方案

虽然可以用Redux模式,但为保持原生纯粹性,我推荐更轻量的发布订阅模式:

const store = (() => { let todos = [] const subscribers = [] return { getTodos: () => [...todos], addTodo: (text) => { todos.push({ id: Date.now(), text, completed: false }) subscribers.forEach(fn => fn()) }, subscribe: (fn) => { subscribers.push(fn) return () => subscribers.filter(sub => sub !== fn) } } })()

这种模式的优势在于:

  1. 避免直接暴露状态对象
  2. 提供可控的修改接口
  3. 实现响应式更新的基础机制

3. 核心功能实现详解

3.1 新增待办事项

在header的input上监听keyup事件:

document.querySelector('.new-todo').addEventListener('keyup', (e) => { if (e.key === 'Enter' && e.target.value.trim()) { store.addTodo(e.target.value.trim()) e.target.value = '' } })

常见陷阱:

  • 未处理连续空格输入(需trim())
  • 忘记清空输入框
  • 没考虑键盘兼容性(应同时监听keyCode)

3.2 列表渲染优化

采用文档片段(documentFragment)批量操作DOM:

const renderTodos = () => { const fragment = document.createDocumentFragment() store.getTodos().forEach(todo => { const li = document.createElement('li') li.innerHTML = ` <div class="view"> <input class="toggle" type="checkbox"> <label>${escapeHtml(todo.text)}</label> <button class="destroy"></button> </div> ` fragment.appendChild(li) }) const list = document.querySelector('.todo-list') list.innerHTML = '' list.appendChild(fragment) }

重要:必须实现escapeHtml防止XSS攻击,这是原生开发容易忽略的安全隐患。

3.3 状态切换与删除

使用事件委托提高性能:

document.querySelector('.todo-list').addEventListener('click', (e) => { const target = e.target const li = target.closest('li') const id = parseInt(li.dataset.id) if (target.matches('.toggle')) { store.toggleTodo(id) } else if (target.matches('.destroy')) { store.removeTodo(id) } })

委托要点:

  1. 只在父元素绑定一个监听器
  2. 用matches()精确判断事件源
  3. 通过closest()找到业务元素
  4. 使用data-*属性传递业务ID

4. Vue对比版本实现差异

4.1 模板语法对比

Vue版本可以简化为:

<template> <li v-for="todo in filteredTodos" :key="todo.id"> <div class="view"> <input class="toggle" type="checkbox" v-model="todo.completed"> <label>{{ todo.text }}</label> <button class="destroy" @click="removeTodo(todo.id)"></button> </div> </li> </template>

框架优势立即显现:

  • 自动XSS防护
  • 双向绑定简化状态同步
  • 声明式渲染无需手动DOM操作

4.2 状态管理对比

Vuex版本的核心逻辑:

const store = new Vuex.Store({ state: { todos: [] }, mutations: { addTodo(state, text) { state.todos.push({ id: Date.now(), text, completed: false }) } } })

与原生实现的本质区别:

  1. 响应式系统自动处理依赖追踪
  2. 不可变状态管理
  3. 完善的调试工具链

4.3 性能优化差异

原生需要手动实现的优化点,在Vue中通过:

  • 虚拟DOM的diff算法
  • 异步更新队列
  • 组件级更新策略

但原生版本在简单场景可能有更高性能,因为:

  • 无虚拟DOM开销
  • 精确控制DOM操作范围
  • 更少的内存占用

5. 项目进阶与调试技巧

5.1 本地存储持久化

实现localStorage同步:

const STORAGE_KEY = 'todos-vanillajs' const todoStorage = { fetch() { const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') todos.forEach((todo, index) => { todo.id = index }) return todos }, save(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)) } }

注意事项:

  • 需要处理JSON序列化异常
  • 考虑存储空间限制(约5MB)
  • 敏感数据需加密存储

5.2 性能监控方案

添加性能标记:

function update() { performance.mark('renderStart') renderTodos() performance.mark('renderEnd') performance.measure('render', 'renderStart', 'renderEnd') }

分析指标建议:

  • DOMContentLoaded时间
  • 平均渲染耗时
  • 内存占用变化

5.3 测试策略设计

基础测试方案:

describe('TodoList', () => { beforeEach(() => { document.body.innerHTML = __html__['index.html'] init() }) it('should add new todo', () => { const input = document.querySelector('.new-todo') input.value = 'Test todo' input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter' })) expect(document.querySelectorAll('.todo-list li').length).toBe(1) }) })

测试重点:

  1. 用户交互模拟
  2. DOM状态断言
  3. 边缘情况覆盖

6. 从项目中学到的经验

经过完整实现后,我总结出几点关键认知:

  1. 框架的价值:现代框架解决的不仅是语法糖问题,更是提供了可维护的工程化方案。当我在原生版本中手动实现shouldComponentUpdate等效逻辑时,深刻体会到React的优化价值。

  2. 基础的重量:能徒手实现完整TodoMVC的开发者,在排查框架问题时往往更快定位到根本原因。就像懂汇编的程序员对内存管理有更深理解。

  3. 技术选型平衡:对于简单页面,原生实现可能更轻量;但复杂应用必须用框架。我曾维护过一个2万行代码的原生项目,最终重构为Vue节省了40%代码量。

建议每个前端开发者都至少完成一次这样的练习,你会:

  • 真正理解虚拟DOM的价值
  • 掌握浏览器原生API的细节
  • 在框架使用中做出更明智的决策
  • 建立完整的前端知识体系