尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
DOM增删改与节点遍历:从写死数据到动态刷新的实践之路
从写死数据到动态刷新Web APIs Day3的DOM增删改与遍历实践我印象特别深的一次经历是做某个后台管理系统的列表页时产品经理过来说“这个表格的数据不要写死要能动态增删行。”当时我第一反应是“这有什么难的”结果真上手才发现把一个按钮的点击事件、DOM节点的创建、插入位置、以及后续数据的更新串起来远没有想象中那么简单。也就是从那天起我意识到Web APIs里关于DOM操作的内容绝对是非凡不可的硬功夫。今天这篇就围绕Web APIs学习的第三天来聊。Day3的核心内容大致是元素创建、插入、替换、删除以及节点遍历与集合操作。如果前两天的内容解决的是“怎么把页面上的元素找出来”、“怎么给元素绑定事件”那第三天解决的则是“怎么让页面上的内容真正动起来、变起来”。这篇适合所有刚开始系统学习前端、或者做了几个月页面但一直靠复制粘贴操作DOM的同学。我会把原理、步骤、坑和实际项目里的用法都过一遍尽量让读完的人能直接上手。1. 从“调接口拿数据”到“把数据渲染上页面”Day3到底在解决什么问题1.1 一个最典型的场景动态列表的刷新很多初学者有这样一个动作拿到后端返回的数组直接在JS里用document.write往页面里塞内容或者用innerHTML拼接一大段字符串。结果页面是出来了但只要数据一变整个页面的内容就要重新生成一遍用户滑到一半的滚动位置、输入框里的内容、选中的状态全都丢了。真正做项目的时候我们需要的通常是这种能力后端返回了10条数据我把它渲染成一个有序列表。过一会儿数据更新成12条我只把变化的那部分列表项加进去或删掉而不是把整个列表推倒重来。这种“精准修改”的能力就是Day3的核心——对DOM节点做增、删、改、查。举个例子你有一个待办清单ul idtodoList li写周报/li li修BUG/li /ul现在用户提交了一条新待办“预约会议室”前端要做的事是创建一个新的li节点把文字内容填进去再把这个节点追加到ul里。在这个过程中createElement、textContent、appendChild这三个API就会全部用上。而如果用户勾选了某个待办要把它删掉那就需要removeChild或者remove方法。1.2 Day3是前面两天的“集结点”如果把Web APIs的学习拆成几天来看前期你在学习怎么通过querySelector、getElementById之类的选择器找到DOM节点紧接着又要学习怎么通过addEventListener给节点绑定交互事件而Day3的重点则是深入操作DOM本身的结构。这三块串联起来之后你才算真正具备了“前端交互开发”的完整链路事件触发 → 找到目标节点 → 修改结构。这个能力是后面所有框架学习的基础。你可能听说过Vue或者React有虚拟DOM、有diff算法它们做的事情本质上也是“创建节点、比较节点、更新节点”。如果你连原生的节点操作都不够熟悉直接去学框架的diff逻辑会非常吃力。相反把原生API吃透了再去看框架的源码或者设计思路你会觉得很多理念是可以一一对应的。2. 元素的创建、插入与删除一套完整的增删改操作逻辑2.1 创建节点不是只有createElement一种方式很多初学者一上来就先学document.createElement()这当然是标准做法但我建议你先意识到一点创建节点其实有几种完全不同的思路各自有各自的适用场景。第一种是createElement配合appendChild或者insertBefore这种方式是最“精细”的。你可以先创建一个li再创建一个span把span放进li里再把li放进ul里。每一步都清清楚楚适合节点结构复杂、需要单独绑定事件或者设置属性的场景。第二种是insertAdjacentHTML。这个方法是我个人在项目里非常高频使用的它接收两个参数第一个是插入位置第二个是HTML字符串。位置有四个可选值beforebegin、afterbegin、beforeend、afterend分别代表元素自身前后和内部首尾。比如ul.insertAdjacentHTML(beforeend, li预约会议室/li)一行代码就能完成创建和插入两个动作。第三种是innerHTML。这个方法适合整块替换内容但用在局部增删上要格外小心。你给一个div重新赋值innerHTML相当于把它内部所有的旧节点先清空再根据字符串重新解析创建一批新节点。在列表频繁变更的场景下它会造成不必要的性能损耗而且会把事件绑定一并丢掉。为什么很多新人不理解“为什么我绑定的事件失效了”十有八九就是因为在绑定完事件之后又用innerHTML重绘了那个容器的内容。三种方式我用一个简单的对比如下方法适用场景需要注意的点createElement结构复杂、需要精确控制、要绑定事件代码量稍大步骤多insertAdjacentHTML插入一段确定的HTML字符串注意字符串拼接的转义问题innerHTML整块替换容器内容会丢失容器内已有节点的事件绑定2.2 插入位置的精确控制insertBefore与appendChild的关系新手经常忽略一个点appendChild只能把节点追加到父元素的末尾但实际页面里我们要插入的位置往往不是末尾而是“某个已有元素的前面”。这个时候就需要insertBefore了。insertBefore(newNode, referenceNode)接收两个参数第一个是新节点第二个是参照节点。如果参照节点传null它的效果就跟appendChild一样插到末尾。很多项目里的排序功能本质上就是先把节点从当前位置移除然后通过insertBefore插到目标位置前面。我这里有一段实际写排序功能时用过的思路// 假设sortableList是一个ulitem是要移动的litargetItem是目标位置的li sortableList.removeChild(item); sortableList.insertBefore(item, targetItem);这个操作有个很明显的特征removeChild和insertBefore配合使用的时候被移动的节点不需要重新创建。因为节点本身还是同一个DOM节点只是换了个位置。这里有一个特别容易被忽略的细节——同一个节点被插入到新位置时它会自动从原来的位置消失。也就是说你不必先手动把它从旧位置删除再插入新位置直接insertBefore到新位置就可以了。我当时第一次做拖拽排序时就是栽在这个细节上删了一遍又插了一遍结果节点直接消失了。后来才明白节点的appendChild和insertBefore操作本身就带有“移动”语义。2.3 删除节点removeChild和remove到底用哪个在比较老的教学资料里删除节点一般是先找到父节点再通过parentNode.removeChild(childNode)来删。但现代浏览器里节点自身也有一个remove()方法可以直接把自己从DOM树中删掉。这两种方式有什么区别removeChild需要你先取得父节点再进行删除最后会返回被删除的节点意味着你还可以在删除后把它重新插入到其他地方实现“移动”操作。remove()则更直接没有返回值调用之后节点就销毁了。就日常做项目来说我更推荐优先使用remove()代码更简洁可读性也好。但如果你需要保留被删除节点的引用稍后要复用那就用removeChild因为它把节点返回给你了。还有一个新手很容易忽略的细节remove()虽然把节点从DOM树里移除了但这个节点上绑定的事件监听器并不会因此自动解除。如果页面里有很多被反复创建和删除的节点并且每个节点都绑定过事件那么时间一长内存里就可能堆积大量失去引用的监听器造成内存泄漏。现代V8引擎的垃圾回收机制通常能处理一部分情况但在极端场景下还是建议在删除节点前手动removeEventListener或者把事件监听统一挂在容器上用事件委托的方式集中处理。关于事件委托后面我会详细讲。3. 节点集合与遍历HTMLCollection和NodeList都是“类数组”3.1 children与childNodes的核心差异当我们通过document.querySelectorAll获取一组节点时得到的是一个NodeList通过element.children获取子元素时得到的则是一个HTMLCollection。这两个集合类型很容易混因为它们都是“类数组”都可以用length属性、可以用下标访问也都支持forEach现代浏览器中HTMLCollection也支持了但在某些旧环境里可能不支持。但它们有一个本质区别必须记住HTMLCollection是动态的liveNodeList通常是静态的。这个“动态”意味着当DOM结构发生变化时HTMLCollection的成员会自动更新而你不需要重新获取。反过来querySelectorAll得到的NodeList是当你调用它那一瞬间的快照之后DOM怎么变它都不会跟着变。我直接给你一个平常编码中出现过的对比示例const div document.getElementById(container); const children div.children; // HTMLCollection动态 const nodeList div.querySelectorAll(div); // NodeList静态 console.log(children.length); // 比如 1 const newDiv document.createElement(div); div.appendChild(newDiv); console.log(children.length); // 变成 2HTMLCollection自动更新 console.log(nodeList.length); // 还是 1NodeList没有感知到新节点了解这个差异对写代码很有帮助。比如你用children做遍历如果遍历过程中又新增了子节点动态集合的长度会一直变化可能导致死循环。这就是为什么在通过HTMLCollection遍历并同时操作节点结构时一定要非常小心。再往下拆一个容易混淆的点children只包含元素节点而childNodes包含所有子节点包括文本节点、注释节点。我们日常遍历子元素时基本都应该优先用children。想想看ul li结构里如果li之间写了换行和空格那childNodes里就会有一堆文本节点看起来是一堆空白的text节点。你要是没意识到这一点直接遍历childNodes去做操作很容易被这些空白节点干扰。3.2 firstElementChild、parentNode这些导航属性怎么用除了通过集合一次性拿到所有子节点DOM还提供了一系列导航属性方便你在节点之间跳转。parentNode、firstChild、lastChild、nextSibling、previousSibling这些是包含文本节点的版本而firstElementChild、lastElementChild、nextElementSibling、previousElementSibling则是专门跳过文本节点、只关注元素节点的版本。做实际页面的时候强烈建议优先使用带Element后缀的那一套。原因跟上面一样HTML里的换行和空格会产生文本节点如果使用不带Element的属性处理起来会有很多干扰。举个例子有一份结构是这样的ul li idfirst苹果/li li香蕉/li /uldocument.getElementById(first).nextSibling很可能返回的是一个文本节点因为/li和li之间有个换行符而nextElementSibling才是你真正想要的“香蕉”那个li。这个细节说实话我在工作初期被坑过不下三次每次都是调试半天最后才发现是换行符创造的空白文本节点在捣乱。3.3 遍历时最容易踩的死循环和越界问题有了动态集合的概念之后就能理解为什么遍历DOM同时修改结构是一件危险的事了。我之前做某个模拟项目时想实现一个功能把列表里所有li中的文本替换成大写。最直观的思路是获取所有li的集合遍历每一个改掉textContent。如果我用的是getElementsByTagName(li)得到的HTMLCollection遍历过程中一旦我向列表里新增了一个li哪怕是在遍历过程之前新增的只要遍历还没结束这个集合的length就会跟着变化。假如你在遍历时执行appendChild集合长度增加循环次数也会增加极端情况下会陷入无限循环。更安全的做法是先把集合转成静态数组再对数组做遍历const items Array.from(document.querySelectorAll(li)); items.forEach(item { // 在这里做增删操作都不会影响集合本身 });这种“先快照、再遍历”的思路不仅适用于DOM操作在很多处理动态数据的场景里都是通用的规则不要在遍历的过程中修改你正在遍历的容器。4. 案例实战用原生API做一个可增删的待办列表4.1 需求拆解和页面结构前面讲了不少方法与原理但技术点不落到项目里始终是空的。我们用待办列表这个经典场景来串一串。需求是这样页面上有一个输入框、一个“添加”按钮和一个列表。用户输入内容后点击按钮内容作为新的一项出现在列表底部每一项的右侧有一个“删除”按钮点击后该项消失如果某项的文本被点击则给它加一个删除线表示已完成。页面结构先这样规划div classtodo-app input idtodoInput typetext placeholder输入待办事项 / button idaddBtn添加/button ul idtodoList/ul /div注意列表一开始是空的所有项都是运行时由JS创建的。这样才符合Day3“动态创建元素”的训练目标。4.2 添加逻辑创建、填充、插入“添加”按钮的事件处理程序里我们需要做四件事读取输入框的值、判断值是否为空、创建列表项、把列表项插入列表末尾。如果用createElement来实现大概是这样const input document.getElementById(todoInput); const addBtn document.getElementById(addBtn); const list document.getElementById(todoList); addBtn.addEventListener(click, () { const text input.value.trim(); if (!text) return; const li document.createElement(li); li.textContent text; const deleteBtn document.createElement(button); deleteBtn.textContent 删除; deleteBtn.classList.add(delete-btn); li.appendChild(deleteBtn); list.appendChild(li); input.value ; input.focus(); });代码本身不复杂但有几个值得注意的地方。第一个是trim()它能把输入框首尾的空格去掉防止用户只输入空格时依然创建了一个空白项。第二个是input.focus()添加完之后让焦点自动回到输入框方便连续录入。这两个小细节是很多教学代码不会讲的但放到实际页面里体验差异特别明显。换成insertAdjacentHTML的写法会更紧凑list.insertAdjacentHTML(beforeend, li ${text} button classdelete-btn删除/button /li );这段模板字符串看起来方便但有一个隐患需要提醒如果用户输入的文本包含HTML标签比如img srcx onerror...直接拼进innerHTML或insertAdjacentHTML里会触发意外的问题。这类问题在项目里属于安全红线。所以在用HTML字符串方式插入用户输入内容之前至少要做一个简单的转义function escapeHtml(str) { return str.replace(/[]/g, (m) ({ : amp;, : lt;, : gt;, : quot;, : #39; }[m])); }之前在某个内部项目中我就是没做转义直接把用户输入的字符串插进了页面里结果一个用户提交了一段包含特殊字符的内容页面上直接解析出了奇怪的DOM结构排查了很久才发现问题根源。从那以后凡是涉及用户输入和HTML字符串拼接的场景我都会先包一层转义函数。如果使用的是createElement加textContent的方案其实天然就规避了这个问题因为textContent只会把内容当作纯文本不会解析HTML。4.3 删除与完成状态如何利用事件委托统一处理接下来是删除按钮。如果按照最直观的思路在添加每一项的时候就给删除按钮绑定click事件那代码会变成这样deleteBtn.addEventListener(click, () { li.remove(); });这种方式本身没有错但存在一个扩展性隐患如果列表项数量很大每一项都单独绑定一个监听器内存里就会积累大量事件处理函数。更好的做法是把监听器统一绑定到ul容器上利用事件冒泡机制在事件到达ul时判断实际触发事件的元素是谁。这种模式就是事件委托。用事件委托改写的删除逻辑和完成逻辑如下list.addEventListener(click, (event) { const target event.target; if (target.classList.contains(delete-btn)) { target.closest(li).remove(); return; } if (target.tagName LI) { target.classList.toggle(done); } });这里用到event.target来获取实际被点击的元素再用closest(li)从按钮向上查找到所属的列表项。之所以要closest而不是直接target.parentNode是为了防止未来在li内部嵌套更多结构比如增加一个图标或者一个span到那时候target.parentNode不一定还是li但closest(li)百分百能往上找到正确的项。这里我特别建议你用classList.toggle而不是classList.add来实现完成状态的切换因为toggle自带“有则移除、无则添加”的语义天然支持第二次点击取消完成状态不需要写额外的判断分支。4.4 这个案例背后隐藏的编程思路很多人会觉得待办列表太“教学化”实际项目不会这么简单。但我想说的是这个案例背后的编程思路比功能本身重要得多。第一“数据变化驱动界面更新”这种事用原生DOM操作来写虽然繁琐但会让你逐步建立起“界面是数据的一种映射”的意识。每一次改动节点之前先问自己对应的数据源是什么数据源变了界面应该怎么跟着变第二事件委托是一个普适性的设计模式不管你是用原生JS还是框架理解事件传播机制都是调试复杂交互的基础。第三安全边界意识上面提到的HTML转义看起来不起眼但一旦线上出了问题就是事故级别。5. 性能与细节批量插入、重绘重排和文档碎片的使用5.1 为什么循环插入节点会让页面卡顿如果在一个循环里用appendChild插入1000个列表项浏览器会执行1000次DOM插入操作。每一次插入都可能导致浏览器重新计算页面布局重排和重新绘制重绘页面会明显卡顿尤其是在低配设备上。为什么因为浏览器渲染页面是一个流水线过程DOM结构改变之后它需要重新计算每个元素的位置和大小然后绘制到屏幕上。如果循环里每次都触发这个过程浏览器就是在反复做全套重活。要解决这个问题一个非常经典的工具是DocumentFragment。你可以把它理解成一个“虚拟的轻量级文档容器”先把所有新节点放进这个容器里最后再把这个容器一次性插入到真实DOM中。浏览器只需要处理一次真实DOM的变更性能提升非常明显。const fragment document.createDocumentFragment(); for (let i 0; i 1000; i) { const li document.createElement(li); li.textContent 条目 ${i 1}; fragment.appendChild(li); } list.appendChild(fragment);注意fragment本身并不会出现在DOM里它只是一个中转站。当list.appendChild(fragment)执行完fragment里的子节点会全部被转移进listfragment变为空。这个机制让代码看起来优雅性能上又避免了循环插入带来的反复重排。5.2 字符串拼接与多次重排的取舍除了DocumentFragment还有一种常见做法是先用数组收集HTML字符串最后一次性通过innerHTML赋值。比如let html ; for (let i 0; i 1000; i) { html li条目 ${i 1}/li; } list.innerHTML html;这种方式性能也很好因为只触发了一次重排但代价是原列表里的事件绑定会全部丢失。如果列表里没有任何事件需要保留用这种方式完成一次性大批量渲染是可接受的。但凡是涉及到交互的列表比如点击行有响应、行内有按钮我都会优先考虑DocumentFragment因为节点本身不会丢失事件。还有一个务实层面的建议别盲目追求极限性能。对一个实际的项目来说列表量可能在几十到几百之间用appendChild循环插入其实也不会有什么体感卡顿。真正需要用到DocumentFragment或字符串拼接优化的是那种一次性渲染几千甚至上万节点的场景。把文档碎片用对地方比在每一行代码里都优化要有意义得多。5.3 批量更新的另一个思路先离线修改再上屏这里分享我在某个项目里用过的偏门但很有效的技巧。如果某个页面中的数据联动比较复杂比如一次操作要同时更新列表里的多个文本、多个样式类可以先把整个列表容器从DOM中摘下来在内存里完成所有修改再把它放回去。const parent list.parentNode; parent.removeChild(list); // 在这里对list做任意的增删改因为它已经不在页面中了 // 浏览器不会因为这些改动触发重排 parent.appendChild(list);这样做的好处非常明显离屏状态下DOM节点依然可以正常操作但不会触发页面渲染。等你把节点放回去时浏览器一次性渲染最终结果省掉了中间所有过渡状态的计算。这个技巧在做大规模表格重排、复杂筛选联动时很管用。当然它也会带来一个副作用操作期间如果有用户交互事件发生在该区域内会因为节点不在DOM树里而暂时失效。所以在使用前要评估好操作的耗时和用户的实时交互需求大多数情况下这类批量更新都非常快用户几乎感知不到节点被摘走又放回的过程。6. 一个真实排错经历为什么列表项一直新增失败6.1 问题表现说一个我实际遇到的排查过程也帮大家巩固一下这一天的知识点。当时我在写一个模拟项目X结构类似页面上有一组选项卡每个选项卡对应一个列表。点击某个选项卡就从这个列表里读取数据并渲染。一开始功能正常后来我加了另一个功能每次点击选项卡时如果当前列表为空就自动插入一条占位信息。结果快把我逼疯的现象发生了占位信息有时候插入成功有时候怎么点都没反应而且控制台没有任何报错。6.2 排查链路先检查事件绑定是不是正常点了选项卡之后回调函数有没有进入。通过在函数第一行打印日志确认回调是进入了但代码执行到插入逻辑时列表却没有变化。再次检查插入逻辑中用到的节点引用发现我用一个变量保存了列表容器的引用这个变量是页面加载时通过querySelector获取的。问题就出在这里后来我在另一个函数里用innerHTML重新给这个列表容器赋了一次值。这一赋容器内部的节点全部被替换了但我的变量引用的依然是那个容器本身。按道理innerHTML只是换掉了子节点容器引用仍然有效……除非容器本身也被替换了。继续追查发现代码某处直接把整个容器替换成了一个新的元素const newList document.createElement(ul); oldList.parentNode.replaceChild(newList, oldList);因为替换之后旧容器已经不在DOM树里了而我保存的变量依然指向旧容器。往一个不在页面中的节点里插入子节点页面自然看不到任何变化。浏览器也不报错因为它觉得你的操作是合法的只是这个容器不在可视的DOM结构里了。6.3 最终的修复和教训修复的办法其实很简单在需要插入操作的时候不要依赖页面加载时保存的旧引用而是每次都重新通过document.getElementById或querySelector获取当前DOM树里的真实节点。这次排查让我牢牢记住了一个原则保存DOM节点引用时一定要意识到这个引用可能因为容器的替换、移动而失效。尤其是做动态渲染较多的页面时别以为一个变量保存了节点就能永远放心使用。另外排查这类问题时不妨用一个直接的方法验证节点是否还在DOM树里比如在控制台打印node.isConnected它返回true表示节点还在文档中false表示已经被移除了。这个小属性对调试这类问题非常高效比反复查看页面效果靠谱多了。再看一遍那个最容易被忽略的知识点replaceChild替换的是“子节点”但它如果被用来替换整个容器那外部指向旧容器的引用就全部作废了。这提醒我们操作DOM时永远要有一个意识操作之前搞清楚你手里的引用是哪个层级上的节点操作之后哪些引用会继续有效哪些会变成悬空的。7. 深挖两个高频面试点类数组转数组与for...of遍历7.1 为什么NodeList不能直接调用mapdocument.querySelectorAll(li)可以返回一个NodeList支持forEach但早期的浏览器实现里它并不支持map、filter这些数组方法。于是很多面试题会问怎么把一个类数组转成真正的数组在ES6之后最优雅的做法是Array.from()const items Array.from(document.querySelectorAll(li)); items.map(item item.textContent);或者使用展开运算符const items [...document.querySelectorAll(li)];这两种方式原理类似都是把可迭代对象展开成一个新数组。区别在于Array.from还接收第二个参数一个映射函数可以顺带完成数组元素的转换const texts Array.from(document.querySelectorAll(li), li li.textContent);这个写法在项目里非常常见比如你要一次性拿到所有列表项的文本或者给每一项加一个序号都可以直接写在Array.from的第二个参数里比先转数组再map省一步。7.2 for...of与forEach在DOM遍历里的区别NodeList支持forEach是好事但forEach存在一个限制没法在循环中使用break或continue来提前跳出或跳过。而你用for...of遍历NodeList时就可以正常使用这两个关键字。for (const li of document.querySelectorAll(li)) { if (li.textContent 不需要的项) continue; li.classList.add(visible); }这个区别在工作量不大时可能不明显但一旦列表中需要做条件筛选、提前中断for...of就是更合适的遍历方案。另外for...of天然支持await如果你需要在遍历过程中逐个做异步请求forEach做不到for...of加上await可以。这些细节不值一提但关键时刻能省你很多事。7.3 数组方法在DOM操作中的组合用法最后分享一个结束前的实用组合。把类数组转成数组之后很多数组操作就都能施展了。比如一键删除所有选中的项Array.from(document.querySelectorAll(li.selected)) .forEach(li li.remove());比如批量修改某项的样式Array.from(document.querySelectorAll(li)) .filter(li li.dataset.status expired) .map(li li.classList.add(danger));又比如取到列表中特定数据结构的项再做进一步展示更新。这些操作每天写页面时都在重复而它们的根基就是“把类数组转成真数组”这一步。8. 把这些API串起来的工程化思维8.1 三个开发中必须养成的习惯总的来说Day3的知识点密度并不高靠死记硬背API列表是撑不起实际开发的。回想我自己做过的项目能稳定高效地使用DOM操作靠的是三个习惯。第一操作前先确认数据形态。你要增删改的节点是从哪里来的是接口返回的数组是用户输入的内容还是页面里原本就有的结构数据源决定了你用创建新节点的方式还是用现有节点的引用。第二修改后注意引用的有效性。特别是涉及替换、移动、重绘之后旧引用很可能已经失效。第三批量操作时先考虑性能组织方式。是用DocumentFragment集中插入还是离屏操作完再挂回来或者干脆字符串拼接一次赋值。这些都应该是操作之前就决定的事而不是出了问题再回头优化。8.2 为什么说原生DOM是理解现代框架的钥匙我之前有一阵子在带一个刚入行半年的同事他一开始学React总觉得生命周期和渲染机制很难理解。我建议他先回头把原生DOM操作再过一遍搞清楚appendChild到底把节点怎么移来移去、DocumentFragment为什么能提升性能、事件委托又是怎么工作的。他补完这些再去写React很多困惑瞬间解开了。比如React里用key就是为了准确识别哪些节点需要复用而创建、插入、删除节点的底层逻辑还是那些原生API。我始终觉得框架是解决复杂业务场景的利器但底层能力应该长在你自己身上。你只要能徒手用原生API把一块数据渲染成一个可交互的列表理解起来任何框架都会更轻松。相反如果一开始就只想靠框架遇到复杂交互时容易陷入“文档翻遍也不知道从哪改起”的困境。8.3 一个简单但很实用的自查清单最后送你一个自检清单。每次你用DOM完成一个交互功能之后对照着问一遍自己数据从哪来如果数据变了界面是否需要同步变化怎么同步新增的节点上有没有带用户输入的内容如果有我有没有做转义用到的节点引用是不是DOM树里当前有效的引用如果操作涉及批量插入有没有考虑使用DocumentFragment或离屏更新事件绑定是给每个节点单独加还是委托给了统一容器删除节点后有没有遗留的事件监听器这些问题在Day3的学习阶段可能覆盖不全但在实际项目中它们会反复出现。我可以在日常练习中刻意用原生API做一些小场景比如动态表格、卡片列表、拖拽排序的简化版手感就会慢慢出来。等你哪一天写完功能不再需要频繁刷新页面验证能下意识预判这个操作会不会引发布局抖动、引用会不会失效那就算真正把Day3学到位了。
RELATED

