Skip to content

JavaScript教程 - 16 DOM

DOM 继续!

16.12 事件

重新来说回事件。

1 事件对象

什么是事件对象?

浏览器在事件触发时自动创建的一个特殊对象,包含了与该事件相关的所有信息和功能(包括鼠标、键盘等信息)。例如鼠标相关的事件,会包含事件触发时候的鼠标位置。

举个栗子:

鼠标在 div 中移动,获取事件对象。

html
<body>
  <div id="div1" style="width: 100px; height: 100px; background-color: lightblue;"></div>

  <script>
    const div1 = document.getElementById('div1');

    div1.onmousemove = function (event) {
      div1.innerHTML = event.x + ", " + event.y;  // 可以获取事件的详细信息
    };
  </script>
</body>
  • 在上面的代码中,给 div 添加鼠标移动事件回调方法,回调方法的第一个参数就是事件对象。
  • 我们可以从事件对象中获取到鼠标当前的位置,event.xevent.y 表示鼠标相对于浏览器窗口的坐标,event.xevent.y 也可以使用 clientXclientY

运行如下:

同样,通过箭头函数或者 addEventListener 事件监听的方式也可以获取。

js
div1.onmousemove = event => {
  div1.innerHTML = event.x + ", " + event.y;
}

div1.addEventListener("mousemove", function () {
  div1.innerHTML = event.x + ", " + event.y;
});

div1.addEventListener("mousemove", event => {
  div1.innerHTML = event.x + ", " + event.y;
});

2 事件的常用属性

不同的事件,事件对象是不同的,但是它们都继承自 Event 对象。

html
<body>
  <div id="div1" style="width: 100px; height: 100px; background-color: lightblue; margin: 100px;"></div>

  <script>
    const div1 = document.getElementById('div1');

    div1.onclick = function (event) {
      console.log(event.offsetX + ', ' + event.offsetY);  // 鼠标相对于事件目标元素的坐标
      console.log(event.x + ', ' + event.y);  // 鼠标相对于浏览器窗口的坐标
      console.log(event.screenX + ', ' + event.screenY);  // 鼠标相对于屏幕的坐标
      console.log(event.pageX + ', ' + event.pageY);  // 鼠标相对于整个文档的坐标(包含滚动)
    };
  </script>
</body>
  • 上面通过一些方式获取到了鼠标相对于不同对象的位置。

3 事件冒泡

什么是事件冒泡?

事件冒泡是指当 DOM 元素上的事件被触发时,这个事件会从该元素向上传播到它的父元素、祖先元素,直到 document 根节点。

例如,div1 > div2 > div3 三个 div 嵌套,当点击 div3 的时候,会触发 div3 的点击事件,然后点击事件会向上传播给 div2,然后继续传播给 div1

演示一下:

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>For技术栈</title>

  <style>

    #div1 {
      width: 200px;
      height: 200px;
      background-color: lightblue;
    }

    #div2 {
      width: 150px;
      height: 150px;
      background-color: lightgreen;
    }

    #div3 {
      width: 100px;
      height: 100px;
      background-color: pink;
    }
    
  </style>
</head>

<body>
  <div id="div1">
    <div id="div2">
      <div id="div3"></div>
    </div>
  </div>

  <script>
    const div1 = document.getElementById('div1');
    const div2 = document.getElementById('div2');
    const div3 = document.getElementById('div3');

    div1.addEventListener('click', ) = function (event) {
      alert('点击div1');
    };
    div2.onclick = function (event) {
      alert('点击div2');
    };
    div3.onclick = function (event) {
      alert('点击div3');
    };

  </script>
</body>
</html>

显示如下:

当点击 div3 的时候,会依次弹出 点击div3点击div2点击div1 ,一次传递给父元素。

需要注意,事件的冒泡,和事件的订阅是没有关系的,div3没有订阅点击事件,事件也是会传递给 div2 和 div1 的。另外和 CSS 样式无关,div3 在 div2 中,即使通过 CSS 样式,让 div3 显示到 div2 的外面,div3 的样式还是会传递给 div2 和 div1,因为从 HTML 结构上它们是父子关系。

事件的冒泡对于我们的开发是有利的,会简化我们的开发。

