件处理
客户端js程序采用异步事件驱动编程模型。在这种情况下当文档,浏览器,元素发生一些事情的时候,会产生事件。
举例 当浏览器加载完文档以后会触发一个事件。该事件会有一个函数进行处理,即回调函数
这种只不单单用于web界面,所有使用图形界面的应用程序都采用了这种方式。
事件分类
依赖于设备的输入事件
有些事件和特定输入设备直接相关。比如鼠标和键盘。
touchmove 当触点在触控平面上时发生该事件
独立于设备的输入事件
click事件表示激活了链接的事件。通过鼠标,按钮或者移动设备上的触摸触发该事件
用户界面事件
通常用于HTML表单元素,包括文本输入域获取键盘焦点的focus事件,提交按钮将会触发submit事件
状态变化事件
不是由用户活动由网络或者浏览器活动触发,表示某种生命周期或相关状态的变化。
online 返回浏览器的联网状态
特定的api事件
一些web api会有自己的事件类型
拖放的api dragstart 当用户拖动一个元素,或者选择一个文本的时候触发该事件
计时器和错误处理程序
计时器在指定的时间后触发该事件,错误处理程序,try catch 对应于一个响应,会有异步进行抛出
传统事件类型
表单事件
当提交表单和重置表单时会触发submit和reset事件,当用户和类按钮(包括单选和复选)交互的时,将会发生click事件,当用户输入文字,选择选项或选择复选框改变相应的表单元素的状态时,将会触发change事件,通过键盘改变焦点的表单元素在得到和失去焦点时将会触发focus和blur事件。
通过事件处理程序能取消submit和reset事件的默认操作。某些click事件也是如此,focus和clur事件不会冒泡,但其他所有表单事件都可以。
无论用户何时输入文字,都会触发input事件。
window事件
window事件是指事件的发生与浏览器窗口本身而非窗口中显示的任何特定文档内容相关。
load事件
load事件与文档和其所有外部资源(图片)完全加载并显示给用户时将会触发。
unload事件
unload事件,当用户离开当前文档转向其他文档时将会触发。
unload事件处理程序可以用于保存用户的状态,但其不能取消用户转向其他地方。
beforeunload事件
此事件将会询问用户是否确定离开当前页面。如果beforeunload的回调函数返回一个字符串,那么在新页面加载之前,字符串将会出现在展示给用户确认的对话框上,这样用户将会有机会取消跳转停留在当前页上
注意;该事件仅仅是在当前页面的跳转更改等,转换标签不会触发该事件
onerror属性
此为一个window对象的属性。在js出错的时候将会触发其
其他
像img元素这样的替换元素也能为其注册load和error事件处理程序。当外部资源完全加载或发生阻塞加载错误时将会触发该事件。某些浏览器也支持about事件,当图强因为用户停止加载进程而导致失败的时候也会触发该事件。
focus和blur事件也为window事件,当浏览器窗口从操作系统中得到或失去键盘焦点的时候将会触发该事件
当用户调整浏览器窗口大小或滚动其会触发resize和scroll事件,scroll事件也能在任何可以滚动的文档元素上触发,例如css的的overflow属性也能触发。
鼠标事件
当用户在文档上移动和单击鼠标时都会产生鼠标事件。这些事件在鼠标指针所对应的最深嵌套元素上触发。但其会冒泡直到文档的最顶层。
clientX和clientY属性指定了鼠标在窗口坐标中的位置。button和which属性指定了按下的鼠标键是哪个。
当鼠标辅助键按下的时候,对应的属性为altkey和ctrlkey和metakey和shiftkey会设置为true,对于click事件,detail属性指定了其是单击,双击,还是三击。
每当用户移动和拖动鼠标时,会触发mousemove事件,当用户按下或释放鼠标按键的时候触发mousedown和mouseup事件。
在mousedown和mouseup事件队列之后,浏览器也会触发click事件,如果用户在很短的时间内单击两次鼠标,则第二个事件为dblclic事件,当单击鼠标右键时,浏览器会显示上下文菜单,在显示菜单之前,也会触发contextmenu事件,如果取消这个事件将会阻止菜单的显示,该事件为获得鼠标右击通知的最简单方法。
当用户移动鼠标指针从而使它悬停到新元素上时,浏览器就会在该元素上触发mouseover事件,当鼠标移动指针从而使它不在悬停在某个元素上时,浏览器会触发mouseout事件,(该事件有relatedTarget属性指明这个过程涉及的其他元素)
当用户滚动鼠标的时候,浏览器触发mousewheel事件,传递事件对象属性指定轮子转动的大小和方向。
键盘事件
当键盘聚焦到web浏览器时,用户每次按下或释放键盘上的按键时都会产生事件,键盘快捷键叶同样能被浏览器和操作系统吃掉,此时对js事件处理程序不可见,无论任何文档元素获取键盘焦点都会触发键盘事件,并会冒泡到window对象,
触摸屏和移动设备事件
用户旋转设备的时会产生orientationchange事件,有一个缩放和旋转手势,当手势开始时将会生成getsturestart事件,手势结束时将会生成gestureend事件。在这两个事件之间是跟踪手势过程的gesturechange事件队列,将事件传递的事件对象属性为scale和rotation
握紧手势的scale值小于1.0
撑开手势的scale的值大于1.0
rotation为事件开始时手指旋转的角度。以度为单位正值表示顺时针方向旋转
当手指触摸屏幕的时候将会触发touchstart事件,当手指移动时会触发touchmove事件,当手指离开屏幕时会触发touchend事件,触摸事件传递的事件对象有一个changedTouches属性,该属性为一个类数组对象,其每个元素都描述触摸的位置。
当设备允许用户从竖屏旋转到横屏模式时会在window对象上触发orientationchanged事件。该对象的orientation属性能给出当前方位,其值为0, 90, 180, 或 -90
注册事件处理程序
设置js对象属性为事件处理程序
事件处理程序属性的名字由on后面跟着事件名组成。onclick,onchange,onload,onmouseover
onload 当对象的资源被加载的时候,该对象的onload事件将会被触发,然后将表单的提交的onsubmit事件和一个处理函数进行绑定
onsubmit 在表单提交的时候,将会触发该事件
下方栗子演示一个提交的时候表单验证的过程
其中validate函数为一个自定义的表单验证函数,其函数的参数this指向elt
window.onload = () => {
// 查找一个<form>元素
var elt = document.getElementById("shipping_address");
// 注册事件处理程序函数
// 在表单提交之前调用该函数
elt.onsubmit = () => { return validate(this); };
}
设置HTML标签属性为事件处理程序
<button onclick="alert('Thank you');">点击这里</button>
点击按钮会弹出一个对话框
addEventListener()
为事件绑定一个处理的函数
<script>
var b = document.getElementById("my button");
b.onclick = () => { alert("Thanks for clicking me!"); };
b.addEventListener("click", () => { alert("Thanks again!"); }, false); // 最后一个参数为是否进行冒泡
</script>
上方会弹出两个对话框,一个触发了onclick事件,一个触发了addEventListener注册的click事件。
注册的click处理的函数,将会按照注册的顺序依次不断的调用。(可以注册多个处理程序)。
document.removeEventListener("mousemove", handleMouseMove, true);
移出在mousemove上注册的事件,处理函数为handLenMouseMove,在事件上直接注册,没有冒泡的过程。
事件处理程序的调用
事件处理程序运行的环境
this关键字指的是事件的目标。
事件处理程序的作用域
这是个坑的集散地,表单上有一个HTML事件处理程序想要引用window的location对象,必须使用window.location,不能使用location,因为该location为表单作用域链上的location
事件处理程序的返回值
如果返回false则告诉浏览器不要执行这个事件的默认操作。
调用顺序
通过设置对象属性,HTML属性注册的处理程序一直优先调用
使用addEventList注册的处理程序,按照DOM的顺序进行调用
使用attachEvent()注册的事件按照随机的顺序调用
事件传播
当事件目标为window对象或其他一些单独对象,浏览器会简单的调用对象上的处理程序响应事件。
调用目标元素上注册的事件处理函数
在调用目标元素上注册的事件处理函数的时候,该事件会冒泡到DOM树的树跟,调用目标的父元素的事件处理程序,即可以在共同的祖先元素上注册一个处理程序来处理所有的事件
例如:在form元素上注册change事件处理程序取代在表单的每个元素上注册change事件处理程序。
原因:冒泡
注意:load事件,(load当资源加载完成以后,将会触发load事件,不单单指整个文档)其会在Document对象上停止冒泡不会传播到window对象,只有整个文档都加载完成的时候将会触发window对象的load事件
捕获
事件的第一阶段 捕获 发生在目标处理程序调用程序之前 addEventener()把一个布尔值作为其第三个参数,如果为true那么事件处理程序被注册为捕获事件处理程序,会在事件传播的第一个阶段调用。事件传播的捕获阶段像是反向的冒泡,,将会最先调用window对象的捕获处理程序,然后将会调用document对象的捕获处理程序,接着是body对象的捕获处理程序,逐渐按照DOM树往下,直到调用事件目标的父元素的捕获事件捕获。
事件取消
通过调用事件对象的preventDefault()方法取消事件的默认操作。
文档加载事件
web应用需要web浏览器通知它们文档加载完毕和为操作准备就绪的时间。
当文档准备就绪的时候调用函数
事件冒泡和传播
事件冒泡属于微软的,向上
事件传播属于网景浏览器(怀旧,一个时代,可惜已经不存在了),正好相反。
后来w3c将这两种给统一了,规定任何事件首先向下传播直到遇到目标元素,如果没有遇到冒泡元素,将会不断的向上冒泡进行返回。
件被看作是 JavaScript 与网页之间交互的桥梁,当事件发生时,可以通过 JavaScript 代码(函数)执行相关的操作。例如,用户可以通过鼠标拖曳登录框,改变登录框的显示位置;或者在阅读文章时,选中文本后自动弹出分享、复制选项。本章将对 DOM 中的事件进行详细讲解。
事件可被理解为是 JavaScript 侦测到的行为,这些行为指的就是页面的加载、鼠标单击页面、鼠标滑过某个区域、按下键盘等具体的动作,它对实现网页的交互效果起着重要的作用。在深入学习事件时,需要对一些非常基本又相当重要的概念有一定的了解。
事件处理程序指的就是 JavaScript 为响用户行为所执行的程序代码。例如,用户单击 button 按钮时,这个行为就会被 JavaScript 中的click 事件侦测到;然后让其自动执行,为 click 事件编写的程序代码,如在控制台输出“按钮被单击了”。
事件驱动是指,在 Web 页面中 JavaScript 的事件,侦测到的用户行为(如鼠标单击、鼠标移入等),并执行相应的事件处理程序的过程。
事件绑定指的是为某个元素对象的事件绑定事件处理程序。在 DOM 中提供了3种事件的绑定方式。下面将针对以3种事件绑定方式的语法以及各自的区别进行详细讲解。
事件的行内绑定式是通过HTML标签的属性设置实现的,具体语法格式如下。
<div onclick="alert('handle click')"></div>在上述语法中,div 可以是任意的HTML标签,如 <button>、<input>标签等;事件是由 on 和事件名称组成的一个 HTML 属性,如单击事件对应的属性名为 onclick;事件的处理程序指的是 JavaScript 代码,如匿名函数等。
需要注意的是,由于开发中提倡 JavaScript 代码与 HTML 代码相分离。因此,不建议使用行内绑定事件。
动态的绑定方式很好地解决了JavaScript代码与HTML代码混合编写的问题。在JavaScript代码中,为需要事件处理的 DOM 元素对象,添加事件与事件处理程序。具体语法格式如下。
div.onclick = function handleClick () {
console.log('handle click');
};在上述语法中,事件的处理程序一般都是匿名函数或有名的函数。在实际开发中,相对于行内绑定来说,事件的动态绑定的使用居多。
行内绑定与动态绑定除了实现的语法不同以外,本质是相同的,都是给 DOM 元素绑定了一个 onclick 属性。
为了给同一个 DOM 对象的同一个事件添加多个事件处理程序,DOM中引入了事件流的概念,可以让DOM对象通过事件监听的方式实现事件的绑定。由于不同浏览器采用的事件流实现方式不同,事件监听的实现存在兼容性问题。通常根据浏览器的内核可以划分为两大类,一类是早期版本的IE浏览器(如IE6~8),一类遵循W3C标准的浏览器(以下简称标准浏览器)。
接下来,将根据不同类型的浏览器,分别介绍事件监听的实现方式。
(1)早期版本的IE浏览器
在早期版本的IE浏览器中,事件监听的语法格式如下。
DOM对象.attachEvent(type,callback);在上述语法中,参数 type 指的是为 DOM 对象绑定的事件类型,它是由 on 与事件名称组成的,如 onclick。。参数 callback 表示事件的处理程序。
(2)标准浏览器
标准浏览器包括IE8版本以上的IE浏览器(如IE9~11),新版的Firefox、Chrome等浏览器。具体语法格式如下。通过这种方式我们可以给元素注册多个事件处理函数,而 btn.onclick = fn 是赋值操作只能设置一个事件处理函数。
DOM对象.addEventListener(type, callback, [capture]);在上述语法中,参数 type 指的是 DOM 对象绑定的事件类型,它是由事件名称设置的,如 click。参数 callback 表示事件的处理程序。参数 capture 默认值为 false,这个属性后面单独介绍,一般情况我们都使用它的默认值。
现在 IE 浏览器已经被淘汰,所以我们不需要再去记忆 attachEvent() 的用法,但是我们需要了解过去,过去在使用这种方式注册事件的时候需要处理浏览器的兼容性,下面我们演示下:
function addEventListener(element, type, listener) {
// 能力检测: 就是要看当前的浏览器是否支持此标签对象的属性或是方法
if (element.addEventListener) {
element.addEventListener(type, listener, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, listener);
} else {
element['on' + type] = listener;
}
}在事件处理函数中的 this 指向当前触发该事件的 DOM 元素。
link.onclick = function handleLink () {
photo.src = this.href;
p.textContent = this.title;
return false;
};但是通过行内绑定注册的事件,调用的函数中 this 指向的是 window。
<button onclick="handle()">按钮</button>
<script>
function handle () {
// 此处的 this 指向 window
console.log(this);
}
</script>行内绑定事件 onclick="handle()" 中的 "" 双引号内部其实可以看做是一个匿名函数,"" 双引号内部的这个匿名函数才是事件处理函数,在事件处理函数中又调用了 handle() 方法。
<!-- 此处的 this 指向的是触发事件的对象 button -->
<button onclick="handle(this)">按钮</button>
<script>
function handle (btn) {
// 此处的 this 指向 window
console.log(btn);
}
</script>绑定事件的元素可以解除绑定,例如:我们可以让按钮点击一次之后解除事件绑定。三种绑定事件的解除事件的方式不同,下面我们分别来介绍。
行内绑定事件和动态绑定事件本质上都是给 DOM 元素设置 onclick 属性,对应的解除绑定事件的方式都是把 onclick 属性重新设置为 null。
当按钮执行完点击事件的处理程序后立即解除事件的绑定
<button onclick="handle(this)">按钮</button>
<script>
function handle (btn) {
alert('Hello');
btn.onclick = null;
}
</script>btn.onclick = function handle () {
this.onclick = null;
};标准绑定事件使用 addEventListener(type, callback, [capture]); 方法,对应的解除绑定使用的方法是 removeEventListener(type, callback, [capture]),需要注意的是,如果注册的事件需要解除的话,使用 addEventListener() 注册事件的时候,传入的 callback 不能是匿名函数,因为解除事件绑定的时候还需要引用这个函数。
const div = document.querySelector('#div');
div.addEventListener('click', handle);
function handle () {
alert('hello');
this.removeEventListener('click', handle);
}我们已经会使用 addEventListener(type, callback, [capture]),方法给元素注册事件,但是这个方法的第三个参数的作用我们还不清楚,下面我们就来介绍该方法的第三个参数,这里我们需要先来学习 DOM 中的事件流(事件模型)。
DOM (文档对象模型)结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素结点与根节点之间按特定的顺序传播,路径所经过的节点都会收到该事件,这个传播过程可称为 DOM 事件流。
事件顺序有两种类型:事件捕捉和事件冒泡。
这是 IE 浏览器对事件模型的实现,也是最容易理解的。冒泡,顾名思义,事件像个水中的气泡一样一直往上冒,直到顶端。
从DOM 树型结构上理解,就是事件由叶子节点沿祖先结点一直向上传递直到根节点;从浏览器界面视图 HTML 元素排列层次上理解就是事件由具有从属关系的触发事件的元素一直传递到根元素直到文档对象。
addEventListener(type, callback, [capture]),该方法的第三个参数为 false 的时候设置触发事件的方式为事件冒泡,该参数默认为 false。
一般情况下,我们都会使用事件冒泡的方式注册事件。
const outer = document.querySelector('#outer');
const inner = document.querySelector('#inner');
outer.addEventListener('click', function () {
console.log('点击了 outer');
}, false);
inner.addEventListener('click', function () {
console.log('点击了 inner');
}, false);
document.body.addEventListener('click', function () {
console.log('点击了 body');
}, false);
document.addEventListener('click', function () {
console.log('点击了 document');
}, false);
window.addEventListener('click', function () {
console.log('点击了 window');
}, false);执行结果:
使用行内绑定和动态绑定事件的方式默认使用的是事件冒泡。
Netscape 的实现,它与冒泡型刚好相反,由 DOM 树最顶层元素一直到触发事件的元素。
addEventListener(type, callback, [capture]),该方法的第三个参数为 true 的时候设置触发事件的方式为事件捕获。
const outer = document.querySelector('#outer');
const inner = document.querySelector('#inner');
outer.addEventListener('click', function () {
console.log('点击了 outer');
}, true);
inner.addEventListener('click', function () {
console.log('点击了 inner');
}, true);
document.body.addEventListener('click', function () {
console.log('点击了 body');
}, true);
document.addEventListener('click', function () {
console.log('点击了 document');
}, true);
window.addEventListener('click', function () {
console.log('点击了 window');
}, true);执行结果:
使用行内绑定和动态绑定事件的方式无法使用事件捕获。
我们已经对上面两个不同的事件模型进行了解释和对比。DOM 标准同时支持两种事件模型,即事件捕获与事件冒泡,但是,事件捕获先发生。两种事件流都会触发 DOM 中的所有对象,从 document对象开始,也在 document 对象结束(大部分兼容标准的浏览器会继续将事件是捕捉/冒泡延续到window 对象)。
如图:首先是捕获传递事件,接着是冒泡传递,所以,如果一个处理函数既注册了捕获型事件的监听,又注册冒泡型事件监听,那么在 DOM 事件模型中它就会被调用两次。
DOM 标准的事件模型最独特的性质是,文本节点也会触发事件(在IE不会)。
事件委托,通俗地来讲,就是把一个元素的处理事件的函数委托到另一个元素。
一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
举个例子,比如一个宿舍的同学同时快递到了,一种方法就是他们都傻傻地一个个去领取,还有一种方法就是把这件事情委托给宿舍长,让一个人出去拿好所有快递,然后再根据收件人一一分发给每个宿舍同学。
在这里,取快递就是一个事件,每个同学指的是需要响应事件的 DOM 元素,而出去统一领取快递的宿舍长就是代理的元素,所以真正绑定事件的是这个元素,按照收件人分发快递的过程就是在事件执行中,需要判断当前响应的事件应该匹配到被代理元素中的哪一个或者哪几个。
下面我们来做一个练习,为下面的每一个 li 注册点击事件,当点击当前 li 的时候打印 li 中的文本内容。
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>首先我们用传统的方式来实现,先获取到页面上所有的 li,然后遍历所有的 li,给每一个 li 注册点击事件,这里使用 addEventListener() 注册事件的时候省略了第三个参数,默认为 false,事件冒泡的方式。
这样做不好的地方有两点,第一:我们需要为每一个 li 元素创建一个新的事件处理函数,每次创建都需要销毁时间和内存。第二:当点击按钮往 ul 中添加新的 li 元素的时候需要给新创建的 li 注册点击事件。
const lis = document.querySelectorAll('#list li');
lis.forEach(function (li) {
li.addEventListener('click', function () {
console.log(this.textContent)
});
});下面我们使用事件委托的方式优化上面的代码,把点击事件注册给父元素 ul,当点击 li 的时候通过事件冒泡把点击事件传递给父元素 ul。
const ul = document.querySelector('#list');
ul.addEventListener('click', function () {
console.log('test');
// 此处的 this 是注册事件的元素 ul
console.log(this);
});代码改完之后点击 li,这段代码确实可以执行,但是我们的目标是打印 li 之间的内容,而通过打印发现此处的 this 不是我们想要的当前点击的 li,而是注册事件的元素 ul。所以这里需要强调一点,在注册事件的时候,事件源是注册事件的对象。
那如何获取当前触发事件的元素 li 呢?当事件被触发的时候事件处理函数会接收一个参数,这个参数叫做事件对象,事件对象可以提供触发事件的时候相关的数据,下一小节详细介绍,这里我们先用事件对象解决当前的问题,事件对象中有一个 target 属性,这个属性就是当前触发事件的对象。
在 IE 浏览器中获取事件对象的方式不同,IE 中是通过 window.event 获取事件对象,以前在获取事件对象的时候还要处理浏览器兼容性问题,IE 浏览器现在已经被淘汰所以浏览器兼容性的处理我们就不再演示。
const ul = document.querySelector('#list');
// 事件参数(对象) e
ul.addEventListener('click', function (e) {
// e.target 触发事件的元素
console.log(e.target.textContent);
// 注册事件的元素
console.log(this);
});到这里这个案例就完成了,我们再来扩展下这个案例,如果想要点击特定的 li 来触发事件该如何实现?
<ul id="list">
<li>item 1</li>
<li class="cls">item 2</li>
<li class="cls">item 3</li>
......
<li>item n</li>
</ul>如上代码,如果想点击具有特性类样式或者特定 id 的元素触发事件,可以通过判断当前点击的元素 e.target 的类样式或者 id 属性进行判断。
if (e.target.className === 'cls') {
// ....
}但是如果想像 CSS 选择器一样更加灵活的匹配的话,上面的判断不够灵活,这里可以使用 元素.matches(选择器) 来匹配特定元素。当元素匹配指定的选择器返回 true。
const ul = document.querySelector('#list');
ul.addEventListener('click', function (e) {
// matches 方法,当元素匹配指定的选择器返回 true
if (e.target.matches('.cls')) {
console.log(e.target.textContent);
}
});利用事件冒泡的特性,将本应该注册在子元素上的处理事件注册在父元素上,这样点击子元素时发现其本身没有相应事件就到父元素上寻找作出相应。这样做的优势有:1. 减少内存消耗,避免重复创建相同事件处理函数,只需要把多个子元素的事件委托给父元素。2.随时可以添加子元素,添加的子元素会自动有相应的处理事件。
const tbody = document.querySelector('tbody');
tbody.addEventListener('click', function (e) {
// 注册事件的元素 tbody
// console.log(this);
// 触发事件的元素(你点击的那个元素)
// console.log(e.target)
// 判断元素是否是指定的元素
// console.log(e.target.matches('.del'))
if (e.target.matches('.del')) {
e.target.parentNode.parentNode.remove();
}
});每当触发一个事件,就会产生一个事件对象 event,该对象包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。上一小节中我们使用事件对象获取触发事件的元素。
例如:鼠标操作产生的 event中会包含鼠标位置的信息;键盘操作产生的event中会包含与按下的键有关的信息。
所有浏览器都支持 event 对象,但支持方式不同,在标准 DOM 中 event 对象必须作为唯一的参数传给事件处理函数,在 IE 中 event 是 window 对象的一个属性。
成员 | 描述 | 备注 |
type | 触发的事件名称 | |
eventPhase | 事件流在传播阶段的位置 | |
target | 触发事件的元素 | |
srcElement | target 的别名,老版本的 IE 中使用 | |
clientX / clientY | 基于浏览器的可视区域,鼠标坐标值 | 可配合固定定位,基于窗口定位 |
pageX / pageY | 基于整个页面,页面滚动有关,鼠标在页面的坐标值 | 可配合绝对定位,基于页面定位 |
key | 获取按键输入 | |
preventDefault() | 取消默认行为 | |
stopPropagation() | 阻止事件冒泡 |
const img = document.querySelector('#img');
document.addEventListener('mousemove', function (e) {
// 鼠标位置 - 图片大小的一半
img.style.left = e.clientX - 96 / 2 + 'px';
img.style.top = e.clientY - 80 / 2 + 'px';
});设置样式,让 body 的高度等于 1500px(垂直方向出现滚动条),滚动条下拉这时候移动鼠标,图片的纵向位置跟鼠标脱离。
原因是 clientX 和 clientY 获取的是鼠标在当前可视区域的位置。如果出现滚动条的话可以通过 pageX 和 pageY 获取鼠标在当前文档中的位置。
const img = document.querySelector('#img');
document.addEventListener('mousemove', function (e) {
img.style.left = e.pageX - 96 / 2 + 'px';
img.style.top = e.pageY - 80 / 2 + 'px';
});这里获取图片大小的时候写的是具体值,将来图片替换后,还需要改变这里的大小。我们可以使用 getComputedStyle() 获取图片的大小。
const img = document.querySelector('#img');
img.addEventListener('load', function () {
const style = window.getComputedStyle(img, null);
const imgWidth = parseInt(style.width);
const imgHeight = parseInt(style.height);
document.addEventListener('mousemove', function (e) {
img.style.left = e.pageX - imgWidth / 2 + 'px';
img.style.top = e.pageY - imgHeight / 2 + 'px';
});
});注意:这里需要在 img 标签加载完毕后获取图片的大小,否则获取到的图片大小是 0,因为 load 事件代表图片被加载,否则的话代码从上到下执行到这个位置,图片还没有被下载回来,这个时候获取图片的大小是 0。
#img {
width: 100px;
position: absolute;
left: 0;
top: 0;
}
.toLeft {
transform: rotateY(180deg);
}const img = document.querySelector('#img');
let x = 0;
let y = 0;
document.addEventListener('keydown', function (e) {
switch (e.key) {
case 'ArrowLeft':
x -= 10;
img.classList.add('toLeft');
break;
case 'ArrowRight':
x += 10;
img.classList.remove('toLeft');
break;
case 'ArrowUp':
y -= 10;
break;
case 'ArrowDown':
y += 10;
break;
}
img.style.left = x + 'px';
img.style.top = y + 'px';
});// contextmenu 鼠标右键事件
document.addEventListener('contextmenu', function(e) {
// 禁止点击的默认行为,即显示上下文菜单
e.preventDefault()
});
// 禁止选中文字事件
document.addEventListener('selectstart', function(e) {
// 禁止选中文字的默认行为,即不能选中文字
e.preventDefault()
})const loginBg = document.querySelector('#bg');
const loginLink = document.querySelector('#link');
const loginBox = document.querySelector('#login');
const closeBtn = document.querySelector('#closeBtn');
const loginTitle = document.querySelector('#title');
loginLink.addEventListener('click', function () {
loginBox.style.display = 'block';
loginBg.style.display = 'block';
});
closeBtn.addEventListener('click', function () {
loginBox.style.display = 'none';
loginBg.style.display = 'none';
});
// 拖动事件的三个过程:鼠标按下 mousedowm,鼠标移动 mousemove,鼠标松开 mouseup
const style = window.getComputedStyle(loginBox, null);
// 模态框跟着鼠标走的原理
loginTitle.addEventListener('mousedown', function (e) {
const loginLeft = parseInt(style.left);
const loginTop = parseInt(style.top);
// 步骤一:当鼠标按下时,需要立即得到鼠标在盒子中的坐标
var x = e.pageX - loginLeft;
var y = e.pageY - loginTop;
// 为整个页面添加鼠标移动事件
document.addEventListener('mousemove', move);
function move(e) {
// 步骤二:模态框的left和top等于鼠标在页面的坐标减去鼠标在盒子内的坐标
// 注意:一定要加上px
login.style.left = e.pageX - x + 'px';
login.style.top = e.pageY - y + 'px';
}
// 步骤三:鼠标松开时取消整个页面的鼠标移动事件
document.addEventListener('mouseup', function (e) {
document.removeEventListener('mousemove', move);
});
});在这之前我们已经使用过了单击事件、鼠标经过和鼠标离开的事件,浏览器给我们提供的事件种类非常多,下面我们列出一些常用的事件,使用的方式都是一样的。
描述 | 事件名称 |
鼠标单击 | click |
鼠标双击 | dblclick |
鼠标移入 | mouseover |
鼠标移出 | mouseout |
鼠标移动 | mousemove |
获取焦点 | focus |
失去焦点 | blur |
键盘按下 | keydown |
键盘弹起 | keyup |
不能识别功能键 ctrl、alt 等 | keypress |
文本框的输入事件 | input |
const search = document.querySelector('#search');
function hanldeFocus(e) {
if (e.key === 's') {
search.focus();
e.preventDefault();
}
}
document.addEventListener('keydown', hanldeFocus);
search.addEventListener('focus', function () {
document.removeEventListener('keydown', hanldeFocus);
});
search.addEventListener('blur', function () {
document.addEventListener('keydown', hanldeFocus);
});么是事件
我想你很可能听说过事件驱动, 但是事件驱动到底是什么?为什么说浏览器是事件驱动的呢?为什么 NodeJS 也是事件驱动的 ? 两者是一回事么?
实际上不管是浏览器还是 Nodejs 都是事件驱动的,都有自己的事件模型。在这里,我们只讲解浏览器端的事件模型,如果对 Nodejs 事件模型感兴趣的,请期待我的 Nodejs 部分的讲解。
事件驱动通俗地来说就是什么都抽象为事件。
浏览器依靠事件来驱动APP运行下去,如果没有了事件驱动,那么APP会直接从头到尾运行完,然后结束,事件驱动是浏览器的基石。
本篇文章不讲解事件循环的内容,事件循环部分会在本章的其他章节讲解,敬请期待。
一个简单的例子
其实现实中的红绿灯就是一种事件,它告诉我们现在是红灯状态,绿灯状态,还是黄灯状态。 我们需要根据这个事件自己去完成一些操作,比如红灯和黄灯我们需要等待,绿灯我们可以过马路。
下面我们来看一个最简单的浏览器端的事件:
html代码:
<button>Change color</button>
js代码:
var btn = document.querySelector('button');
btn.onclick = function() { console.log('button clicked')}
代码很简单,我们在button上注册了一个事件,这个事件的handler是一个我们定义的匿名函数。当用户点击了这个被注册了事件的button的时候,这个我们定义好的匿名函数就会被执行。
如何绑定事件
我们有三种方法可以绑定事件,分别是行内绑定,直接赋值,用addEventListener。
这个方法非常不推荐
html代码:
<button onclick="handleClick()">Press me</button>
然后在script标签内写:
function handleClick() { console.log('button clicked')}和我上面举的例子一样:
var btn = document.querySelector('button');
btn.onclick = function() { console.log('button clicked')}这种方法有两个缺点
btn.onclick = functionA;btn.onclick = functionB;
这样只有functionB有效,这可以通过addEventListener来解决。
因此addEventListener横空出世,这个也是目前推荐的写法。
旧版本的addEventListener第三个参数是bool,新版版的第三个参数是对象,这样方便之后的扩展,承载更多的功能, 我们来重点介绍一下它。
addEventListener可以给Element,Document,Window,甚至XMLHttpRequest等绑定事件,当指定的事件发生的时候,绑定的回调函数就会被以某种机制进行执行,这种机制我们稍后就会讲到。
语法:
target.addEventListener(type, listener[, options]); target.addEventListener(type, listener[, useCapture]); target.addEventListener(type, listener[, useCapture, wantsUntrusted ]); // Gecko/Mozilla only
type是你想要绑定的事件类型,常见的有click, scroll, touch, mouseover等,旧版本的第三个参数是bool,表示是否是捕获阶段,默认是false,即默认为冒泡阶段。新版本是一个对象,其中有capture(和上面功能一样),passive和once。 once用来执行是否只执行一次,passive如果被指定为true表示永远不会执行preventDefault(),这在实现丝滑柔顺的滚动的效果中很重要。更多请参考Improving scrolling performance with passive listeners
框架中的事件
实际上,我们现在大多数情况都是用框架来写代码,因此上面的情况其实在现实中是非常少见的,我们更多看到的是框架封装好的事件,比如React的合成事件,感兴趣的可以看下这几篇文章。
虽然我们很少时候会接触到原生的事件,但是了解一下事件对象,事件机制,事件代理等还是很有必要的,因为框架的事件系统至少在这方面还是一致的,这些内容我们接下来就会讲到。
事件对象
所有的事件处理函数在被浏览器执行的时候都会带上一个事件对象,举个例子:
function handleClick(e) { console.log(e);}
btn.addEventListener('click', handleClick);这个e就是事件对象,即event object。 这个对象有一些很有用的属性和方法,下面举几个常用的属性和方法。
事件传播
前面讲到了事件默认是绑定到冒泡阶段的,如果你显式令useCapture为true,则会绑定到捕获阶段。
事件捕获很有意思,以至于我会经常出事件的题目加上一点事件传播的机制,让候选人进行回答,这很能体现一个人的水平。了解事件的传播机制,对于一些特定问题有着非常大的作用。
一个Element上绑定的事件触发了,那么其实会经过三个阶段。
从最外层即HTML标签开始,检查当前元素有没有绑定对应捕获阶段事件,如果有则执行,没有则继续往里面传播,这个过程递归执行直到触达触发这个事件的元素为止。
伪代码:
上面已经提到了,这里省略了。
从触发这个事件的元素开始,检查当前元素有没有绑定对应冒泡阶段事件,如果有则执行,没有则继续往里面传播,这个过程递归执行直到触达HTML为止。
伪代码:
上述的过程用图来表示为:
如果你不希望事件继续冒泡,可以用之前我提到的stopPropagation。
伪代码:
事件代理
利用上面提到的事件冒泡机制,我们可以选择做一些有趣的东西。 举个例子:
我们有一个如下的列表,我们想在点击对应列表项的时候,输出是点击了哪个元素。
HTML代码:
<ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li></ul>
JS代码:
document.querySelector('ul').addEventListener('click',
e => console.log(e.target.innerHTML))
在线地址
上面说了addEventListener会默认绑定到冒泡阶段,因此事件会从目标阶段开始,向外层冒泡,到我们绑定了事件的ul上,ul中通过事件对象的target属性就能获取到是哪一个元素触发的。
“事件会从目标阶段开始”,并不是说事件没有捕获阶段,而是我们没有绑定捕获阶段,我描述给省略了。
我们只给外层的ul绑定了事件处理函数,但是可以看到li点击的时候,实际上会打印出对应li的内容(1,2,3或者4)。 我们无须给每一个li绑定事件处理函数,不仅从代码量还是性能上都有一定程度的提升。
这个有趣的东西,我们给了它一个好听的名字“事件代理”。在实际业务中我们会经常使用到这个技巧,这同时也是面试的高频考点。
总结
事件其实不是浏览器特有的,和JS语言也没有什么关系,这也是我为什么没有将其划分到JS部分的原因。很多地方都有事件系统,但是各种事件模型又不太一致。
我们今天讲的是浏览器的事件模型,浏览器基于事件驱动,将很多东西都抽象为事件,比如用户交互,网络请求,页面加载,报错等,可以说事件是浏览器正常运行的基石。
我们在使用的框架都对事件进行了不同程度的封装和处理,除了了解原生的事件和原理,有时候了解一下框架本身对事件的处理也是很有必要的。
当发生一个事件的时候,浏览器会初始化一个事件对象,然后将这个事件对象按照一定的逻辑进行传播,这个逻辑就是事件传播机制。 我们提到了事件传播其实分为三个阶段,按照时间先后顺序分为捕获阶段,目标阶段和冒泡阶段。开发者可以选择监听不同的阶段,从而达到自己想要的效果。
事件对象有很多属性和方法,允许你在事件处理函数中进行读取和操作,比如读取点击的坐标信息,阻止冒泡等。
最后我们通过一个例子,说明了如何利用冒泡机制来实现事件代理。
本文只是一个浏览器事件机制的科普文,并没有也不会涉及到很多细节。希望这篇文章能让你对浏览器时间有更深的理解,如果你对nodejs时间模型感兴趣,请期待我的nodejs事件模型。 事件循环和事件循环也有千丝万缕的联系,如果有时间,我会出一篇关于时间循环的文章。
*请认真填写需求信息,我们会在24小时内与您取得联系。