FEATURED · 精选文章

原生JavaScript实战:从零构建交互式TODO应用,掌握事件与DOM操作

发布时间 / 2026/9/9 23:25:48
来源 / 创域科博编辑部
栏目 / 资讯中心
原生JavaScript实战:从零构建交互式TODO应用,掌握事件与DOM操作 1. 一个“Day05”意味着什么从Day01到Day04我一直在跟一套前端基础训练营的节奏走。每天一个主题从HTML骨架到CSS布局再到现在到了Day05终于轮到JavaScript登场。这一天的题眼和之前完全不同前面的日子是在“画页面”从Day05开始才是真正在“做应用”。Day05是整个训练营学习曲线最陡的转折点之一。前四天你还可以靠复制粘贴和微调样式混过去但第五天开始逻辑开始介入交互开始产生代码不再是“写对了就能看见”的状态而变成了“写对了才能动起来”的状态。对那些目标明确、想从零转行前端的人来说Day05是第一个真正需要入门级工程思维的日子。这一天我给自己定了一个小目标不依赖任何框架用原生JavaScript把之前静态的页面改成可交互的TODO应用。这个目标不大但它恰好覆盖了JavaScript最常见的几块核心能力——事件监听、DOM操作、状态管理、渲染刷新。把这几样打好底子后面不管是学Vue还是React都会轻松很多因为你已经理解了它们底层替你做掉的那些事情。这篇文章就是Day05的完整实战记录。适合三类人看正在自学JavaScript、卡在“看得懂但不会写”阶段的人带新人或自己搞训练营需要一套可复制的作业设计的人以及想看看原生JavaScript到底能不能做出像样交互的怀疑论者。2. 整体设计与思路拆解2.1 为什么第五天必须上JavaScript先说结论Day05的任务必须满足一个标准——它得让学生第一次感受到“代码让页面活过来了”的成就感但又不能因为API太多而产生挫败感。前四天学的是HTML和CSS这两样东西的特点是“所见即所得”。你写一个div它在浏览器里就是一个div你加个颜色它立刻变颜色。这种即时反馈让新手觉得安全、有掌控感。但JavaScript完全不同它最大的门槛不是语法本身而是“代码和结果之间隔着一层”——你写一个函数页面不会立刻变化只有某个事件发生时它才执行。这个过程需要一种新的心智模型。Day05的设计就是要在第一时间建立这个心智模型。我从训练营的角度拆解过大部分失败的学习案例都有一个共同点太早引入复杂框架让学生在完全不懂DOM的情况下就开始操作虚拟DOM结果就是连“点击按钮为什么会弹窗”都要查半天。所以第五天我坚持用原生JavaScript讲原因很简单——框架都有自己的抽象层你只有理解了原生机制才能知道框架优化了什么、又帮你兜住了什么。这就像学车之前先学离合器原理一样日常驾驶可能用不上手动挡但你真遇到刹车失灵或半坡起步时懂得原理的人慌的时间至少短一半。2.2 选定的技术方案与取舍逻辑我最终确定的Day05技术方案非常克制甚至可以说简单到有些寒酸原生JavaScriptES6语法一个HTML文件一个CSS文件一个JS文件不引入任何第三方库这套方案在真实业务开发中当然是“非主流”的——没人会拿原生JS去写生产级应用。但作为教学场景和个人练手场景它的优势恰恰体现在“什么都没有”上。不引入jQuery是为了避免混淆DOM的原生API。不引入Vue是为了避免“数据驱动”的概念遮蔽掉“手动操作DOM”的底层认知。不引入打包工具是为了让整个项目的复杂度保持在“用浏览器直接打开index.html”就能运行的水平。有人可能会问为什么不直接上手框架我的答案是框架是效率工具不是学习工具。Day05这个阶段学习目标不是“产出多快的开发效率”而是“理解交互背后的每一步到底发生了什么”。用原生JS写一个TODO应用整个数据流是透明的你点击按钮浏览器触发事件你的函数执行手动更新DOM。这个链路里没有任何黑盒。一旦你亲手写过这套链路后面学任何框架都会觉得“哦它只是把我想手动做的事自动化了”而不是“它是个魔法”。2.3 TODO应用的功能范围界定确定了技术方案之后接下一个关键问题是做多大范围的功能我当时给Day05圈定的功能列表只有四项输入待办事项点击添加按钮后插入列表点击待办项文字可以切换完成状态划掉点击删除按钮移除对应待办项显示当前未完成事项的数量这个范围是刻意控制的。我没有做编辑功能因为编辑数据需要“双态切换”文本变成输入框、保存后再变回去这对第五天的学生来说信息量太大。我没有做拖拽排序因为HTML5拖拽API本身就是一个完整的独立课题。我也没有做本地存储因为localStorage虽然就一行代码但它会让学生误以为“数据本来就该自动保留”反而忽略了内存中状态管理的基本功。道理很简单第五天的核心训练目标是“事件 DOM操作 状态刷新”这条铁三角链路。功能每多一个注意力就会被分散一分。把范围压缩到四项反而能让每个功能都做出深度——事件怎么绑定、状态怎么管理、DOM怎么增量更新每一点都能掰开揉碎了讲明白。3. 核心细节解析与实操要点3.1 事件监听的三种写法与选型JavaScript里给按钮绑定事件至少有三种常规写法。很多人学的时候都只记住了一种但到了Day05这种集中练习的场景我建议三种都亲手写一遍因为它们的差异直接关系到你对事件模型的理解深度。第一种HTML内联写法。在button标签上直接写οnclickaddTodo()。这种写法最简单哪怕没学过JavaScript的人都能猜出意思。但它的致命缺陷是把行为与结构耦合在一起——一旦JS文件加载失败整个页面的交互全部失效而且调试时很难定位。第二种属性绑定写法。在JS里写btn.onclick function() {}。这比内联好一些行为与结构分离了但它有一个隐蔽的坑如果你给同一个元素的同一个事件绑定了两次后面那次会覆盖前面那次。第三种addEventListener写法。这是Day05的推荐方案。它可以给同一元素的同一事件注册多个回调还能在第三个参数里指定事件阶段捕获还是冒泡。虽然现在的TODO应用用不到这些高级功能但提前养成习惯后面处理复杂交互时会轻松很多。我在实战中遇到过一个很典型的问题用第一种写法时函数必须挂到window对象上才能被内联事件访问到。如果我用的是ES6的import/export语法模块作用域内的函数默认不会暴露到全局内联事件会直接报undefined。这个坑踩过一次之后就长记性了——从现在起统一用addEventListener不给自己留暗门。3.2 DOM操作中的性能与正确性问题DOM操作是Day05的重头戏也是新手最容易写出低效代码的地方。最常见的错误是在每次数据变化时直接把整个列表的innerHTML重新渲染一遍。比如function render() { todoListEl.innerHTML todos.map(item li${item.text}/li).join(); }这种方式在数据量小的时候完全没问题甚至看起来还挺优雅的。但一旦列表达到几百上千条每次重新拼接字符串并触发浏览器重排性能就会明显劣化。更重要的是这种做法会在交互场景中引发一个魔鬼细节——如果你在渲染前给某个输入框设置了焦点而在渲染时它所在的DOM被整体替换掉了焦点就会丢失光标会跳到页面顶部。所以Day05我选择了另一套思路用createElement动态创建节点而不是拼接innerHTML。这样每次只处理变化的那一项性能损失小而且可以按需插入或移除不影响其他节点。这个选择在TODO应用这种小场景下可能看不出差距但它逼着你建立起“只更新需要更新的部分”这一渲染意识这是后面学虚拟DOM时最重要的精神铺垫。还有一个细节容易被忽略创建完元素后一定要用appendChild或insertBefore把它挂到DOM树上——光createElement不挂载元素就是个孤儿节点不会出现在页面上。这个问题我几乎每年都能在一两个学员身上看到控制台打印出来明明有对象页面上死活不显示最终发现就是忘了appendChild。3.3 状态管理的核心数据与视图分离Day05最容易讲歪的一个点是让学生把数据直接写在DOM里——从一个input里读值更新li的textContent就完事了。这样确实能跑但它没法回答一个更基础的问题如果两个地方需要显示同一个数据该怎么办所以我在设计这个TODO应用时用了一个非常专业但容易理解的结构维护一个todos数组作为唯一数据源DOM只是这个数组的可视化投影。数组变化了就先更新数组再根据数组重新渲染局部视图。复制代码let todos []; function addTodo(text) { todos.push({ id: Date.now(), text: text, done: false }); render(); } function toggleTodo(id) { const todo todos.find(item item.id id); if (todo) { todo.done !todo.done; render(); } }这样设计的好处是显而易见的无论你从哪个入口修改数据点击按钮、键盘回车、甚至未来加入的本地存储最终都会收敛到统一的render逻辑。视图永远不会出现“我改了这里但那边没同步”的脏状态。有人可能会说这不就是手动版的数据驱动吗对这就是。你现在手动封装一遍这个模式以后学Vue的响应式数据、学React的useState都会发现它们做的事情本质上是一样的——让你声明“数据变了UI要跟着变”只是框架帮你自动化了这个过程还加上了各种优化策略。所以千万不要觉得Day05这套状态管理写得土它是所有现代前端框架的思想源头。3.4 渲染函数的设计与增量更新渲染函数是整个TODO应用的心脏。我在Day05的具体实现中把渲染拆成了两个层次第一个层次是初始化渲染也就是页面加载时把todos数组里的所有项一次性画出来。这个用了一个简单的循环对每个todo创建li节点、文本节点、删除按钮依次挂载到ul下。第二个层次是增量渲染只针对发生变化的单个待办项做局部更新。比如切换完成状态时我只需要找到当前列表项里的文字节点给它加一个line-through的样式类而不需要重新渲染整个列表。删除待办项时也只需要调用removeChild把对应的li从ul里移除其他项完全不受影响。这种设计让整个应用的操作反馈非常即时即使数据到了一千条也不会有明显的卡顿感。更重要的是它让“DOM是什么”这个问题变得具象化了——DOM不是抽象概念它就是你页面上的树状结构你做的各种DOM操作本质上就是对这个树的增删改查。这里还有一个小技巧给每个todo分配一个唯一id我用的是Date.now()虽然严谨来说不是绝对唯一但单页面场景足够用并在li上设置data-id属性。这样点击删除按钮时我不需要去猜是哪一项直接通过dataset.id就能拿到对应的标识然后从todos数组里筛掉它。这个技巧在真实项目中也极其常用几乎所有列表型交互都需要这个id桥梁。4. 实操过程与核心环节实现4.1 项目结构与文件准备实操的第一步我把整个项目放在一个独立的文件夹里避免和之前几天的Demo混在一起。我在本地建了一个名为day05-todo的目录里面只放了三个文件index.html负责页面结构style.css负责样式呈现app.js负责所有交互逻辑这三层分离是Web开发的基本功。虽然Day05的应用很小把所有代码塞进一个HTML文件里也能跑但从第一天开始就养成文件分离的习惯后面维护复杂项目时不会手忙脚乱。这也是我反复跟学员强调的项目初期多花十秒钟建文件项目后期能省十分钟翻代码。我还顺手用VS Code装了Live Server插件用本地服务的方式打开页面。这里有个新手经常踩的坑直接在浏览器里双击index.html用file://协议打开大多数情况下没问题但一旦你后面用了ES Module语法import import浏览器会直接报跨域错误。所以Day05就开始用Live Server是成本最低、收益最明显的习惯养成方案。4.2 HTML骨架与静态页搭建index.html这一层我没有做太复杂的结构就是纯语义化的页面骨架!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleDay05 - TODO清单/title link relstylesheet hrefstyle.css /head body div classapp h1我的待办清单/h1 div classinput-area input typetext idtodo-input placeholder输入待办事项按下回车或点击添加 button idadd-btn添 加/button /div ul idtodo-list/ul div classsummary 还有 span idpending-count0/span 项待完成 /div /div script srcapp.js defer/script /body /html这个结构里有两个细节值得讲一下。第一script标签放在了body结尾并且加了defer属性。defer的意思是等HTML解析完了再执行JS这样可以保证JS代码里document.querySelector能找到元素。即使脚本放在head里加defer也可以达到同样的效果。第二input和button外面包了一层.input-area这个不是为了好看而是后面样式设flex布局时方便——让输入框自动撑满剩余宽度按钮保持固定宽度。4.3 CSS样式与视觉反馈设计CSS这层我花了大概二十分钟。核心目的是做一个干净整洁但不过度设计的界面把视觉上的注意力留给交互反馈。我用了flex布局的垂直结构把整个app容器居中设置了最大宽度为480像素这个宽度在手机上全屏显示、在桌面上也不会显得太宽。输入区域用了flex水平布局输入框自适应宽度按钮固定在右侧。待办事项的列表项也用了flex让文字占据剩余空间删除按钮固定在右侧。视觉反馈方面我重点设计了两个状态第一是hover状态待办项的背景色在鼠标悬停时会轻微变灰给人一种“这一行可以交互”的暗示。第二是完成状态done类的元素的文字变为灰色并加上中划线透明度也会降低让人一眼扫过去就知道哪些事项已经完成。删除按钮默认是一个不太显眼的叉号鼠标悬停时变成红色提示它是一种破坏性操作。还有一个细节我单独为空的输入框做了校验样式——如果用户什么都没输入就点了添加输入框的边框会短暂变红并弹出一个提示文字。这个功能虽然只有几行代码但它让整个应用的使用体验立刻从“能跑”提升到了“能用”的级别。4.4 JavaScript逻辑的完整实现下面是Day05最核心的部分。我先把完整的app.js放出来然后一段一段拆开讲const inputEl document.getElementById(todo-input); const addBtn document.getElementById(add-btn); const todoListEl document.getElementById(todo-list); const pendingCountEl document.getElementById(pending-count); let todos []; // 渲染未完成数量 function updateSummary() { const pending todos.filter(item !item.done).length; pendingCountEl.textContent pending; } // 创建单个待办项的DOM节点 function createTodoItem(todo) { const li document.createElement(li); li.className todo-item; if (todo.done) li.classList.add(done); li.dataset.id todo.id; const textSpan document.createElement(span); textSpan.className todo-text; textSpan.textContent todo.text; textSpan.addEventListener(click, function () { toggleTodo(todo.id); }); const delBtn document.createElement(button); delBtn.className del-btn; delBtn.textContent ✕; delBtn.addEventListener(click, function () { deleteTodo(todo.id); }); li.appendChild(textSpan); li.appendChild(delBtn); return li; } // 渲染整个列表 function render() { todoListEl.innerHTML ; todos.forEach(todo { const li createTodoItem(todo); todoListEl.appendChild(li); }); updateSummary(); } // 添加待办 function addTodo() { const text inputEl.value.trim(); if (!text) { inputEl.classList.add(input-error); setTimeout(() inputEl.classList.remove(input-error), 600); return; } todos.push({ id: Date.now(), text: text, done: false }); inputEl.value ; inputEl.focus(); render(); } // 切换完成状态 function toggleTodo(id) { const todo todos.find(item item.id id); if (todo) { todo.done !todo.done; render(); } } // 删除待办 function deleteTodo(id) { todos todos.filter(item item.id ! id); render(); } // 事件绑定 addBtn.addEventListener(click, addTodo); inputEl.addEventListener(keydown, function (event) { if (event.key Enter) { addTodo(); } }); // 初始渲染 render();这段代码的每一块都值得展开说。addTodo函数里先用trim把输入两端的空格去掉防止用户不小心多打几个空格贡献一个全是空格的脏数据。空值校验通过后用Date.now()生成一个毫秒级时间戳作为唯一id然后push到todos数组里。添加完成后把输入框清空并重新focus——这个细节很关键用户添加完一条之后可以马上继续输入下一条整个流程不需要任何鼠标点击。createTodoItem函数是创建单个节点的地方。这里我把文字Span和删除按钮的点击事件分别绑在了两个不同的元素上——点击文字触发切换完成状态点击按钮触发删除。如果不这样分开而是把整个li绑一个click那点击删除按钮时会同时触发冒泡上的toggleTodo导致删除后又立刻变成完成态出现一个非常诡异的bug。这个冒泡问题在第4.5节我会专门讲。render函数负责全量渲染。我用了最直接的方式先把todoListEl的innerHTML清空然后遍历todos数组逐个创建li并挂载。这种方式在小规模场景下简单可靠不容易出脏状态。前面第3.2节我提过它不是性能最优解但对于Day05的学习目标它的正确性优先级远大于性能优化。等你理解了整条链路之后再去优化增量更新才有意义。toggleTodo和deleteTodo的逻辑几乎一模一样先根据id在数组里找到目标对象做出修改然后调用render重新刷新页面。这就是最原始的数据驱动模式——状态变了视图跟着变。事件绑定部分我做了两件事。第一件是点击按钮添加绑定click。第二件是在输入框上绑keydown监听回车键。这里有个常见误区新手喜欢在window上绑keydown然后判断event.key但如果页面里还有其他输入框回车键的意图就会发生冲突。所以我建议把回车监听绑定在输入框自身语义更清晰也彻底规避误触问题。4.5 事件冒泡与this指向的实战坑写TODO应用的过程中事件机制涉及的几个坑必须单独拿出来讲因为它们是Day05多数人会卡住的地方。第一个坑是事件冒泡。默认情况下事件在DOM树中会从目标元素向上传播到祖先元素这叫冒泡阶段。比如你点击删除按钮点击事件会依次在按钮、li、ul、body上触发。如果li本身也绑定了click那么点击删除按钮时删除逻辑执行完紧接着li上的click也会被触发。我在上面的实现里把删除按钮的click事件和文字Span的click事件分开绑定并且在删除按钮的click回调里没有调用event.stopPropagation()但为什么不会触发li上的click因为我在li上根本没有绑click事件——我把切换状态的监听绑在了文字Span上。如果你非要把click绑在li上那就要在删除按钮的回调里加上event.stopPropagation()。这个方法的作用是阻止事件继续冒泡传播。如果你不阻止删除完事项后点击事件冒泡到lili又触发toggleTodo最终结果是——这条待办被删掉了但删除前它的完成状态已经被切换了一次。这个bug非常恶心因为它的表现不是报错而是逻辑顺序错乱新手排查起来往往一头雾水。第二个坑是函数里的this指向。在箭头函数出现之前回调函数里的this指向的是调用它的上下文而不是外层对象。我在Day05故意用function() {}而不是箭头函数是为了让学生看一眼如果我在createTodoItem里用this它指向的是当前正在处理的DOM元素如果我误用this去访问todos数组会直接得到一个undefined。箭头函数没有自己的this它继承外层作用域的this所以在现代JavaScript里大部分回调场景推荐用箭头函数来避免this指向混乱。但理解function里this的取值规则仍然很重要——因为你迟早会遇到老代码、遇到别人写的function如果不懂this的规则那Python return undefined的报错都算温和的最怕的是this值错得不动声色逻辑全乱。4.6 键盘输入与表单提交的细节处理Day05这个TODO应用的输入方式有两种点击按钮添加和回车添加。用回车添加时有一个极其常见的新手问题按回车后页面整个刷新了。这个问题通常发生在你忘记给input设置type类型或者form表单位于某个form标签内部时。如果你把input和button包在一个form里回车默认触发表单提交行为浏览器会重新加载页面。解决的方案有两种要么form的submit事件里调用event.preventDefault()阻止默认提交要么干脆不用form标签用div包裹从根上避免这个行为。我在Day05的实现中选择了div包裹这是新手最不容易出错的方式。但在真实项目中用form的好处在于语义化和无障碍支持所以具体怎么选要依据场景。不管怎么选你自己心里得清楚回车触发提交这个机制是什么而不是遇到问题就林子大了什么鸟都有地瞎猜。如果你将来写React的表单不理解preventDefault的用途回车刷新页面这个问题还是会卷土重来。另外还有一个交互细节输入框的placeholder文字我写的是“输入待办事项按下回车或点击添加”。这行字看似无关紧要但它极大地降低了用户的学习成本。一个页面如果只有输入框和按钮用户看到后大概率是要猜的。写清楚交互方式用户就不用猜了。这个“降低猜测成本”的意识是从产品角度给技术人的一个提醒——代码不仅要自己写得爽还要给使用者留一条清晰的路。4.7 本地存储的扩展实验Day05的主线功能已经全部实现了。这时候如果你想再push自己一把我建议尝试一个扩展实验——用localStorage给TODO应用加数据持久化能力。具体做法只有两步。第一步在初始加载时从localStorage里读取之前存下来的数据const saved localStorage.getItem(day05-todos); if (saved) { todos JSON.parse(saved); }第二步在每次render时把最新的todos数组序列化后存回去function render() { // 原有渲染逻辑 localStorage.setItem(day05-todos, JSON.stringify(todos)); }这套做法的核心概念是“序列化”。因为localStorage只能存字符串而todos是一个对象数组所以存储前需要JSON.stringify把它变成字符串读取时再通过JSON.parse把它解析回数组。这两个API都是JavaScript的JSON标准内置方法属于必会级别。这个扩展实验能让学员立刻感受到一个质的飞跃刷新页面后待办事项还在。这个体验对学习动力的提升效果远远大于技术本身——因为它让人觉得“我确实做出了个东西”。不过要提醒一点localStorage不是无限容量的各大浏览器通常限制在5MB左右所以它适合存轻量级数据不适合存图片或大文本。对TODO应用来说百来条数据完全没压力。5. 常见问题与排查技巧实录5.1 按回车页面刷新问题描述在输入框里输入内容后按回车不是添加待办而是整个页面闪烁了一下内容全部清空。排查思路这几乎可以断定是表单提交导致的。打开浏览器开发者工具看地址栏是否出现了问号参数如果有说明表单以GET方式提交了页面自然被刷新。解决方案第一选择是检查HTML结构确认你用的不是form包裹或者如果是form就给submit事件加event.preventDefault()。第二选择是检查keydown监听里是否漏了switch虽然Day05这个场景回车只有添加一个用途但养成return的判断习惯对以后处理组合按键有帮助。5.2 点击删除后整个项消失了但待办数量变了问题描述点击某条待办的删除按钮发现不仅这条项没了其他项也被清空了但未完成数量显示正确。排查思路这是典型的“全量重渲染”副作用。大概率你删除了todos数组里的某一项然后调用renderrender里第一行是innerHTML 把所有项都清掉了下一行再遍历todos重新创建。按理说这应该是正确的但如果其他项都不见了说明在遍历时抛出异常中断了渲染流程。打开控制台看有没有报错。最常见的报错是某个变量为null或undefined比如你在createTodoItem里试图读取todo.text但某条todo数据没有text字段。或者你误把filter写成了splice删除了从某个位置开始的所有元素。解决办法先用console.log把todos打印出来确认数组本身是正常的。数据没问题就看DOM操作如果全量渲染时报错排查到具体报错行修掉就好了。5.3 点击删除同时触发了切换状态问题描述删除一条待办它先被划掉了然后才被删除。排查思路这是事件冒泡的典型特征。你在li上绑定了toggleTodo点击事件删除按钮的click事件触发后事件冒泡到li又触发了toggleTodo。执行顺序取决于代码绑定顺序表现就是我刚才说的先划掉再删除。解决方案在删除按钮的回调里调用event.stopPropagation()或者把toggleTodo的监听从li移动到内部的文字Span上我在4.4节的写法就是这个思路。两种方式都能解决但后一种更符合语义——整个li的面积比文字Span大很多如果用户不小心点到li的空白区域比如右边空白处也会莫名其妙切换状态移动监听会显著降低误触概率。5.4 添加按钮点击无效控制台报Cannot read property addEventListener of null问题描述页面打开后任何按钮点击都没有反应控制台报错Cannot read property addEventListener of null。排查思路这代表你在JS里通过document.getElementById获取某个元素时返回了null。最常见的原因是script标签放在head里且没有加defer导致JS执行时HTML还没有解析到目标元素body里的DOM节点还不存在。解决方案把script标签移到body结束标签之前或者加上defer属性。这是我反复强调过的HTML加载顺序问题。如果你用的是外部引入的JS文件一定要确保DOM ready之后再去查询元素。5.5 输入空格也能添加到列表问题描述输入框只敲了几个空格点添加列表里出现一条空白的待办。排查思路input.value拿到的值包含空格你没有做trim处理。虽然页面看起来是空的但在JavaScript里空格也是字符所以todos数组里就多了一条text为空格的数据渲染出来视觉上就是一条空白项。解决方案在addTodo里调用text.trim()再检查trim之后的结果是否为空字符串。这是各种输入校验中最基础的一环尤其适合在这里练成肌肉记忆。以后凡是用户输入的内容落地前先trim一遍能省掉无数脏数据问题。5.6 数据量很大时页面卡顿问题描述手工测试了一两百条待办后发现每次添加或删除都有肉眼可见的卡顿。排查思路这里卡顿的根源就在全量重渲染。每次添加一条render都会清空整个ul然后重新创建所有li节点浏览器需要反复重排重绘。数据少时无感数据一多就暴露。解决方案可以升级为增量渲染。添加时用createTodoItem创建新li直接appendChild到ul末尾删除时通过document.querySelector找到data-id对应的li直接removeChild。这样每次DOM操作只涉及一条数据性能几乎没有开销。当然Day05阶段的小项目数据量一般不会超过几百条全量重渲染已经是可用的但了解这个优化方向对后续学习有好处。6. 一些我在实战中沉淀的体会DAY05这个看似简单的TODO应用整套写下来其实用了我整整一个上午。更准确地说不是写代码用了一个上午而是“一边写一边把每个API、每个行为都搞明白”用了一个上午。从开始写HTML骨架到最终在Live Server里刷新页面看到所有交互都正常那种满足感和第一次学会骑自行车差不多——动作很小但你明知道从这一刻起你的世界变大了。我还记得自己第一次踩到事件冒泡这个坑时的心情我盯着代码看了二十分钟没看出任何毛病直到在chrome devtools的Event Listener面板里看到li上首绑了个click才恍然大悟。从那之后我养成了一个习惯——凡是遇到“逻辑不对但代码看着没毛病”的诡异bug第一个怀疑对象就是事件传播。这个习惯后来帮我排掉了不少真正的生产问题。Day05这个项目我强烈建议你别只是复制代码看懂就算完而是动手敲一遍之后再主动做三件拓展的事第一把字体变大变小的“设置”功能加上第二尝试把待办事项按完成状态进行筛选全部/未完成/已完成三个标签第三把整个应用改成支持多列表的场景。每多做一步你对DOM和数据驱动的理解都会再深一层。这次实战也让我重新确认了一个观点教程和项目之间最大的差距不是知识量而是“出了bug你会不会慌”。Day05把所有常见的坑都集中到一个火柴盒大小的项目里让你踩一遍踩得越疼记得越牢。这也正是我把这些坑记录下来的原因——希望下一个做Day05的人能少一点疑惑多一点从容。最后再分享一个小技巧如果你以后想让这个TODO应用更完善可以从“数据校验”和“无障碍”两个方向入手。数据校验指的是在添加之前就判断输入内容的长度、格式是否合理无障碍指的是让键盘操作、屏幕阅读器也能正常使用这个应用。这两个方向可能在Day05阶段还显得有些远但它们是每一个“从能跑到好用”的应用必须跨越的门槛。先在这个小项目上建立意识将来遇到大规模复杂交互时你会感谢今天认真走过的这一步。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