当然,我们也可以在 div3 的点击事件中,取消点击事件的冒泡,这样 div2 和 div1 就收不到事件了。

举个例子:

js
div3.onclick = function (event) {
  alert('点击div3');
  event.stopPropagation();  // 停止事件冒泡
};
  • 在 div3 的点击事件处理函数中,可以通过 event.stopPropagation(); 停止事件的冒泡。

4 触发事件的对象

在事件的回调函数中,我们可以获取到触发事件的对象,还可以获取绑定事件的对象。

举个栗子:

如果事件是从 div3 传递给 div1 的,那么在 div1 的回调函数中

js
div1.onclick = function (event) {
  console.log(event.target);  // 触发事件的对象<div id="div3">
  console.log(this);  // 绑定事件的对象,<div id="div1">
  console.log(event.currentTarget); // 绑定事件的对象,同this <div id="div1">
};
  • event.target 表示当前触发事件的对象,事件是 div3 触发的传递给 div1 的,那么这里 target 还是 div3。
  • this(非箭头函数)表示绑定事件的对象,这里是 div1,event.currentTarget 表示绑定事件的对象,所以和 this 相同,如果在 箭头函数中,没有办法获取到 this,那么可以使用 event.currentTarget 获取到绑定事件的对象。
  • 如果在 div3 的事件回调函数中,那么 event.targetthisevent.currentTarget 是相同的。

5 阻止默认行为

<a> 标签在点击的时候,会跳转到 href 属性指定的地址,在讲解 HTML5 的时候说过,可以通过如下方式阻止 <a> 标签的默认行为:

html
<a href="javascript:void(0);">点击我什么都不会发生</a>
<!-- 或者 -->
<a href="javascript:;">点击我什么都不会发生</a>

有时候我们需要通过监听 <a> 的点击事件,做一些处理,那么可以像上面这样禁用 <a> 标签的默认行为。

除了上面这种方式,我们还可以在 <a> 标签的点击事件中,通过 event.preventDefault(); 阻止 <a> 标签的默认行为:

html
<a id="my-a" href="https://www.foooor.com">For技术栈</a>

<script>
const myA = document.getElementById('my-a');
myA.onclick = function(event) {
  event.preventDefault();  // 阻止默认行为

  alert('点击了a标签');
};
</script>

6 事件的委派

什么是事件的委派?

事件委派(Event Delegation)是 JavaScript 中的一种事件处理技巧,它利用事件冒泡,把多个子元素的事件,统一交给父元素来监听和处理,从而减少事件绑定数量、提高性能和灵活性。

举个栗子:

查看一下下面的两种事件绑定方式。

html
<body>
  <ul id="list">
    <li>苹果</li>
    <li>香蕉</li>
    <li>橘子</li>
  </ul>

  <script>
    // 方式一:传统方式
    const liItems = document.querySelectorAll("#list li");
    // 遍历liItems,给每个li添加点击事件
    liItems.forEach((li) => {
      li.addEventListener("click", () => {
        console.log("你点击了", li.textContent);
      });
    });

    // 方式二:事件委派方式
    const listEle = document.getElementById("list");
    // 给ul添加点击事件
    listEle.addEventListener("click", (e) => {
      // 将liItems转换为数组,判断点击的是否是数组中的li
      const liArray = Array.from(liItems);
      if (liArray.includes(e.target)) {
        console.log("你点击了", e.target.textContent);
      }
    });
  </script>
</body>
  • 在上面的代码中,方式一是给所有的元素都绑定事件;
  • 方式二是将元素的绑定事件委托给父元素,当点击子元素的时候,会通过事件冒泡传递给父元素,然后在判断点击的是哪个子元素,这样就减少了事件的绑定数量。

如果我们此时向父元素中动态的添加子元素,通过第二种事件委派的方式,就不需要动态添加的元素添加绑定事件,从而简化了处理。

7 事件的传播机制

事件的传播其实是经历三个阶段的:

  1. 捕获阶段:事件从最外层(window / document)开始,也就是事件一开始是由祖先元素捕获,然后从祖先元素依次向子元素传播,直到传播到目标元素。

  2. 目标阶段:事件到达真正的目标元素,此阶段事件会在目标元素本身触发。

  3. 冒泡阶段:事件从目标元素开始,逐层向上传播回祖先元素,直到 document

