ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从拖拽到状态隔离:Web-Dev-For-Beginners 温室项目中的 DOM 操作与 JavaScript 闭包实战

从拖拽到状态隔离:Web-Dev-For-Beginners 温室项目中的 DOM 操作与 JavaScript 闭包实战 从拖拽到状态隔离Web-Dev-For-Beginners 温室项目中的 DOM 操作与 JavaScript 闭包实战【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners本篇技术文章基于 Web-Dev-For-Beginners 仓库中温室Terrarium项目的第三课文档完整讲解如何用原生 JavaScript 实现“把 14 株植物随意拖拽摆放”的交互功能从defer引入脚本、getElementById定位 DOM 元素到用一个闭包closure封装pointerDrag、elementDrag、stopElementDrag三个内部函数实现坐标追踪与事件清理。读完并跟做一遍你将掌握指针事件Pointer Events驱动拖拽的完整模式以及为什么闭包是“每个可拖拽元素拥有独立状态”的标准解法。原文档配图DOM 与其引用的 HTML 标记的树形表示。一、什么是 DOM 操作为什么还需要闭包DOMDocument Object Model文档对象模型是对网页文档结构和内容的对象化数据表示。浏览器加载 HTML 页面后会在内存中构建这棵“对象树”JavaScript 通过 DOM API 可以访问、修改、重排页面上的任何元素——这正是静态页面变成交互界面的桥梁。原课文明确指出网页开发中围绕 DOM 管理的种种难题往往是大家转向使用 JavaScript 框架的动机而本项目选择自己用原生 JavaScript 管理 DOM。同时本课引入 JavaScript 闭包的概念一个函数内部再嵌套函数使得内部函数可以访问外部函数的作用域。原文档也坦承闭包是“巨大而复杂的主题”本模块只在温室代码中呈现一个最基础的用法一个外部函数包含内部函数让内部函数能访问外部函数的作用域。在本项目中我们会把两者结合起来用闭包来驱动 DOM 操作让温室页面上的每株植物都可以被用户拖拽到任意位置。二、前置条件与脚本接入defer的作用前置条件完成本课需要已经拥有前几课构建好的温室 HTML 和 CSS即3-terrarium模块第 1、2 课的产物。到本课结束时你将能把植物从侧边栏拖进温室。仓库中 solution/index.html 给出了完整的页面结构页面左右两侧各有#left-container与#right-container各容纳 7 株植物中间是#terrarium玻璃罐区域。任务创建并引入script.js在你的温室文件夹中新建一个名为script.js的文件并在index.html的head段引入它script src./script.js defer/script原文档对defer的说明值得逐字记住使用defer是为了让JavaScript 在 HTML 文件完全加载之后再执行也可以改用async属性它允许脚本在 HTML 仍在解析时执行但在本例中拖拽脚本必须等 HTML 元素完全可用之后才能执行因此defer是正确的选择。这一点在仓库答案文件的 index.html 中得到印证script src./script.js defer/script正是放在head中的最后一行。若脚本先于 DOM 就绪而执行document.getElementById(plant1)会返回null后续拖拽绑定将直接失败——这就是defer防住的典型时序错误。三、DOM 元素用 ID 定位 14 株植物第一步是为你要操作的元素建立引用。在我们的场景里就是当前等待在侧边栏里的14 株植物。任务代码dragElement(document.getElementById(plant1)); dragElement(document.getElementById(plant2)); dragElement(document.getElementById(plant3)); dragElement(document.getElementById(plant4)); dragElement(document.getElementById(plant5)); dragElement(document.getElementById(plant6)); dragElement(document.getElementById(plant7)); dragElement(document.getElementById(plant8)); dragElement(document.getElementById(plant9)); dragElement(document.getElementById(plant10)); dragElement(document.getElementById(plant11)); dragElement(document.getElementById(plant12)); dragElement(document.getElementById(plant13)); dragElement(document.getElementById(plant14));这段代码在做什么你在引用文档并在其 DOM 中查找带有特定 ID 的元素。还记得在 HTML 第一课参见 1-intro-to-html/README.md中你给每株植物图片都赋了不同的idplant1之类的标识吗现在正是用上这个功夫的时刻。识别出每个元素后就把它传进名为dragElement的函数下一节创建这样 HTML 中的元素就变得可拖拽了。仓库答案 solution/script.js 的前 14 行与上述任务代码逐行一致随后才是dragElement的函数定义——JavaScript 的函数声明会被提升hoisting所以先调用、后定义的写法可以正常工作。为什么用 ID 而不是 CSS 类原文档留下的自测题为什么按 ID 引用元素而不用 CSS class答案可回溯到 CSS 一课ID 对单个元素是唯一的class 是为一组元素共享样式设计的。当 JavaScript 需要精确操作某一个元素时ID 提供了唯一的定位而class可能匹配多个元素getElementsByClassName会返回一个集合无法确定性地指向“这一株”植物。从源码结构看getElementById返回单个元素引用这正好与dragElement(terrariumElement)的“一次绑一个元素”签名相匹配。四、闭包概念一个糖果数组的例子在写dragElement闭包之前原文档用一个例子说明闭包的基本行为——外部函数包围一个内部函数内部函数可以“看见”外部函数里的局部变量function displayCandy(){ let candy [jellybeans]; function addCandy(candyType) { candy.push(candyType) } addCandy(gumdrops); } displayCandy(); console.log(candy)在这个例子中displayCandy函数包围了一个往数组里推入新糖果类型的内部函数该数组已存在于外层函数内。如果你运行这段代码console.log(candy)会报candy未定义因为它是displayCandy的局部变量“封闭在闭包内”。✅ 原文档的自测题如何让candy数组变得可访问尝试把它移到闭包外面——这样数组就成了全局变量而不再只存在于闭包的局部作用域内。把这个模式平移到温室项目上14 株植物各自需要独立记录自己的四个坐标。如果坐标只是脚本顶层的全局变量任意一株植物的拖拽都会污染其他植物的状态用闭包则可以为每个元素生成一套私有且持久的坐标变量。五、构建dragElement闭包骨架现在来创建dragElement闭包一个外部函数包裹着内部函数在本例中是三个。闭包的价值在于让内部函数访问外部函数的作用域。任务在script.js的元素声明下方创建函数function dragElement(terrariumElement) { //set 4 positions for positioning on the screen let pos1 0, pos2 0, pos3 0, pos4 0; terrariumElement.onpointerdown pointerDrag; }逐部分拆解dragElement从脚本顶部的声明处接收terrariumElement对象即某一株植物的img元素引用四个局部位置变量pos1–pos4初始化为0它们会在后续每株植物的拖拽中被反复改写实现“追踪该元素在屏幕上的位置”。温室将被这些被拖动的元素填充因此应用需要知道它们被放到了哪里被传入的温室元素被绑定了一个onpointerdown事件处理器。onpointerdown是 Web API 的一部分属于 Pointer Events 家族在按下按钮或我们的场景中触摸到一个可拖拽元素时触发。根据原文档的提示它在桌面与移动浏览器上均有支持可用 CanIUse 之类的兼容性查询工具核对各浏览器支持情况。✅ 原文档的自测题onclick事件处理器的跨浏览器支持更好为什么这里不采用答案在于我们构建的交互类型——这是拖拽而非单击onclick只在按下与释放都发生在同一元素后的“点击”语义上生效无法捕捉按住后指针离开的移动过程而指针事件onpointerdown/onpointermove/onpointerup天然覆盖鼠标、触摸和手写笔。仓库答案 solution/script.js 中dragElement的定义与任务代码完全一致且函数注释引用了 MDN 对闭包的标准定义闭包是“函数与其词法环境外部状态引用的组合”。六、pointerDrag捕获拖拽的起点terrariumElement已经就绪当onpointerdown事件触发时pointerDrag函数被调用。任务在terrariumElement.onpointerdown pointerDrag;一行之下即闭包内部添加function pointerDrag(e) { e.preventDefault(); console.log(e); pos3 e.clientX; pos4 e.clientY; }这段代码做了多件事阻止默认行为e.preventDefault()抑制指针按下时浏览器的默认事件例如选中图片、触发图片拖影。这样你对界面行为拥有更大控制权。原文档还布置了一个对照实验等整个脚本文件构建完成后去掉e.preventDefault()再试一次观察发生了什么观察事件对象在浏览器中打开index.html点击一株植物你会在控制台中看到捕获到的e事件对象。翻一翻这个对象可以看到指针按下事件中收集了多少信息clientX、clientY、screenX、pointerId等记录按下坐标局部变量pos3和pos4被设置为e.clientX与e.clientY——即你点击/触摸那一刻指针在视口中的 X、Y 坐标。当你按住并拖动植物时需要精确控制植物的行为因此要持续跟踪它的坐标。✅ 此时应该更清楚为什么整个应用被构建为一个大闭包如果不这样做你如何为 14 个可拖拽植物中的每一个都保留独立的坐标空间闭包里的pos1–pos4正是每个dragElement调用各得一套的私有状态。注册移动与结束事件在pos4 e.clientY之下再绑定两个指针事件完成初始动作document.onpointermove elementDrag; document.onpointerup stopElementDrag;这表示你希望植物跟随指针一起被拖拽onpointermove并在你松开植物时停止该手势onpointerup。这两个事件与onpointerdown同属一个指针事件 API。注意它们被绑定在document上而不是植物元素上——因为指针一旦离开植物图片区域绑定在元素自身上的移动事件就会丢失绑在文档上才能保证拖拽全程平滑跟踪。此时界面会抛错因为你还没有定义elementDrag和stopElementDrag接下来把它们补上。七、elementDrag与stopElementDrag移动与清理你希望的行为是随时能抓起任意植物放到屏幕任意位置。这个接口非常自由比如没有“投放区”约束让你可以用添加、移除、重新摆放植物的方式随心所欲地设计温室。elementDrag增量坐标计算在pointerDrag的结束花括号之后添加function elementDrag(e) { pos1 pos3 - e.clientX; pos2 pos4 - e.clientY; pos3 e.clientX; pos4 e.clientY; console.log(pos1, pos2, pos3, pos4); terrariumElement.style.top terrariumElement.offsetTop - pos2 px; terrariumElement.style.left terrariumElement.offsetLeft - pos1 px; }这里大量改写了闭包内设置的那四个局部位置变量。逻辑链条是拖动过程中pos1被设为pos3上一次的e.clientX减去当前的e.clientX即指针水平方向的位移增量pos2做同样的垂直方向运算随后把pos3、pos4重置为当前坐标作为下一次计算的基线拖动时可以在控制台实时看到这些变化值最后改写植物的 CSS 内联样式以offsetTop/offsetLeft元素当前的上、左偏移为基础减去位移增量得出新的top与left像素值。原文档提示offsetTop和offsetLeft是 CSS 属性根据元素的定位父级确定元素位置其参照对象可以是任何未被以static方式定位的父元素。这一说明与仓库 solution/style.css 的结构互为印证.plant被设置为position: absolute其父级.plant-holder为position: relative——正是因为存在这个定位参照链offsetTop/offsetLeft的读写才能驱动植物真正移动。整个“位置重算”过程让你得以精确修正温室与植物的行为。stopElementDrag把事件处理器置空完成界面的最后一步在elementDrag的结束花括号之后添加function stopElementDrag() { document.onpointerup null; document.onpointermove null; }这个小小的函数在手指/鼠标抬起时把onpointerup与onpointermove重置为null于是你可以重新开始拖同一株植物或开始拖另一株。✅ 原文档自测题如果不把这些事件置空会发生什么——elementDrag会持续被document上的pointermove触发即使你并没有按住任何植物每多拖过一株植物文档上就会再叠加一套监听未释放的处理器既浪费性能又会造成“没在拖、植物却在动”的错乱行为。这就是拖拽系统里“注册—清理”成对出现的原因。对照仓库的完整答案把以上片段按序拼起来就得到仓库中的完整实现 solution/script.js。它的整体形态值得完整过一遍——pointerDrag、elementDrag、stopElementDrag三个内部函数全部嵌套在dragElement闭包内部function dragElement(terrariumElement) { //set 4 positions for positioning on the screen let pos1 0, pos2 0, pos3 0, pos4 0; terrariumElement.onpointerdown pointerDrag; function pointerDrag(e) { e.preventDefault(); console.log(e); // get the initial mouse cursor position for pos3 and pos4 pos3 e.clientX; pos4 e.clientY; // when the mouse moves, start the drag document.onpointermove elementDrag; // when the mouse is lifted, stop the drag document.onpointerup stopElementDrag; } function elementDrag(e) { // pos1 where the Xmouse WAS - where it IS pos1 pos3 - e.clientX; // pos2 where the Ymouse WAS - where it IS pos2 pos4 - e.clientY; //reset pos3 to current location of Xmouse pos3 e.clientX; //reset pos4 to current location of Ymouse pos4 e.clientY; console.log(pos1, pos2, pos3, pos4); // set the elements new position: terrariumElement.style.top terrariumElement.offsetTop - pos2 px; terrariumElement.style.left terrariumElement.offsetLeft - pos1 px; } function stopElementDrag() { // stop calculating when mouse is released document.onpointerup null; document.onpointermove null; } }注意文件头部的注释该可拖拽元素的解法灵感来自 w3schools 的“Draggable HTML Element”经典教程本课将其从鼠标事件升级为指针事件并套用闭包结构。完成后的温室效果如下图所示原文档配图八、挑战扩展你的闭包原文档的 挑战为闭包添加新的事件处理器让植物能做更多事情——例如双击某株植物将其置于最前操作z-index。放开手脚发挥创意。从源码结构看这个扩展完全落在现有闭包之内在dragElement内部再加一个terrariumElement.ondblclick bringToFront;其中bringToFront内部函数同样能访问terrariumElement实现上只需把该元素的style.zIndex抬高。这也再次印证了闭包的组织价值——每个植物的所有交互逻辑都封闭在各自的作用域里。九、复习与自学习更多拖拽与指针事件路径原文档的“Review Self Study”部分给出了三条延伸方向这里完整保留HTML Drag and Drop API屏幕内拖拽元素看似简单实际有多种做法和各自的取舍。浏览器原生提供了完整的拖放 APIdragstart、dragover、drop等你可以试着在别的项目中体验它。本课之所以没用它是因为想要实现的效果与之不同自由定位而非投放区语义指针事件规范关于 Pointer Events 的更多信息可查阅 W3C 的 Pointerevents 规范与 MDN 的 Pointer Events 文档浏览器兼容性始终使用 CanIUse 一类的工具核查目标浏览器对所用 API 的支持情况。本模块还配有 DOM 进阶作业任选一个 DOM 接口做深度调研并写出技术分析与可运行的代码示例作业详情见 assignment.hi.md英文原文为 assignment.md。小结这节课的技术主线可以浓缩成一条链getElementById拿到元素引用 → 每株植物各自调用一次dragElement生成闭包实例得到私有的pos1–pos4状态 →onpointerdown捕获按下坐标并在document上注册onpointermove/onpointerup→elementDrag用“旧坐标减新坐标”的增量重算offsetTop/offsetLeft并写回内联样式 →stopElementDrag置空处理器完成清理。整条链路只依赖原生 DOM API 与 Pointer Events却是理解框架层拖拽库、状态管理与事件生命周期的一扇真实入口。仓库中的完整可运行答案在 solution/script.js、solution/index.html 与 solution/style.css对照本文各小节的片段逐步构建即可复现这个可自由拖拽的温室。【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表