Web 开发 08

1 addEventListener

在前端开发中,addEventListener 是 JavaScript 中用于给 DOM 元素添加事件监听器的方法,它能让你检测并响应用户的交互行为(如点击、输入、鼠标移动等)。

简单来说,它的作用是:告诉浏览器:"当某个元素发生特定事件时,请执行我指定的函数"

基本语法

javascript

javascript 复制代码
元素.addEventListener(事件类型, 处理函数, [是否捕获]);
  • 事件类型 :字符串,如 'click'(点击)、'input'(输入)、'mouseover'(鼠标悬停)等。
  • 处理函数:事件触发时要执行的函数(可以是匿名函数或命名函数)。
  • 是否捕获 :可选参数(布尔值),默认 false(表示事件冒泡阶段触发),一般用默认值即可。

举个例子

比如给一个按钮添加点击事件,点击后弹出提示:

html

预览

html 复制代码
<button id="myBtn">点击我</button>

<script>
  // 获取按钮元素
  const btn = document.getElementById('myBtn');
  
  // 添加点击事件监听器
  btn.addEventListener('click', function() {
    alert('按钮被点击了!');
  });
</script>

当你点击按钮时,就会执行监听器中的函数,弹出提示框。

为什么要用 addEventListener

相比直接在 HTML 中写 onclick 或在 JS 中给元素赋值 onclick 属性,它有几个优势:

  1. 可以给同一个元素的同一个事件添加多个处理函数(不会被覆盖)。
  2. 可以灵活地通过 removeEventListener 移除事件监听(避免内存泄漏)。
  3. 支持事件捕获 / 冒泡的精细控制。

这是前端开发中处理交互的核心方法之一,几乎所有用户操作相关的功能(如表单提交、菜单切换、滚动加载等)都会用到它。

具体分析示例代码(贪吃蛇移动)

javascript 复制代码
window.addEventListener("keydown", (event) => {
  if (event.key === "ArrowUp" && inputDirection.x !== 0) {
    inputDirection = { x: 0, y: -1 };
  } else if (event.key === "ArrowDown" && inputDirection.x !== 0) {
    inputDirection = { x: 0, y: 1 };
  } else if (event.key === "ArrowLeft" && inputDirection.y !== 0) {
    inputDirection = { x: -1, y: 0 };
  } else if (event.key === "ArrowRight" && inputDirection.y !== 0) {
    inputDirection = { x: 1, y: 0 };
  }
});

这段代码是通过键盘方向键控制某个对象(很可能是游戏中的角色,比如贪吃蛇)移动方向的核心逻辑。让我们一步步拆解它的工作原理,理解为什么键盘输入能驱动代码运行:

1. 整体作用

javascript

javascript 复制代码
window.addEventListener("keydown", (event) => { ... })
  • 这行代码给整个浏览器窗口(window)添加了一个键盘按下事件监听器
  • 意思是:当用户在页面上按下任何键盘按键时,就会执行后面的箭头函数(处理逻辑)

2. 事件对象(event)的作用

当你按下键盘时,浏览器会自动创建一个事件对象 (这里用event接收),它包含了按键的所有信息:

  • event.key:表示按下的具体按键(比如 "ArrowUp" 就是上箭头键)。
  • 其他信息:比如是否同时按下了 Shift/Ctrl 键(event.shiftKey)、按键的编码等,但这里我们只需要event.key

3. 核心逻辑:判断按键并修改方向

代码里的inputDirection是一个记录当前移动方向的变量(比如{x: 0, y: -1}表示向上移动),它的结构:

  • x:水平方向(1= 右,-1= 左,0= 不水平移动)
  • y:垂直方向(1= 下,-1= 上,0= 不垂直移动)

四个if/else分别对应四个方向键,逻辑可以总结为:
"当按下某个方向键时,只有当前移动方向与它垂直,才允许改变方向"(比如正在左右移动时,只能上下转向;正在上下移动时,只能左右转向)。

逐个分析:

javascript

python 复制代码
// 按下上箭头键时
if (event.key === "ArrowUp" && inputDirection.x !== 0) {
  inputDirection = { x: 0, y: -1 }; // 改为向上移动(y=-1)
}
  • 条件inputDirection.x !== 0:表示当前正在左右移动(x 是 1 或 - 1)。
  • 此时按下上箭头,就把方向改为向上(y=-1,x=0 表示停止水平移动)。

javascript

javascript 复制代码
// 按下下箭头键时
else if (event.key === "ArrowDown" && inputDirection.x !== 0) {
  inputDirection = { x: 0, y: 1 }; // 改为向下移动(y=1)
}
  • 同样要求当前在左右移动(x≠0),才允许改为向下。

javascript

javascript 复制代码
// 按下左箭头键时
else if (event.key === "ArrowLeft" && inputDirection.y !== 0) {
  inputDirection = { x: -1, y: 0 }; // 改为向左移动(x=-1)
}
  • 条件inputDirection.y !== 0:表示当前正在上下移动(y 是 1 或 - 1)。
  • 此时按下左箭头,就把方向改为向左(x=-1,y=0 表示停止垂直移动)。