也就是事件是由祖先元素捕获,然后传播给目标元素,然后再由目标元素逐层向上传播回祖先元素。

我们在监听事件进行处理的时候,默认是在冒泡阶段触发的,可以通过监听事件的第三个参数,将触发阶段修改到捕获阶段。

举个栗子:

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>For技术栈</title>

  <style>

    #div1 {
      width: 200px;
      height: 200px;
      background-color: lightblue;
    }

    #div2 {
      width: 150px;
      height: 150px;
      background-color: lightgreen;
    }

    #div3 {
      width: 100px;
      height: 100px;
      background-color: pink;
    }
    
  </style>
</head>

<body>
  <div id="div1">
    <div id="div2">
      <div id="div3"></div>
    </div>
  </div>

  <script>
    const div1 = document.getElementById('div1');
    const div2 = document.getElementById('div2');
    const div3 = document.getElementById('div3');

    div1.addEventListener('click', function (event) {
      alert('点击div1');
    }, true);
    div2.addEventListener('click', function (event) {
      alert('点击div2');
    }, true);
    div3.addEventListener('click', function (event) {
      alert('点击div3');
    }, true);

  </script>
</body>
</html>
  • 上面通过设置 addEventListener 第三个参数为 true,让事件在捕获的阶段触发,这样在点击 div3 的时候,会依次弹出 点击div1 > 点击div2 > 点击div3
  • 在捕获阶段,也同样可以通过 event.stopPropagation(); 方法阻止事件的传播。

一般情况下,我们是不需要修改的,除非有特殊需求。

8 常用事件

鼠标事件

可绑定几乎所有可视元素,如按钮、图片、div、a 标签等。

事件名描述示例代码
click单击(左键)button.addEventListener("click", fn)
dblclick双击div.addEventListener("dblclick", fn)
mousedown鼠标按下div.addEventListener("mousedown", fn)
mouseup鼠标释放
mousemove鼠标移动用于绘图、拖动
mouseenter鼠标进入(不冒泡)类似 mouseover,但不涉及子元素
mouseleave鼠标离开(不冒泡)
mouseover鼠标移入(会冒泡)用于提示、浮层
mouseout鼠标移出(会冒泡)
contextmenu鼠标右键可自定义右键菜单

举个栗子:

js
document.querySelector("button").addEventListener("click", () => {
  alert("按钮被点击了");
});

键盘事件

默认可以绑定可以获取到焦点的元素,例如<input><textarea><button>documentwindow 对象。如果是非表单元素,例如 div,需添加 tabindex="0" 属性才可以获得焦点,然后可以接收键盘事件。

事件名描述注意事项
keydown按键按下(包括功能键)最常用,支持组合键
keyup按键释放可用于监听输入完成

举个栗子:

js
document.addEventListener("keydown", (e) => {
  console.log(e.key + ", " + e.keyCode);  // 按下按键会打印按键名字和按键的号码
});

document.addEventListener("keydown", (e) => {
  if (e.ctrlKey && e.key === "s") {  // 同时按下ctrl+s
    e.preventDefault();
    alert("保存命令被拦截");
  }
});

表单事件

可绑定表单类元素,如 <form><input><textarea><select>

