事件处理程序
有 3 种分配事件处理程序的方式:
- HTML 特性(attribute):οnclick=“…”。
- DOM 属性(property):elem.onclick = function。
- 方法(method):elem.addEventListener(event, handler[, phase]) 用于添加,removeEventListener 用于移除。
HTML 特性很少使用,因为 HTML 标签中的 JavaScript 看起来有些奇怪且陌生。而且也不能在里面写太多代码。
DOM 属性用起来还可以,但我们无法为特定事件分配多个处理程序。在许多场景中,这种限制并不严重。DOM 属性的方式只是标准的 html attribute on<event>
解析到了 dom 对象上成为了属性。所以本质这两种方式都是一样的。
最后一种方式是最灵活的,但也是写起来最长的。并且有些事件只能使用这种方式。例如 transtionend 和 DOMContentLoaded。
注意:html 中添加函数的时候需要带()
,而 dom 属性上添加的时候不需要带,因为它就是纯粹的 js 操作。
函数名带括号为函数调用,attribute 上就是要进行函数调用。因为特性 on 解析成 js 是这样的:
<script>
function fn() {...}
</script>
<button onclick="fn()"></button>
<script>
button.onclick = function() { fn() };
// 如果标签上不带括号就会变成这样,反而执行不了了。
button.onclick = function() { fn };
</script>
addEventListener
element.addEventListener(event, handler[, options]);
- event 事件名,例如:“click”。
- handler 处理程序。
- options 具有以下属性的附加可选对象:
- once:如果为 true,那么会在被触发后自动删除监听器。
- capture:事件处理的阶段,我们稍后将在 冒泡和捕获 一章中介绍。由于历史原因,options 也可以是 false/true,它与 {capture: false/true} 相同。
- passive:如果为 true,那么处理程序将不会调用 preventDefault(),我们稍后将在 浏览器默认行为 一章中介绍。
要移除处理程序,可以使用 removeEventListener
:
addEventListener 也支持对象作为事件处理程序。在这种情况下,如果发生事件,则会调用对象中的 handleEvent 方法。
<button id="elem">Click me</button>
<script>
let obj = {
handleEvent(event) {
alert(event.type + " at " + event.currentTarget);
}
};
elem.addEventListener('click', obj);
</script>
对象中也可以添加多个事件的事件处理程序,handleEvent 方法中调用其他方法就行,具体不赘述了。
事件对象 event
无论你如何分类处理程序 —— 它都会将获得一个事件对象作为第一个参数。该对象包含有关所发生事件的详细信息。
event 对象的一些常见属性:
- event.type
- 事件类型,这里是 “click”。
- event.currentTarget
- 处理事件的元素。这与 this 相同,除非处理程序是一个箭头函数,或者它的 this 被绑定到了其他东西上,之后我们就可以从 event.currentTarget 获取元素了。
- event.clientX / event.clientY
- 指针事件(pointer event)的指针的窗口相对坐标。
事件冒泡、捕获
当一个事件发生时 —— 发生该事件的嵌套最深的元素被标记为“目标元素”(event.target)。
- 然后,事件从文档根节点向下移动到 event.target,并在途中调用分配了 addEventListener(…, true) 的处理程序(true 是 {capture: true} 的一个简写形式)。
- 然后,在目标元素自身上调用处理程序。
- 然后,事件从 event.target 冒泡到根,调用使用 on、HTML 特性(attribute)和没有第三个参数的,或者第三个参数为 false/{capture:false} 的 addEventListener 分配的处理程序。
每个处理程序都可以访问 event 对象的属性:
- event.target —— 引发事件的层级最深的元素。
- event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
- event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。
任何事件处理程序都可以通过调用 event.stopPropagation() 来停止事件,但不建议这样做,因为我们不确定是否确实不需要冒泡上来的事件,也许是用于完全不同的事情。
捕获阶段很少使用,通常我们会在冒泡时处理事件。这背后有一个逻辑。
在现实世界中,当事故发生时,当地警方会首先做出反应。他们最了解发生这件事的地方。然后,如果需要,上级主管部门再进行处理。
事件处理程序也是如此。在特定元素上设置处理程序的代码,了解有关该元素最详尽的信息。特定于 的处理程序可能恰好适合于该 ,这个处理程序知道关于该元素的所有信息。所以该处理程序应该首先获得机会。然后,它的直接父元素也了解相关上下文,但了解的内容会少一些,以此类推,直到处理一般性概念并运行最后一个处理程序的最顶部的元素为止。
事件委托
捕获和冒泡允许我们实现最强大的事件处理模式之一,即 事件委托 模式。
这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。
在处理程序中,我们获取 event.target 以查看事件实际发生的位置并进行处理。
额外补充:
event.target
属性指向触发事件的DOM元素,即事件发生时被点击的元素。event.currentTarget
属性指向绑定事件的DOM元素,即事件被绑定在哪个元素上。
实践过程中,事件委托非常适合 action in markup 的设计思想。这个思想就是利用 html 标签的灵活性,通过增添自定义的 attribute 来标记元素标签,js 再根据这些标记来区分并使用不同的操作。这种思想也是一种设计组件的思想。
例如,我们想要编写一个有“保存”、“加载”和“搜索”等按钮的菜单。并且,这里有一个具有 save、load 和 search 等方法的对象。如何匹配它们?
第一个想法可能是为每个按钮分配一个单独的处理程序。但是有一个更优雅的解决方案。我们可以为整个菜单添加一个处理程序,并为具有方法调用的按钮添加 data-action 特性(attribute):
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // 修改this从指向dom到指向Menu对象
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
浏览器默认行为
有很多默认的浏览器行为:
- mousedown —— 开始选择(移动鼠标进行选择)。
- 在 上的 click —— 选中/取消选中的 input。
- submit —— 点击 或者在表单字段中按下 Enter 键会触发该事件,之后浏览器将提交表单。
- keydown —— 按下一个按键会导致将字符添加到字段,或者触发其他行为。
- contextmenu —— 事件发生在鼠标右键单击时,触发的行为是显示浏览器上下文菜单。
有两种方式来告诉浏览器我们不希望它执行默认行为:
- 主流的方式是使用 event 对象。有一个 event.preventDefault() 方法。
- 如果处理程序是使用 on(而不是 addEventListener)分配的,那返回 false 也同样有效。
自定义事件
DOM 上的事件
我们知道整个 DOM 存在一个内建类的继承树,事件也是其中的一部分。内建事件类的根是内建的 Event 类。
我们就可以通过它来创造自定义事件:
let event = new Event(type[, options]);
- type —— 事件类型,可以是像这样 “click” 的字符串,或者我们自己的像这样 “my-event” 的参数。
- options —— 具有两个可选属性的对象:
- bubbles: true/false —— 如果为 true,那么事件会冒泡。
- cancelable: true/false —— 如果为 true,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么。
- 默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}。
绑定事件到元素上
内建的事件,它们就已经绑定到元素上了,我们只需要直接监听事件就可以。但是自定义事件肯定是没有绑定到所有元素上的,所以我们需要将自定义事件绑定到某个元素上,然后才能在这个元素上监听使用它。
elem.dispatchEvent(event)
将事件绑定到元素上。
比如创建一个 click 自定义事件,效果就和内建的 click 事件一样。
<button id="elem" onclick="alert('Click!');">Autoclick</button>
<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>
UI 事件
UI 事件和 DOM 事件有点不一样,它是类似鼠标这样外设的一些行为。如果我们想要创建这样的事件,我们应该使用 UI 事件的内建类而不是 new Event。例如,new MouseEvent("click")
。
具体不赘述了。
完全的自定义事件
对于我们自己的全新事件类型,例如 “hello”,我们应该使用 new CustomEvent
。从技术上讲,CustomEvent 和 Event 一样。除了一点不同。
在第二个参数(对象)中,我们可以为我们想要与事件一起传递的任何自定义信息添加一个附加的属性 detail。detail 中可以有任何数据。所有处理程序可以以 event.detail
的形式来访问它。
- 这个 detail 的存在我猜测就是vue 自定义事件传值的底层原理。
<h1 id="elem">Hello for John!</h1>
<script>
// 事件附带给处理程序的其他详细信息
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "John" }
}));
</script>
UI 事件
鼠标移动,拖拽,指针。键盘按键,滚动等事件。具体不赘述了。
表单事件
聚焦、失焦
在元素获得/失去焦点时会触发 focus 和 blur 事件。
它们的特点是:
- 它们不会冒泡。但是可以改为在捕获阶段触发,或者使用 focusin/focusout。
- 大多数元素默认不支持聚焦。使用
tabindex
可以使任何元素变成可聚焦的。
可以通过 document.activeElement
来获取当前所聚焦的元素。
change、input
两个都会在表单元素值发生改变时触发,对于文本输入框有一点区别是: input 像节流,change 事件像防抖。
input 输入就会触发,change 要鼠标失焦后才会触发。比如我输入 ikun,input 就会立即触发 4 次,但 change不一定触发,因为焦点还在输入框内,输入光标还在闪动,鼠标点击其他区域后,输入框失焦,change 触发。
cut、copy、paste
剪贴/拷贝/粘贴行为。行为可以被阻止。event.clipboardData 属性可以用于访问剪贴板。除了火狐(Firefox)之外的浏览器都支持 navigator.clipboard。
表单提交
提交表单到服务器有两种方式:
- 第一种 —— 点击 或 。
- 第二种 —— 在 input 字段中按下 Enter 键
- 按回车,这其实是在 上触发了一次 click 事件。
这两个行为都会触发表单的 submit 事件。处理程序可以检查数据,如果有错误,就显示出来,并调用 event.preventDefault()
,这样表单就不会被发送到服务器了。
另外表单自身上还有一个form.submit()
方法,这个方法允许从 JavaScript 启动表单发送,不用通过表单的 submit 事件。
我们可以使用此方法动态地创建表单,并将其发送到服务器。
- 注意:动态创建的表单也必须插入文档中才能提交
let form = document.createElement('form');
form.action = 'https://google.com/search';
form.method = 'GET';
form.innerHTML = '<input name="q" value="test">';
// 该表单必须在文档中才能提交
document.body.append(form);
form.submit();
页面生命周期事件
四个事件
HTML 页面的生命周期包含三个重要事件:
DOMContentLoaded
—— 浏览器已完全加载 HTML,并构建了 DOM 树,但像 和样式表之类的外部资源可能尚未加载完成。load
—— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。beforeunload
/unload
—— 当用户正在离开页面时。
每个事件都是有用的:
- DOMContentLoaded 事件 —— DOM 已经就绪,因此事件处理程序可以查找 DOM 节点,并初始化接口。
- load 事件 —— 外部资源已加载完成,样式已被应用,图片大小也已知了。
- beforeunload 事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。
- unload 事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作,例如发送统计数据
页面生命周期过程
- 当 DOM 准备就绪时,document 上的 DOMContentLoaded 事件就会被触发。在这个阶段,我们可以将 JavaScript 应用于元素。
- 诸如 之类的脚本会阻塞 DOMContentLoaded,浏览器将等待它们执行结束。
- 图片和其他资源仍然可以继续被加载。
- 当页面和所有资源都加载完成时,window 上的 load 事件就会被触发。我们很少使用它,因为通常无需等待那么长时间。
- 当用户想要离开页面时,window 上的 beforeunload 事件就会被触发。如果我们取消这个事件,浏览器就会询问我们是否真的要离开(例如,我们有未保存的更改)。
- 当用户最终离开时,window 上的 unload 事件就会被触发。在处理程序中,我们只能执行不涉及延迟或询问用户的简单操作。正是由于这个限制,它很少被使用。我们可以使用
navigator.sendBeacon
来发送网络请求。
document.readyState
是文档的当前状态,可以在 readystatechange
事件中跟踪状态更改:
- loading —— 文档正在被加载。
- interactive —— 文档已被解析完成,与 DOMContentLoaded 几乎同时发生,但是在 DOMContentLoaded 之前发生。
- complete —— 文档和资源均已加载完成,与 window.onload 几乎同时发生,但是在 window.onload 之前发生。
加载脚本 async,defer
html 加载 js 脚本会立即下载并执行完后,才能继续下面的 html 解析,
这会导致两个重要的问题:
- 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
- 如果页面顶部有一个笨重的脚本,它会“阻塞页面”。在该脚本下载并执行结束前,用户都不能看到页面内容
为了解决这个问题,之前都是把脚本在文档最后面引入,但不够方便。因此
两者都能让浏览器异步下载脚本,不阻塞文档解析,让用户早点看到画面。
两者区别:
async:脚本下载完就执行,所以和 dom 构建完成事件DOMContentLoaded
没有固定先后关系。多个 async 引入的脚本也是先到先得。
defer:脚本下载完后,等 dom 树构成后才执行,但是会在DOMContentLoaded
事件之前执行。多个 defer 引入的脚本会保持相对顺序。
实际应用:
- defer 用于需要整个 DOM 的脚本或脚本的相对执行顺序很重要的时候。
- async 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。
动态加载外部资源
onload、onerror
浏览器允许我们跟踪任何 src 外部资源的加载 —— 脚本,iframe,图片等。
这里有两个事件:
- onload —— 成功加载
- onerror —— 出现 error
比如动态加载脚本:
let script = document.createElement('script');
script.src = "http://xxx.xxx.xxx/my.js";
script.onload = function() {
// 加载完了,才能使用 my.js 中的函数
}
document.head.append(script);
这两个事件适用任何外部资源,唯一的例外是 :出于历史原因,不管加载成功还是失败,即使页面没有被找到,它都会触发 load 事件。
跨域
加载外部资源很容易出现跨域。
要允许跨源访问,
crossorigin
的属性值,三个不同的跨域级别:
- script 标签没有 crossorigin 字段:—— 不允许跨域
crossorigin="anonymous"
:—— 如果服务器的响应带有包含 * 或我们的源(origin)的 header Access-Control-Allow-Origin,则允许访问。浏览器不会将授权信息和 cookie 发送到远程服务器。crossorigin="use-credentials"
: —— 如果服务器发送回带有我们的源的header Access-Control-Allow-Origin
和Access-Control-Allow-Credentials: true
,则允许访问。浏览器会将授权信息和 cookie 发送到远程服务器。
https://zh.javascript.info/