Vanilla JS实现Etch Sketch时mousedown/mouseup事件触发异常求助
解决Etch Sketch项目中事件监听器重复触发导致的卡顿问题
问题根源
你的核心问题是事件监听器被重复绑定,而非事件冒泡:
- 在
createGrid的双层循环中,每个单元格创建时都会给gridContainer添加新的mousedown和mouseup监听器。比如创建100×100网格,就会生成10000个mousedown和10000个mouseup监听器,每次鼠标按下/抬起都会触发所有这些监听器,直接导致日志爆炸和页面卡顿。 - 每次
mousedown触发时,还会给当前单元格绑定新的mousemove监听器,多次点击后同一个单元格会堆积大量监听器,进一步拖慢性能。 - 滑块调整网格时,
wipeGrid仅清空DOM,但之前绑定的监听器不会被移除,新创建网格又会添加更多监听器,形成恶性循环最终导致页面冻结。
解决方案
重构代码,减少重复绑定,采用更高效的事件委托模式:
- 将鼠标状态(
mouseDown)设为全局变量,统一管理 - 仅在页面初始化时给
gridContainer绑定一次mousedown和mouseup监听器 - 使用事件委托处理
mousemove,直接在gridContainer上监听,判断目标元素为单元格时再上色
修复后的完整代码
JavaScript代码
const gridContainer = document.querySelector(".grid-container"); let cellColor = "black"; const gridWidth = gridContainer.offsetWidth; // 全局维护鼠标按下状态 let mouseDown = false; const wipeGrid = () => { gridContainer.innerHTML = ""; }; const colorCell = (cell, color) => { cell.style.backgroundColor = color; }; const createGrid = (numberOfCells) => { const col = document.createElement("div"); col.className = "col"; for (let y = 0; y < numberOfCells; y++) { const row = document.createElement("div"); row.className = "row"; for (let x = 0; x < numberOfCells; x++) { const cell = document.createElement("div"); cell.style.cssText = `width:${gridWidth / numberOfCells}px; height:${ gridWidth / numberOfCells }px;`; // 用class代替重复的id,id必须唯一 cell.className = "cell"; row.appendChild(cell); } col.appendChild(row); } gridContainer.appendChild(col); }; function ready(callback) { if (document.readyState != "loading") callback(); else if (document.addEventListener) document.addEventListener("DOMContentLoaded", callback); else document.attachEvent("onreadystatechange", function () { if (document.readyState == "complete") callback(); }); } ready(function () { const slider = document.getElementById("slider"); // 仅绑定一次全局鼠标事件 gridContainer.addEventListener("mousedown", () => { mouseDown = true; }); gridContainer.addEventListener("mouseup", () => { mouseDown = false; }); // 事件委托处理鼠标移动上色 gridContainer.addEventListener("mousemove", (e) => { if (mouseDown && e.target.classList.contains("cell")) { colorCell(e.target, cellColor); } }); // 处理鼠标离开容器时的状态重置 gridContainer.addEventListener("mouseleave", () => { mouseDown = false; }); createGrid(1); slider.addEventListener("input", () => { const sliderValue = document.querySelector(".thumb-value"); sliderValue.innerHTML = `${slider.value}`; wipeGrid(); createGrid(slider.value); }); });
调整后的CSS(新增单元格样式)
* { box-sizing: border-box; padding: 0; margin: 0; font-family: 'Roboto', sans-serif; --thumbNumber: '5'; } .h-100vh { height: 100vh; } .center-me { display: flex; align-items: center; justify-content: center; } .grid-container { border: 0.1px solid black; height: 400px; width: 400px; } .col { display: flex; flex-direction: column; } .row { display: flex; } .gap-2em { gap: 2em; } .gap-1em { gap: 1em; } .title { font-size: 7em; margin:auto; } .input-label { font-size: 1.25rem; } .slider { -webkit-appearance: none; width: 100%; height: 25px; border-radius: 5px; background: #d3d3d3; outline: none; opacity: 1; -webkit-transition: .2s; transition: opacity .2s; } .slider:hover { opacity: 0.7; } .slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 25px; height: 25px; border-radius: 5px; background: #1682FF; cursor: pointer; } .slider::-webkit-slider-thumb::before { content: var(--thumbNumber); } .range-div { display:flex; align-items: center; gap:0.25em; } /* 新增单元格样式,可选,让网格更清晰 */ .cell { border: 1px solid #eee; }
HTML代码(无需修改)
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Etch a Sketch</title> <meta name="description" content="" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@900&family=Space+Mono&display=swap" rel="stylesheet" /> <link rel="stylesheet" href="style.css" /> </head> <body> <div class="center-me h-100vh"> <div class="col gap-2em"> <p class="title">Etch a Sketch</p> <div class="row gap-2em"> <div class="grid-container"></div> <div class="col gap-1em"> <div class="slidecontainer col gap-1em"> <label for="slider" class="input-label"> GRID SIZE</label> <div class="range-div"> <div class="thumb-value input-label">1</div> <input name="slider" type="range" min="10" max="100" value="1" class="slider" id="slider" /> </div> </div> </div> </div> </div> </div> <script src="script.js" async defer></script> </body> </html>
额外优化说明
- 添加
mouseleave监听器:当鼠标移出网格容器时自动重置mouseDown状态,避免鼠标在容器外抬起后回到容器仍继续上色的问题。 - 替换重复id:将单元格的
id="cells"改为class="cell",符合DOM规范(id必须唯一),同时配合事件委托实现精准选择。
内容的提问来源于stack exchange,提问作者hasin
相关产品推荐
相关产品推荐