事件名描述注意事项
submit表单提交必须阻止默认提交:e.preventDefault()
reset表单重置
change内容改变(失去焦点时才触发比如输入框输入后点到别处才触发
input实时输入(每输入一次就触发一次用于动态校验、计数
focus获取焦点不会冒泡
blur失去焦点不会冒泡

举个栗子:

阻止表单提交:

js
document.querySelector("form").addEventListener("submit", function (e) {
  e.preventDefault();
  alert("提交被拦截");
});

窗口与文档事件

可绑定元素:windowdocument

事件名描述
load页面资源(包括图片)全部加载完
DOMContentLoaded仅 DOM 结构加载完成(推荐)
resize浏览器窗口大小变化
scroll页面或元素滚动

举个例子:

js
window.addEventListener("scroll", function (e) {
  console.log("滚动")
});

剪贴板事件

可绑定任何可编辑元素,如 inputtextarea,或使用 contenteditable="true" 的元素

事件名描述
copy复制时触发
cut剪切时触发
paste粘贴时触发

举个栗子:

禁止粘贴:

js
input.addEventListener("paste", (e) => {
  e.preventDefault();
  alert("禁止粘贴!");
});


下面来做两个练习。

16.13 学生信息表

实现一个能添加和删除学生信息的表格,显示如下:

实现思路:

给添加按钮绑定点击事件,点击按钮的时候,获取到姓名和年龄,然后创建 <tr> 元素添加到表格的 <tbody> 中即可。 <tr> 元素中需要创建三个 <td>,在第三个 <td> 中添加删除按钮,并绑定点击事件,用于删除 <tr>

完整代码如下:

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>学生信息表</title>
  <style>
    table, th, td {
      border: 1px solid black;
      border-collapse: collapse;
    }
    table {
      margin-top: 10px;
      min-width: 600px;
    }
    th, td {
      padding: 5px 10px;
    }
  </style>
</head>
<body>

  <h2>学生信息表</h2>
  姓名:<input type="text" id="name" />
  年龄:<input type="number" id="age" />
  <button id="addBtn">添加</button>

  <table id="studentTable">
    <thead>
      <tr>
        <th>姓名</th>
        <th>年龄</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <!-- 动态插入的学生行在这里 -->
    </tbody>
  </table>

  <script>
    // 名字输入框
    const nameInput = document.getElementById("name");
    // 年龄输入框
    const ageInput = document.getElementById("age");
    // 添加按钮
    const addBtn = document.getElementById("addBtn");
    // 表格的body
    const tbody = document.querySelector("#studentTable tbody");

    // 给添加按钮绑定点击事件
    addBtn.addEventListener("click", function () {
      // 获取到输入框的内容,并去掉前后的空格
      const name = nameInput.value.trim();
      const age = ageInput.value.trim();

      if (!name || !age) {
        alert("请输入完整信息!");
        return;
      }

      // 创建一行tr
      const tr = document.createElement("tr");

      // 创建姓名单元格
      const tdName = document.createElement("td");
      tdName.textContent = name;

      // 创建年龄单元格
      const tdAge = document.createElement("td");
      tdAge.textContent = age;

      // 创建操作单元格和删除按钮
      const tdOp = document.createElement("td");
      const delBtn = document.createElement("button");
      delBtn.textContent = "删除";
      // 给按钮添加绑定事件
      delBtn.addEventListener("click", () => {
        tbody.removeChild(tr);  // 删除tr
      });
      tdOp.appendChild(delBtn);

      // 添加单元格到行中
      tr.appendChild(tdName);
      tr.appendChild(tdAge);
      tr.appendChild(tdOp);

      // 添加行到表格中
      tbody.appendChild(tr);

      // 清空输入框
      nameInput.value = "";
      ageInput.value = "";
    });
  </script>

</body>
</html>

16.14 拖拽

创建一个 div,在页面上可以随意拖拽。

如下图:

实现思路:

  1. 当鼠标按下时,记录鼠标相对于元素的偏移量,在移动的时候,设置鼠标也是在元素的相同位置;

  2. 鼠标移动,元素跟随鼠标移动;

  3. 鼠标抬起时,完成拖拽。

代码如下:

html
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>拖拽练习</title>
  <style>
    #drag-box {
      width: 100px;
      height: 100px;
      background-color: lightblue;
      position: absolute; /* 关键 */
      top: 100px;
      left: 100px;
    }
  </style>
</head>
<body>
  <div id="drag-box">拖我</div>

  <script>
    const box = document.getElementById('drag-box');
    let isDragging = false;
    let offsetX = 0;
    let offsetY = 0;

    box.addEventListener('mousedown', function (e) {
      isDragging = true;
      // 计算鼠标点击位置相对 div 的偏移,用于拖拽的准确位置
      offsetX = e.clientX - box.offsetLeft;
      offsetY = e.clientY - box.offsetTop;
      // 防止选中文字
      e.preventDefault();
    });

    document.addEventListener('mousemove', function (e) {
      if (isDragging) {
        // 实时修改div的位置
        box.style.left = (e.clientX - offsetX) + 'px';
        box.style.top = (e.clientY - offsetY) + 'px';
      }
    });

    document.addEventListener('mouseup', function () {
      isDragging = false;
    });
  </script>
</body>
</html>
内容未完......