拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Web-Dev-For-Beginners Terrarium 项目第三课:DOM 操作与 JavaScript 闭包(Closures)实现元素拖拽

Web-Dev-For-Beginners Terrarium 项目第三课DOM 操作与 JavaScript 闭包Closures实现元素拖拽【免费下载链接】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虚拟生态瓶项目的第三课围绕两条主线展开一是 DOMDocument Object Model文档对象模型——浏览器把 HTML 解析成的可编程对象树是 JavaScript 与页面元素交互的通道二是 JavaScript 闭包closure——一个函数包裹在外层函数之内使内部函数可以持续访问外层函数的作用域。读完本篇并动手完成后你将用原生 JavaScriptvanilla JS为 14 株植物实现“按住即可拖到页面任意位置”的交互完整走通pointerdown/pointermove/pointerup指针事件生命周期并理解为什么每株植物都能拥有独立、互不干扰的坐标状态。1. 课程定位与前置要求DOM 操作是 Web 开发的核心技能。正如 MDN 的定义DOM 是描述网页文档结构与内容的对象数据表示。围绕 DOM 操作的各种挑战也是过去许多开发者选择用 JavaScript 框架而非原生 JavaScript 来管理页面状态的重要原因——而本课程坚持只用原生 API 完成全部功能。本课同时引入闭包的概念可以把它理解为“被另一个函数包围的函数”从而让内部函数能够访问外部函数的作用域。需要注意的是JavaScript 闭包是一个庞大且复杂的主题本课只涉及最基础的理念在 Terrarium 的代码中你会看到一组内外部函数以特定方式构建使内部函数能够访问外部函数的作用域。更深入的闭包机制变量提升、词法环境等建议在课后自行研读 MDN 的相关文档。前置要求在开始本课之前你应当已经完成了 Terrarium 项目的 HTML 和 CSS 部分第一课与第二课。本课结束后你将能够在生态瓶页面里把植物拖入、拖出并自由摆放。从仓库中的成品参考来看整个页面结构见 solution/index.html左右两个侧栏容器#left-container、#right-container各摆放 7 株植物共 14 株img classplant idplant1..14中间是用纯 CSS 绘制的玻璃瓶.jar-walls、.dirt等。这些元素就是本课 JavaScript 将要接管操作的对象。2. 准备 JavaScript 文件script.js与defer属性在 Terrarium 文件夹中创建一个新文件script.js然后在index.html的head部分引入它script src./script.js defer/script这里有一个关键的细节为什么要用defer属性defer会让外部脚本等待整个 HTML 文件加载解析完成后才执行从而保证脚本运行时getElementById能查到所有植物元素也可以考虑async属性它允许脚本在 HTML 仍在解析时就执行——但对拖拽功能而言必须确保所有 HTML 元素已经完全可用、可供抓取才能启用拖拽脚本因此本课选用defer而不是async。仓库中的 solution/index.html 正是这样写的第 13 行script src./script.js defer/script3. 建立 DOM 元素引用把 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));这段代码做了什么它引用了document并在其 DOM 中查找具有特定 Id 的元素。还记得在 HTML 第一课中你给每株植物图片都赋予了唯一的 Ididplant1等吗现在这些 ID 派上了用场。找到每个元素后把它作为参数传入一个名为dragElement的函数——这个函数稍后构建。这样HTML 中的元素就获得了或即将获得可拖拽能力。仓库参考实现 solution/script.js 的第 3–16 行与上面完全一致14 次调用逐一注册。思考题为什么用 Id 来引用元素而不是 CSS 类回顾 CSS 课程即可得到答案Id 在整个文档中必须唯一天然指向“某一个”元素getElementById因此总返回精确的一个节点而 class 是为“一组”元素设计的选择器用来做群体样式。JavaScript 需要精确定位单个元素时Id 是更合适的定位方式。若两个元素重复使用了同一个 IdgetElementById只会返回文档中第一个匹配的节点。4. 闭包入门外层函数与内部函数的作用域关系下面创建核心结构——dragElement闭包一个外层函数包裹着若干内部函数在本例中是三个。闭包在“一个或多个函数需要访问外层函数的作用域”时非常有用。先看一个简单示例function displayCandy(){ let candy [jellybeans]; function addCandy(candyType) { candy.push(candyType) } addCandy(gumdrops); } displayCandy(); console.log(candy)在这个例子里displayCandy内部包裹了一个addCandy函数它把一个新糖果类型推入到外层函数中已存在的数组。注意如果你真的运行这段代码console.log(candy)会报错——candy是未定义的因为它是displayCandy的局部变量属于这个闭包的作用域在外部无法访问。思考题如何让candy数组在闭包外也能访问把它声明移出闭包即可——这样数组变成全局变量而不再只存在于闭包的局部作用域中。这个“局部 vs 全局”的取舍正是理解闭包价值的第一步。5. 构建闭包主体dragElement函数与四个位置变量在script.js中把下面这个函数写在 14 个元素声明的下方function dragElement(terrariumElement) { // 定义 4 个用于屏幕定位的坐标 let pos1 0, pos2 0, pos3 0, pos4 0; terrariumElement.onpointerdown pointerDrag; }从源码结构看这四个变量各自承担明确职责参考 solution/script.js 第 23–29 行的完整实现与注释pos3/pos4记录当前指针的clientX/clientY坐标pos1/pos2记录相邻两次pointermove事件之间指针位置的差值“指针原来在哪”减去“指针现在在哪”用来计算本次应让元素移动多少像素。每次调用dragElement(某株植物)JavaScript 都会为该次调用创建一组全新的pos1–pos4并且只有本次调用内定义的三个内部函数能读写它们——这就是“每株植物各自维护独立坐标记忆”的实现机制。如果不用这种结构14 株植物共享同一组全局变量同时或交替拖动时状态就会互相污染。此外传入的terrariumElement元素被赋了一个pointerdown事件处理函数terrariumElement.onpointerdown pointerDrag;。onpointerdown属于 Web 指针事件 API在鼠标按下或触屏触摸元素时触发在桌面浏览器与移动端浏览器上都能工作个别浏览器存在例外可用性可用 CanIUse 查询验证。思考题onclick事件在各浏览器中支持得更好为什么这里不用它想一想你要创造的屏幕交互形态——拖拽是“按住—移动—松开”的连续手势onclick只描述“点并按下并释放”这一个离散瞬间无法捕获中间的移动过程指针事件才能覆盖“按下的那一刻”这正是拖拽的起点。6. 第一个内部函数pointerDrag——捕获拖拽起点terrariumElement已就绪一旦onpointerdown被触发就会调用pointerDrag。把下面这个函数加在terrariumElement.onpointerdown pointerDrag;这一行的正下方即仍处在dragElement函数体内function pointerDrag(e) { e.preventDefault(); console.log(e); pos3 e.clientX; pos4 e.clientY; }这里发生了多件事e.preventDefault();阻止pointerdown时浏览器默认发生的行为例如拖动植物时选中文字、触发图片默认拖拽等让我们对界面行为拥有更精细的控制。课程建议你先把脚本写完然后临时去掉这一行再试一次——观察植物被拖走时页面发生了什么就能直观理解这行的必要性console.log(e);打开index.html后在浏览器中审查页面点击一株植物可以在事件检查器中展开这个事件对象查看pointerdown事件究竟携带了多少信息pos3/pos4被赋值为e.clientX/e.clientY即你按下或触摸植物那一刻它在视口中的 X、Y 坐标。这些坐标值是后续每帧移动计算的基础。思考题为什么整个应用要构建成“一个大闭包”如果不这样组织14 株可拖拽植物各自的坐标状态4 个变量 × 14 个元素要靠什么来维持闭包给出的答案是每次调用自动创建相互隔离的作用域。接着在pos4 e.clientY;之后补充两条事件注册document.onpointermove elementDrag; document.onpointerup stopElementDrag;它们的含义是指针移动时调用elementDrag让植物跟随指针抬起取消选中植物时调用stopElementDrag结束这次拖拽手势。onpointermove与onpointerup和onpointerdown属于同一套指针事件 API。注意它们注册在document上而不是植物元素上——这样即使指针快速移出植物本体移动与松开的追踪也不会中断。此时elementDrag和stopElementDrag尚未定义页面控制台中会报错属于预期现象下面立即补上。仓库参考实现 solution/script.js 第 31–41 行与本课完全对应注释标明了“获取初始鼠标坐标到 pos3/pos4”“鼠标移动时开始拖动”“鼠标抬起时停止拖动”三个阶段。7. 第二个内部函数elementDrag——坐标数学与位置重算把elementDrag加在pointerDrag的右花括号之后仍在dragElement内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; }这段函数反复改写你在外层函数中声明的那 4 个位置变量执行“增量式移动”计算位移量pos1 pos3 - e.clientX——即“指针上一次的位置”减去“指针现在的位置”Y 方向同理得到pos2。每次pointermove只处理两次事件之间的那一小段位移而不是从零开始重算更新基准坐标把pos3、pos4重置为本次事件的clientX、clientY作为下一次 move 的起点。拖动过程中可以观察控制台里这四个值随指针实时变化写回元素位置通过修改植物的 CSS 内联样式top与left完成实际位移。offsetTop与offsetLeft返回元素相对于其“定位父元素”最近一个非static定位的祖先的偏移量offsetTop - pos2、offsetLeft - pos1就是植物新的绝对坐标。为什么offsetTop/offsetLeft在这里有效因为 Terrarium 的 CSS见 solution/style.css中.plant { position: absolute; }第 33–38 行且侧栏容器.container也是position: absolute第 18–25 行。也就是说植物元素相对定位父级摆放offsetTop/offsetLeft读取到的正是可被style.top/style.left直接写回的同一坐标体系——这是整个拖拽数学能够闭环的前提。这一系列位置重算就是让生态瓶里每一株植物的跟手行为足够顺滑的全部秘密。8. 第三个内部函数stopElementDrag——清理事件最后一个任务在elementDrag的右花括号之后加入stopElementDragfunction stopElementDrag() { document.onpointerup null; document.onpointermove null; }这个很小的函数把document上的onpointerup和onpointermove重置为null。它的意义是松开植物之后你可以随时重新开始拖动同一株植物或者开始拖动另一株植物系统都不会残留上一次拖拽的“尾巴”。思考题如果不把这些事件置空会怎样可以推断document上的 move/up 处理函数会一直挂在文档上之后页面上任意指针移动都会继续触发上一株植物的坐标重算表现为“没有按住植物它也在动”或“拖动 A 之后 B 失控”并且每次拖拽都叠加一层无效回调白白消耗性能。至此14 行元素注册 一个三内部函数闭包就构成了一个自由度的拖拽界面可以随时拖动任意植物放到屏幕上任意位置。这个界面刻意不加“投放区”限制让你可以随意增删、重排植物把自己的 Terrarium 布置成想要的样子。完整的可运行参考位于 solution/script.js全文约 64 行页面与样式分别在 solution/index.html 和 solution/style.css。该文件开头的注释也说明了其设计灵感来自 w3schools 的可拖拽元素方案并引用了闭包的定义闭包是“一个函数与其词法环境的组合”让你可以从内部函数访问外部函数的作用域——本课正是用它来追踪被拖动的元素。完成后的效果如下仓库中3-terrarium/3-intro-to-DOM-and-closures/images/terrarium-final.png展示了 14 株植物被拖入玻璃瓶的最终画面可直接在该路径查看。9. 挑战与延伸学习本课挑战给闭包再添加一个新的事件处理函数对植物做点别的事——例如双击植物把它移到最上层操作z-index。尽情发挥创意。自查与延伸阅读如果“在屏幕上拖元素”看起来轻而易举要知道它有很多实现方式也埋着不少陷阱取决于你想要的效果。浏览器本身就提供了一整套 HTML 拖放Drag and DropAPI本课没有使用它是因为想要的交互效果与它的“源/投放区”模型不太一致可以拿这套 API 在自己的项目里试一试指针事件的更多细节可查 W3C 指针事件规范与 MDN 指针事件文档在跨浏览器发布前始终建议用 CanIUse 查询所用 API 的兼容性。10. 课程作业本课的课后作业是 DOM 元素调研作业英文版见 assignment.md到 MDN 的 DOM 接口列表中“领养”一个接口例如Element.classList、Document.querySelector、IntersectionObserver、MutationObserver等在真实网站上找到它的使用场景撰写 300–500 字的分析技术概述 真实实现分析 与 Terrarium 所用指针拖拽方案的对比并附上一个带注释、可运行、连接真实用例的示例代码。这份作业旨在把本课“只拖动元素”的窄切口扩展到对整个 DOM API 家族的系统认知。11. 本课要点回顾知识点本课实现参考位置DOM 元素定位document.getElementById(plant1..14)solution/script.js 第 3–16 行脚本加载时机script src./script.js defer保证 HTML 先于 JS 就绪solution/index.html 第 13 行闭包隔离状态dragElement每次调用生成独立的pos1–pos4solution/script.js 第 23–28 行指针事件生命周期onpointerdown→onpointermove→onpointerup三段式solution/script.js 第 29–63 行阻止默认行为e.preventDefault()防止拖动时选中文本/默认拖图同上第 32 行增量坐标数学pos1 pos3 - e.clientX等四次重算 offsetTop/offsetLeft写回同上第 46–56 行事件清理stopElementDrag将 document 上的 move/up 置null同上第 59–63 行CSS 定位前提.plant { position: absolute }使top/left/offsetTop同坐标系solution/style.css 第 33–38 行到这里你已用闭包 指针事件 增量坐标计算纯手工实现了一个跨鼠标与触屏的拖拽系统——这正是看板卡片、文件拖放上传、图片墙排序等大量现代 Web 交互背后的同一套原理。【免费下载链接】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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门