进阶篇:有特性的一些事件
有些事件表面普通,行为却和直觉不一致:离开页面前的确认、表单回车隐式提交、触摸点挪出元素后仍打在原目标上。把这些边界搞清楚,比再记一批 API 名称更有用。
onbeforeunload 事件
当浏览器窗口关闭或者刷新时,会触发 beforeunload 事件。事件使网页能够触发一个确认对话框,询问用户是否真的要离开该页面。如果用户确认,浏览器将导航到新页面,否则导航将会取消。
根据规范,要显示确认对话框,事件处理程序需要在事件上调用 preventDefault()。
但是请注意,并非所有浏览器都支持此方法,而有些浏览器需要事件处理程序实现两个遗留方法中的一个作为代替:
- 将字符串分配给事件的 returnValue 属性
- 从事件处理程序返回一个字符串。
window.addEventListener('beforeunload', (event) => {
// 规范:调用 preventDefault()
event.preventDefault()
// 部分浏览器仍要求设置 returnValue(如 Chromium)
event.returnValue = '' // 任意字符串,自定义文案已不再显示
// 注意:addEventListener 里 return 字符串通常无效;写在 onbeforeunload 属性处理函数里才是遗留做法
})
某些浏览器过去在确认对话框中显示返回的字符串,从而使事件处理程序能够向用户显示自定义消息。但是,此方法已被弃用,并且在大多数浏览器中不再支持。
为避免意外弹出窗口,除非页面已与之交互,否则浏览器可能不会显示在 beforeunload 事件中创建的提示,甚至根本不会显示它们。
将事件处理程序/监听器加到 window或 document 的 beforeunload 事件后,将阻止浏览器使用内存中的页面导航缓存。
HTML 规范指出在此事件中调用 window.alert()、window.confirm()、window.prompt() 方法,可能会失效。
拦截「浏览器后退」要用 popstate,不是 beforeunload。常见做法如下(与关闭标签无关):
history.pushState(null, null, document.URL)
window.addEventListener('popstate', function () {
if (true) {
// 回退
history.go(-1)
return
}
// 阻止回退
history.pushState(null, null, document.URL)
})
onsubmit 事件
React、Vue 等框架流行后,Web 开发者越来越少直接操作 form 表单了。这些框架一般都有状态管理,form 表单元素值的变化,可以实时反应在对应的数据模型中,开发者可以绕过 onsubmit 事件,通过 AJAX 将数据发送到服务器。
这种操作,在逻辑上来说,没有问题。但从体验上来说,并不友好。它忽略了 onsubmit 本身的一些特性。
<form>
<label for="name">姓名:</label>
<input type="text" name="name" id="name">
<input type="submit">
</form>
如上代码,如果您在文本框中获得焦点并按回车键,表单将自动提交。
基本上,如果用户在文本字段获得焦点时按下回车键,浏览器应该找到表单中的第一个提交按钮并单击它。这种行为在所有浏览器中都是一致的,称为隐式提交。隐式提交对于辅助技术和根本无法使用鼠标的受损用户至关重要。
隐式提交的一些交互逻辑:
- 如果表单中,没有提交按钮,并且只有一个表单元素,则将进行隐式提交。
- 如果表单中,没有提交按钮,但是有多个表单元素,则不会进行隐式提交。
- 如果表单中,有一个或多提交按钮,则第一个按钮会进行隐式提交。
这里说的提交按钮,可以是 <input type="submit">、<button>提交</button>、<input type="image"> 中的任意一个。(注意:IE7 有一个错误,button 元素默认的 type 值为 button 而不是 submit。历史兼容需要写成 <button type="submit">。现行浏览器默认已是 submit。)
在 JavaScript 中防止隐式提交也很容易:
document.getElementById('name').addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault()
}
})
keypress 与 keyCode 已弃用,现行用 keydown 和 event.key。
请注意:
提交按钮的
onclick事件与表单的onsubmit事件是不一样的,并且在不同的浏览器中它们的执行顺序不一。form 元素可以用
submit()方法提交表单,该方法不会触发submit事件。现行可用requestSubmit(),它会走约束验证并触发submit。
隐式表单提交对于辅助技术和重要的用户便利性至关重要,建议提交表单时,尽量采用 onsubmit 事件,它将有助于使网络成为对每个人都更好的地方。
touchmove 事件
touchmove 事件的目标元素和触发 touchstart 事件的目标元素相同,即使当 touchmove 事件触发时,触点已经移出了该元素 。如果要实时获取触摸点所在位置的元素,普通方法已不奏效。
解决方案一(推荐):
// 原生事件用 changedTouches;若在 jQuery 里则是 e.originalEvent.changedTouches[0]
const myLocation = e.changedTouches[0];
const realTarget = document.elementFromPoint(myLocation.clientX, myLocation.clientY);
注意: elementFromPoint() 返回的是最顶层的元素,所以要将需要的元素的 z-index 设置为最高。
注意: 若要在监听里调用 preventDefault() 以阻止滚动,现代浏览器里 touchmove 默认可能是被动监听,需显式 { passive: false },否则 preventDefault() 无效。
el.addEventListener('touchmove', handler, { passive: false })
解决方案二: 在父元素上定义 touchmove 事件。
touchend 事件
部分旧版安卓机中,touchend 事件无效。
解决方案一:在 touchstart 或者 touchmove 事件的监听函数中,阻止事件的默认行为 event.preventDefault()(同样可能需要 { passive: false }),那么到 touchend 事件就能正常触发。
注意: 该方案解决了 touchend 触发问题,但页面默认滚动会被阻止。
解决方案二:同时绑定 touchcancel 和 touchend 事件。
可以缓解 touchend 不触发的问题,但并非所有机型都彻底修好。这是旧版 Android WebView 的历史问题,现行 Chromium 内核已少见。
相关问题
如何移除一个匿名函数的绑定事件?
JavaScript 用 addEventListener 给 DOM 绑定事件,用 removeEventListener 移除绑定事件。
const ckHandler = function () {}
element.addEventListener('click', ckHandler, false)
element.removeEventListener('click', ckHandler)
由上可知,移除绑定事件的函数,需要提供函数名,如果是匿名函数,该如何移除呢?
element.addEventListener('click', function () {
console.log('如何移除事件绑定的匿名函数?')
}, false)
方法一: 在绑定的匿名函数中添加移除函数。
arguments.callee 在严格模式中不可用。只触发一次时,现行优先用 { once: true }。
let count = 0
element.addEventListener('click', function handler(e) {
console.log('如何移除事件绑定的匿名函数?')
count ++
if (count > 2) {
element.removeEventListener(e.type, handler, false);
}
}, false)
element.addEventListener('click', onClick, { once: true })
方法二: 缓存匿名函数。
window.pro_elt_addEventListener=Element.prototype.addEventListener;
Element.prototype.addEventListener=function(){
if(!this.eventList) {
this.eventList={};
}
if(!this.eventList[arguments[0]]) {
this.eventList[arguments[0]]=[];
}
this.eventList[arguments[0]].push(arguments[1]);
window.pro_elt_addEventListener.apply(this,arguments);
};