javascript

javascript 复制代码
// 按下右箭头键时
else if (event.key === "ArrowRight" && inputDirection.y !== 0) {
  inputDirection = { x: 1, y: 0 }; // 改为向右移动(x=1)
}
  • 同样要求当前在上下移动(y≠0),才允许改为向右。

4. 为什么键盘输入能驱动代码运行?

整个流程是这样的:

  1. 你按下键盘上的方向键(比如上箭头)。
  2. 浏览器检测到按键事件,触发window上的keydown事件。
  3. 事件处理函数被执行,通过event.key判断你按的是哪个键。
  4. 根据当前的inputDirection状态,判断是否允许改变方向(避免 180 度反向,比如正在向上时不能直接向下)。
  5. 如果允许,就修改inputDirection的值(比如改为向上)。
  6. 你的程序中一定还有另一段代码(比如游戏循环)在不断读取inputDirection的值,并根据它来更新对象的位置(比如让贪吃蛇向上移动一格)。

简单说:键盘输入通过addEventListener触发方向修改,而程序的其他部分根据修改后的方向来执行实际的移动逻辑

举个场景例子(比如贪吃蛇)

  • 初始时inputDirection{x: 1, y: 0}(向右移动)。
  • 你按下上箭头:代码检测到event.key是 "ArrowUp",且当前inputDirection.x是 1(≠0),所以允许改方向,inputDirection变成{x: 0, y: -1}(向上)。
  • 游戏循环每 100 毫秒执行一次,读取inputDirection,让蛇的位置在 y 轴减 1(向上移动一格)。
  • 如果你此时再按左箭头:因为当前inputDirection.y是 - 1(≠0),允许改方向,蛇会向左移动。
  • 如果你在向上移动时按向下箭头:因为inputDirection.y≠0(正在垂直移动),条件不满足,方向不会改变(避免蛇突然反向撞自己)。

这种逻辑在需要限制移动方向的游戏中很常见,核心就是通过键盘事件动态修改方向变量,再用这个变量驱动实际的位置更新。

2 为什么y: -1 代表向上?这是屏幕坐标系的规则

要理解为什么 y: -1 代表向上,得从屏幕坐标系的规则说起 ------ 这是所有图形界面(包括游戏、网页)都遵循的底层逻辑,也是你的键盘输入能控制小蛇移动的核心前提。

屏幕坐标系:和数学课本里的不一样

在数学里,坐标系是 Y 轴向上为正(越往上数值越大):

plaintext

复制代码
(0,0) ───→ X轴
  │
  ↓
  Y轴(数值增大)

但在屏幕(电脑、手机屏幕)上,坐标系是 Y 轴向下为正(越往下数值越大):

plaintext

复制代码
(0,0)  屏幕左上角(原点)
  │
  ↓
  Y轴(数值增大)──→ 比如Y=100在Y=50的下方

小蛇的移动逻辑:用坐标差计算位置

小蛇的位置是用 (x, y) 坐标表示的(比如 snake.x = 10, snake.y = 20)。每次移动,其实是在当前坐标上加上方向向量inputDirection)。

假设小蛇当前位置是 (10, 20)

  • inputDirection = {x:0, y:1} 时:
    新位置 = (10+0, 20+1) = (10, 21) → Y 值变大,位置更靠下 → 向下移动
  • inputDirection = {x:0, y:-1} 时:
    新位置 = (10+0, 20-1) = (10, 19) → Y 值变小,位置更靠上 → 向上移动

这就是为什么 y: -1 是向上 ------ 因为它让小蛇的 Y 坐标减小,往屏幕上方 "跑"。

为什么初始方向是 y:1 时小蛇会自己动?

结合游戏循环(每帧更新位置):

  1. 游戏循环每隔一段时间(比如 100ms)执行一次。
  2. 每次执行时,都会用 当前位置 + inputDirection 计算新位置。
  3. 初始 inputDirection{x:0, y:1},所以每帧 Y 坐标都 +1 → 小蛇持续向下移动,看起来就是 "自己爬"。

总结:方向值的本质

inputDirection 里的 xy 其实是移动增量

  • x:1 → 右移(X 坐标增大),x:-1 → 左移(X 坐标减小)
  • y:1 → 下移(Y 坐标增大),y:-1 → 上移(Y 坐标减小)

你的键盘事件代码,本质就是通过按键修改这个增量值,而游戏循环则不断用这个增量更新小蛇位置 ------ 这就是键盘输入能 "驱动" 小蛇动起来的完整逻辑。

3 贪吃蛇更新蛇身核心代码

javascript 复制代码
const updateSnake = () => {
  snakeBody.pop();
  const newHead = { ...snakeBody[0] };
  const snakeDirection = getInputDirection();
  newHead.x += snakeDirection.x;
  newHead.y += snakeDirection.y;

  snakeBody.unshift(newHead);
};

