You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vanilla JS实现Etch Sketch时mousedown/mouseup事件触发异常求助

解决Etch Sketch项目中事件监听器重复触发导致的卡顿问题

问题根源

你的核心问题是事件监听器被重复绑定,而非事件冒泡:

  • 在createGrid的双层循环中,每个单元格创建时都会给gridContainer添加新的mousedown和mouseup监听器。比如创建100×100网格,就会生成10000个mousedown和10000个mouseup监听器,每次鼠标按下/抬起都会触发所有这些监听器,直接导致日志爆炸和页面卡顿。
  • 每次mousedown触发时,还会给当前单元格绑定新的mousemove监听器,多次点击后同一个单元格会堆积大量监听器,进一步拖慢性能。
  • 滑块调整网格时,wipeGrid仅清空DOM,但之前绑定的监听器不会被移除,新创建网格又会添加更多监听器,形成恶性循环最终导致页面冻结。

解决方案

重构代码,减少重复绑定,采用更高效的事件委托模式:

  1. 将鼠标状态(mouseDown)设为全局变量,统一管理
  2. 仅在页面初始化时给gridContainer绑定一次mousedown和mouseup监听器
  3. 使用事件委托处理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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 17:40:27