1. 项目概述:为什么选择原生JavaScript实现TodoMVC?
在当今前端框架盛行的时代,很多初学者会直接跳入Vue或React的学习,却忽略了最基础的JavaScript DOM操作能力。这个项目正是为了解决这个问题而生——通过纯原生JavaScript实现完整的TodoMVC应用,帮助开发者建立扎实的基础。
我曾面试过上百位前端开发者,其中80%的候选人能熟练使用Vue/React,但当被要求用原生JS实现一个简单的TODO功能时,超过半数的人会卡在事件委托或数据持久化这样的基础问题上。
1.1 核心功能设计
这个TodoMVC应用包含三个特色模块:
- 基础待办事项:完整的CRUD操作+状态筛选
- 倒数日功能:动态计算日期差值+状态分类
- 学习路线图:可视化时间轴+里程碑状态管理
javascript复制// 典型数据结构设计
{
id: Date.now(), // 唯一标识
text: 'Learn JavaScript', // 内容文本
completed: false, // 完成状态
dueDate: '2025-03-15', // 截止日期(倒数日功能)
type: 'milestone' // 类型标识(路线图功能)
}
1.2 技术选型考量
为什么坚持使用原生实现?
- 框架无关性:理解底层原理后,学习任何框架都会事半功倍
- 性能优势:小型项目中,原生方案通常比框架更轻量
- 面试必备:大厂面试常考原生DOM操作和事件机制
- 调试能力:迫使开发者深入理解浏览器工作原理
2. 核心实现解析
2.1 DOM操作最佳实践
2.1.1 高效元素选择
避免频繁查询DOM:
javascript复制// 一次性获取所有需要操作的元素
const els = {
input: document.getElementById('todo-input'),
list: document.getElementById('todo-list'),
counter: document.querySelector('.todo-count')
};
// 后续直接使用els.input等引用
2.1.2 事件委托模式
处理动态列表的黄金方案:
javascript复制document.getElementById('todo-list').addEventListener('click', e => {
const item = e.target.closest('.todo-item');
if (!item) return;
const id = parseInt(item.dataset.id);
if (e.target.classList.contains('delete-btn')) {
deleteTodo(id);
} else if (e.target.classList.contains('toggle')) {
toggleComplete(id);
}
});
2.2 数据状态管理
2.2.1 最小化状态设计
javascript复制let state = {
todos: [],
filter: 'all', // 'all'|'active'|'completed'
nextId: 1
};
2.2.2 持久化方案
localStorage的健壮性处理:
javascript复制function saveState() {
try {
localStorage.setItem('todoState', JSON.stringify(state));
} catch (e) {
console.error('存储失败,可能是存储空间已满', e);
}
}
function loadState() {
const saved = localStorage.getItem('todoState');
if (saved) {
try {
state = JSON.parse(saved);
// 数据迁移检查
state.todos.forEach(todo => {
if (!todo.hasOwnProperty('createdAt')) {
todo.createdAt = new Date().toISOString();
}
});
} catch (e) {
console.error('解析存储数据失败', e);
}
}
}
3. 进阶功能实现
3.1 倒数日计算
精确到天的日期差值计算:
javascript复制function getDayDiff(targetDate) {
const ONE_DAY_MS = 86400000; // 24*60*60*1000
const today = new Date();
today.setHours(0, 0, 0, 0);
const target = new Date(targetDate);
target.setHours(0, 0, 0, 0);
// 处理时区差异
const timezoneOffset = target.getTimezoneOffset() * 60000;
const normalizedTarget = new Date(target.getTime() + timezoneOffset);
return Math.round((normalizedTarget - today) / ONE_DAY_MS);
}
3.2 时间轴渲染
动态定位算法:
javascript复制function renderTimeline() {
const timeline = document.querySelector('.timeline');
const events = state.todos.filter(t => t.type === 'milestone');
if (events.length === 0) {
timeline.innerHTML = '<div class="empty-tip">暂无里程碑</div>';
return;
}
// 计算日期范围
const dates = events.map(e => new Date(e.dueDate).getTime());
const minDate = Math.min(...dates);
const maxDate = Math.max(...dates);
const range = maxDate - minDate || 1; // 避免除以0
// 渲染节点
timeline.innerHTML = events.map(event => {
const position = ((new Date(event.dueDate) - minDate) / range) * 100;
return `
<div class="milestone" style="left: ${position}%"
data-status="${event.completed ? 'done' : 'pending'}">
<div class="tooltip">${event.text}</div>
</div>
`;
}).join('');
}
4. 常见问题与解决方案
4.1 性能优化要点
问题1:列表渲染导致页面卡顿
- 解决方案:使用文档片段(documentFragment)批量操作
javascript复制const fragment = document.createDocumentFragment();
todos.forEach(todo => {
const li = createTodoElement(todo);
fragment.appendChild(li);
});
listEl.innerHTML = '';
listEl.appendChild(fragment);
问题2:频繁触发localStorage写入
- 解决方案:添加防抖机制
javascript复制let saveTimer;
function scheduleSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(saveState, 500);
}
4.2 安全防护措施
XSS防护:
javascript复制function sanitize(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
}
数据校验:
javascript复制function validateTodo(todo) {
return typeof todo.text === 'string' &&
todo.text.trim().length > 0 &&
!isNaN(new Date(todo.dueDate).getTime());
}
5. 从原生到框架的思维转变
当完成原生实现后,对比Vue版本会明显体会到:
-
声明式 vs 命令式:
- 原生:需要手动
appendChild和removeChild - Vue:通过
v-for自动维护DOM
- 原生:需要手动
-
状态管理:
- 原生:需要手动触发
render() - Vue:响应式数据自动更新视图
- 原生:需要手动触发
-
代码组织:
- 原生:通常集中在单个文件
- Vue:组件化拆分更清晰
javascript复制// Vue版本的相同功能对比
<template>
<div class="todo-app">
<input v-model="newTodo" @keyup.enter="addTodo">
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.completed">
<span :class="{ completed: todo.completed }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">×</button>
</li>
</ul>
</div>
</template>
6. 项目扩展建议
想要进一步提升?可以尝试:
-
添加撤销/重做功能:
- 使用Command模式维护操作历史
- 最大历史记录限制(避免内存溢出)
-
实现服务端同步:
- 添加IndexedDB支持离线模式
- 使用Fetch API与后端交互
-
性能监控:
javascript复制function measurePerf() { const start = performance.now(); renderTodos(); console.log(`渲染耗时:${(performance.now() - start).toFixed(2)}ms`); } -
单元测试:
javascript复制// 使用Jest示例 test('should calculate day difference correctly', () => { const mockDate = new Date(); mockDate.setDate(mockDate.getDate() + 3); expect(getDayDiff(mockDate.toISOString())).toBe(3); });
这个项目最宝贵的不是最终的代码,而是在实现过程中培养的解决问题的思维。当你在原生JS中手动处理过状态更新、DOM操作和事件管理后,才能真正理解现代框架的价值所在。