这段代码是贪吃蛇游戏中更新蛇身体位置的核心函数,负责让蛇按照方向移动并保持身体连贯。我们来逐行拆解它的工作原理:

1. 整体作用

updateSnake 是一个箭头函数,它的核心功能是:根据当前移动方向,更新蛇的头部位置,并让身体跟随头部移动(模拟蛇的爬行效果)。

2. 逐行解析

(1)snakeBody.pop();
  • snakeBody 是存储蛇身体所有部分坐标的数组(假设结构类似 [{x:1, y:1}, {x:1, y:2}, {x:1, y:3}],每个对象代表一节身体的位置)。
  • pop() 是数组方法,作用是删除并返回数组的最后一个元素
  • 这里的目的:蛇移动时,尾部会离开原来的位置,所以需要移除最后一节身体(模拟 "向前爬" 的效果)。
(2)const newHead = { ...snakeBody[0] };
  • snakeBody[0] 是蛇的头部(数组的第一个元素永远代表蛇头)。
  • { ...snakeBody[0] }对象解构赋值,作用是复制蛇头当前的坐标(避免直接修改原头部对象)。
  • 此时 newHead 是蛇头位置的副本,接下来会用它计算新的头部位置。
(3)const snakeDirection = getInputDirection();
  • 调用 getInputDirection() 函数(你代码中应该有这个函数),获取当前的移动方向(即之前提到的 inputDirection,比如 {x:0, y:-1} 代表向上)。
  • 这一步是连接 "方向控制" 和 "移动逻辑" 的桥梁 ------ 键盘输入修改的方向会在这里被读取。
(4)更新新头部的位置

javascript

javascript 复制代码
newHead.x += snakeDirection.x;  // 横向移动:加上水平方向增量
newHead.y += snakeDirection.y;  // 纵向移动:加上垂直方向增量
  • 根据方向向量更新头部位置:
    • 比如当前蛇头在 (3,3),方向是 {x:1, y:0}(向右),则新头部为 (4,3)
    • 比如方向是 {x:0, y:-1}(向上),则新头部为 (3,2)(Y 值减小,符合屏幕坐标系规则)。
(5)snakeBody.unshift(newHead);
  • unshift() 是数组方法,作用是在数组的开头添加一个元素
  • 这里将计算好的 newHead 添加到 snakeBody 的最前面,作为新的蛇头。

3. 整体流程演示(以实例说明)

假设蛇初始状态(snakeBody)是:

javascript

复制代码
// 蛇头在(2,2),身体有两节,整体向上移动(方向{y:-1})
[{x:2, y:2}, {x:2, y:3}, {x:2, y:4}]

执行 updateSnake() 的过程:

  1. snakeBody.pop(); → 移除最后一节 {x:2, y:4},数组变为 [{x:2, y:2}, {x:2, y:3}]

  2. newHead = { ...snakeBody[0] } → 复制蛇头,newHead{x:2, y:2}

  3. snakeDirection 获取到 {x:0, y:-1}(向上)。

  4. 更新 newHeadx=2+0=2y=2+(-1)=1 → 新头部是 {x:2, y:1}

  5. snakeBody.unshift(newHead); → 数组开头添加新头部,最终变为:

    javascript

    复制代码
    [{x:2, y:1}, {x:2, y:2}, {x:2, y:3}]

可以看到:蛇头向前移动了一格(Y 从 2→1,向上),身体每一节都 "跟进" 前一节的位置,尾部被移除 ------ 完美模拟了蛇的移动效果。

4. 关键逻辑总结

  • 蛇的移动本质是:头部向前走一步,身体每一节跟随前一节,尾部消失
  • 这个函数通过 pop()(删尾)和 unshift()(加头)实现了身体的 "跟随" 效果。
  • 方向的作用仅体现在头部的位置计算上(newHead.x/y += 方向值),身体的移动是被动跟随。

结合之前的键盘事件代码,整个逻辑链是:键盘输入→修改方向→updateSnake 用新方向移动头部→身体跟随,最终形成了蛇的可控移动。

相关推荐
奕辰杰20 分钟前
关于npm前端项目编译时栈溢出 Maximum call stack size exceeded的处理方案
前端·npm·node.js
JiaLin_Denny2 小时前
如何在NPM上发布自己的React组件(包)
前端·react.js·npm·npm包·npm发布组件·npm发布包
_Kayo_2 小时前
VUE2 学习笔记14 nextTick、过渡与动画
javascript·笔记·学习
路光.3 小时前
触发事件,按钮loading状态,封装hooks
前端·typescript·vue3hooks
我爱996!3 小时前
SpringMVC——响应
java·服务器·前端
咔咔一顿操作4 小时前
Vue 3 入门教程7 - 状态管理工具 Pinia
前端·javascript·vue.js·vue3
kk爱闹4 小时前
用el-table实现的可编辑的动态表格组件
前端·vue.js
漂流瓶jz5 小时前
JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
前端·javascript·编译原理
换日线°5 小时前
css 不错的按钮动画
前端·css·微信小程序