原生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) } } })()这种模式的优势在于:
- 避免直接暴露状态对象
- 提供可控的修改接口
- 实现响应式更新的基础机制
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) } })委托要点:
- 只在父元素绑定一个监听器
- 用matches()精确判断事件源
- 通过closest()找到业务元素
- 使用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 }) } } })与原生实现的本质区别:
- 响应式系统自动处理依赖追踪
- 不可变状态管理
- 完善的调试工具链
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) }) })测试重点:
- 用户交互模拟
- DOM状态断言
- 边缘情况覆盖
6. 从项目中学到的经验
经过完整实现后,我总结出几点关键认知:
框架的价值:现代框架解决的不仅是语法糖问题,更是提供了可维护的工程化方案。当我在原生版本中手动实现shouldComponentUpdate等效逻辑时,深刻体会到React的优化价值。
基础的重量:能徒手实现完整TodoMVC的开发者,在排查框架问题时往往更快定位到根本原因。就像懂汇编的程序员对内存管理有更深理解。
技术选型平衡:对于简单页面,原生实现可能更轻量;但复杂应用必须用框架。我曾维护过一个2万行代码的原生项目,最终重构为Vue节省了40%代码量。
建议每个前端开发者都至少完成一次这样的练习,你会:
- 真正理解虚拟DOM的价值
- 掌握浏览器原生API的细节
- 在框架使用中做出更明智的决策
- 建立完整的前端知识体系