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
属性,它有几个优势:
- 可以给同一个元素的同一个事件添加多个处理函数(不会被覆盖)。
- 可以灵活地通过
removeEventListener
移除事件监听(避免内存泄漏)。 - 支持事件捕获 / 冒泡的精细控制。
这是前端开发中处理交互的核心方法之一,几乎所有用户操作相关的功能(如表单提交、菜单切换、滚动加载等)都会用到它。
具体分析示例代码(贪吃蛇移动)
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. 为什么键盘输入能驱动代码运行?
整个流程是这样的:
- 你按下键盘上的方向键(比如上箭头)。
- 浏览器检测到按键事件,触发
window
上的keydown
事件。 - 事件处理函数被执行,通过
event.key
判断你按的是哪个键。 - 根据当前的
inputDirection
状态,判断是否允许改变方向(避免 180 度反向,比如正在向上时不能直接向下)。 - 如果允许,就修改
inputDirection
的值(比如改为向上)。 - 你的程序中一定还有另一段代码(比如游戏循环)在不断读取
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
时小蛇会自己动?
结合游戏循环(每帧更新位置):
- 游戏循环每隔一段时间(比如 100ms)执行一次。
- 每次执行时,都会用
当前位置 + inputDirection
计算新位置。 - 初始
inputDirection
是{x:0, y:1}
,所以每帧 Y 坐标都 +1 → 小蛇持续向下移动,看起来就是 "自己爬"。
总结:方向值的本质
inputDirection
里的 x
和 y
其实是移动增量:
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()
的过程:
-
snakeBody.pop();
→ 移除最后一节{x:2, y:4}
,数组变为[{x:2, y:2}, {x:2, y:3}]
。 -
newHead = { ...snakeBody[0] }
→ 复制蛇头,newHead
是{x:2, y:2}
。 -
snakeDirection
获取到{x:0, y:-1}
(向上)。 -
更新
newHead
:x=2+0=2
,y=2+(-1)=1
→ 新头部是{x:2, y:1}
。 -
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
用新方向移动头部→身体跟随,最终形成了蛇的可控移动。