相关推荐

短租开放数据全流程分析:数据清洗到房价预测建模

短租开放数据全流程分析:数据清洗到房价预测建模

做数据分析最怕什么?不是不会调库,而是拿到一堆真实数据后不知道从哪下手。我建议第一个完整项目就从某短租平台的开放数据开始:这是某国际都市的房源数据,字段覆盖价格、房型、位置、评论时间、可订天数,既有连续数值…

📅 2026/10/13 4:22:15
SQL聚合查询从入门到实战:GROUP BY、HAVING与JOIN的常见陷阱与优化

SQL聚合查询从入门到实战:GROUP BY、HAVING与JOIN的常见陷阱与优化

说实话,写了那么多年代码、刷了那么多套面试题,我越来越觉得SQL里最容易被低估的就是聚合。很多人觉得聚合不就是 SUM、COUNT、GROUP BY 嘛,闭着眼都会写。但真到了线上,一个 COUNT 翻倍、一个 WHERE 和 HAVING 用错,分…

📅 2026/10/13 4:22:15
C++游戏开发中enum class实战:从传统enum坑到类型安全迁移

C++游戏开发中enum class实战:从传统enum坑到类型安全迁移

做游戏客户端这几年,C枚举是几乎每个系统都会用到的基础设施:角色状态、怪物AI、技能类型、消息通道、资源分类,随手一数就是一大串。以前项目里到处是C风格的enum,写起来确实爽——不用敲那么多字,还自动兼容整数。可…

