本文深入探讨JavaScript中e.preventDefault()的具体使用及其对事件默认行为的影响。通过对比MDN文档描述与实战案例,解析为何父组件调用该方法会阻断子组件的默认滚动等行为。文章结合马拉松比喻阐明事件监听与默认行为的区别,并详细区分了e.preventDefault()与return false在不同事件绑定方式下的适用场景,帮助开发者准确处理移动端触摸事件及键盘事件的默认行为。
背景介绍
在近期项目开发中,团队遇到一个典型问题:当引入某个组件作为根组件后,原有子组件的移动端滚动功能失效。该项目的滚动交互依赖于touchmove事件触发。经过排查发现,新引入的根组件内部调用了e.preventDefault()来阻止touchmove事件的默认行为。虽然同事通过阻止事件冒泡暂时解决了滚动失效问题,但这引发了对preventDefault()机制的深入思考。
核心疑问
为何在父组件中调用preventDefault()阻止默认事件,会导致子组件的默认行为也随之失效?
文档分析
查阅MDN官方文档,繁体中文版对event.preventDefault()的解释较为简略,仅指出该方法用于取消事件的预设行为,且不影响事件的传递过程。从字面理解,这似乎意味着事件仍会向上传播,但默认动作被取消。
然而,简体中文版MDN对该事件的描述更为详尽,提供了更多上下文信息。
其中提到了“显示处理”这一概念,对于该术语的具体含义尚存疑惑。尽管繁体版与简体版的描述在逻辑上可以对应,但这并未直接解答核心疑问:为什么父元素调用此方法会使得子元素的默认行为消失?
进一步探索
经过广泛搜索,未找到直接针对此特定现象的权威解答。因此,决定通过实际测试与理论推导来寻找结论。
实验验证
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#box{
height: 300px;
overflow: auto;
width:200px;
padding: 40px;
margin: 0 auto;
background: grey;
}
#gdfather{
position: relative;
height: 600px;
overflow: auto;
width:400px;
margin: 40px;
background: rgba(0, 255, 255, 0.219);
}
#father{
position: absolute;
top: 20px;
height: 200px;
overflow: auto;
width: 600px;
margin: 40px;
background-color: rgba(128, 128, 128, 0.349);
}
#son{
position: absolute;
top: 20px;
height: 1400px;
width: 800px;
margin: 40px;
background-color: rgba(205, 92, 92, 0.26);
}
#box2{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height:200px;
width: 200px;
background-color: khaki;
}
</style>
</head>
<body>
<div id='box'>
<div id='gdfather'>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<p>祖先中的内容</p>
<div id='father'>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<p>父亲中的内容</p>
<div id='son'>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
<p>儿子中的内容</p>
</div>
</div>
</div>
</div>
<div id='box2'>
<input id="ipt" type="text">
<div id='inbox'>
<input id='inIpt' type="text">
</div>
</div>
<script>
let gdfather=document.getElementById('gdfather')
let father=document.getElementById('father')
let son=document.getElementById('son')
gdfather.addEventListener('touchmove',(e)=>{
// console.log('gdfather')
// e.preventDefault()
})
father.addEventListener('touchmove',(e)=>{
// console.log('father')
console.log(e.preventDefault.toString())
console.log(e)
})
let box2=document.getElementById('box2')
let inbox=document.getElementById('inbox')
let ipt=document.getElementById('ipt')
let inIpt=document.getElementById('inIpt')
let events
box2.addEventListener('keydown',(e)=>{
// e.preventDefault()
console.log(e===events)
})
ipt.addEventListener('keydown', (e)=>{
console.log(e===events)
console.log(JSON.stringify(e)===JSON.stringify(events))
events=e
})
inIpt.addEventListener('keydown', (e)=>{
// e.preventDefault()
events.preventDefault()
console.log(events)
})
</script>
</body>
</html>总结与机制解析
首先需要明确,元素本身并不“拥有”事件,而是对事件进行监听。我们可以用马拉松比赛来类比:事件从产生到结束的过程如同选手完成比赛,而沿途设置的补给点则对应各个DOM元素。每个补给点提供的补给象征着事件的默认行为。当选手经过补给点时,系统会执行默认动作(如提供补给)。同时,我们可以在这些点设置监听器(如观众欢呼),这对应于元素绑定的事件处理函数。
实际上,即使是同一个事件,所拥有的event也是不同的,这个在上面的118和119行可以进行证明,而同一类型的事件在触发时只会拥有这一个event,这一点通过118和115可以证明,对应到马拉松就是每一次马拉松都是不同的,而一条路线上只可能进行一场马拉松比赛。当我们阻止事件冒泡时,相当于我们在跑到23公里时,放弃了这场比赛,那么在32公里处等着为我们欢呼的人就永远不会触发这个动作。而我们在使用event.preventDefault()这个方法时,相当于我们撤掉了所有的里程点的补给,自然也就没有了默认行为,但我们还在跑,原来在32公里处为我们欢呼的人依旧会为我们欢呼。
基于上述分析,可以清晰理解为何在父组件中调用preventDefault()会导致子组件默认行为失效:因为默认行为是针对整个事件路径上的“补给”而言的,一旦在路径某处被取消,整条路径上的默认行为均不再执行。
e.preventDefault()与return false的区别
事件处理函数的返回值(return false)仅在通过HTML属性直接注册的事件处理函数中有效。也就是说,如果未使用addEventListener()绑定事件,而是直接在HTML标签中指定onclick等属性,此时禁止默认行为应使用return false。
若使用addEventListener()或attachEvent()等标准方法绑定事件,则必须使用e.preventDefault()方法,或者设置事件对象的returnValue属性为false,以正确阻止默认行为。
至此,关于JavaScript中e.preventDefault()的具体使用及其机制已介绍完毕。更多相关技术细节可参考本站往期文章或继续浏览后续内容,感谢大家的支持。