📅 2026/10/13 4:17:14
MORE NEWS

更多资讯

📰

STM32 CAN总线实战:从协议原理到配置收发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

多模态Agent如何规划UI测试路径:从感知融合到动态决策的工程实践

1. 多模态 Agent 做 UI 测试,到底难在哪UI 测试这件事,做过的人都懂——写起来不难,维护起来要命。一个按钮挪了位置、一个弹窗改了文案、一个加载动画多转了半秒,传统基于选择器的自动化脚本就可能集体飘红。而多模态 Agent 的出…

📰

多步任务总在细节崩盘?用技能编排让智能体真正稳定

最初让我意识到"工具调用"和"技能编排"是两码事的,是一次很普通的实测:我在一个demo里给智能体接入了邮件发送、日历查询和文档搜索三个工具,模型只凭从三个工具拿到的零散结果,接连搞砸了一个"帮用户确…

📰

Agent技能系统实战:为大模型装上可插拔的“手”和“脚”

这两年但凡碰过AI应用开发的,应该都绕不开一个词——Agent(智能体)。模型本身只是“脑子”,真正让它干活的,是那一套技能系统,也就是项目标题里说的“agent-skills”。不少团队早期做Agent产品时都有一个共…

📰

AnyPS5:跨平台文件处理与格式转换的轻量化解决方案

1. 项目缘起与核心定位AnyPS5 这个名字第一次出现在我视野里的时候,我正被一堆跨平台文件处理的需求折腾得够呛。团队里有人用 Windows 做设计,有人用 macOS 写代码,还有人习惯在 Linux 环境下跑批处理脚本,每次交付物在三个系统之…

📰

UCINET数据导入实战:格式规范、Excel整理与高频排错

1. 先搞清楚UCINET到底在等什么数据很多人在UCINET上栽的第一个跟头,不是不会跑算法,而是数据根本喂不进去。明明Excel里填得工工整整,一点Import却报错,或者导进去之后变成一团乱麻,节点对不上、值全错位。这些问题的…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