JavaScript 文件拖拽上传插件 dropzone.js 介绍
dropzone.js 是一个开源的 JavaScript 库,提供 AJAX 异步上传功能。
安装
下载dropzone.js文件并添加到页面中即可。Dropzone 不依赖 jQuery 框架。
启用
可以新建一个div元素,然后通过如下 JavaScript 代码启用 dropzone(如果你使用
jQuery):
<div id="dropz"></div>
<script>
$("#dropz").dropzone({
url: "handle-upload.php",
maxFiles: 10,
maxFilesize: 512,
acceptedFiles: ".js,.obj,.dae"
});
</script>
如果你没有使用 jQuery 框架,也可以这样来初始化:
<div id="dropz"></div>
<script>
var dropz = new Dropzone("#dropz", {
url: "handle-upload.php",
maxFiles: 10,
maxFilesize: 512,
acceptedFiles: ".js,.obj,.dae"
});
</script>
其中url是必须的值,指明文件上传提交到哪个页面。其他的值都是可选的,如果使用默认值的话可以省略。
接收文件
Dropzone 并不含任何服务器端的支持和实现,利用 Dropzone 上传文件和利用下面基本的 HTML 表单对于服务器来说是一样的:
<form action="handle-upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
</form>
配置 Dropzone
此插件的特色就在于非常灵活,提供了许多可选项、事件等。下面分类介绍常用的配置项。
功能选项
url:最重要的参数,指明了文件提交到哪个页面。
method:默认为post,如果需要,可以改为put。
paramName:相当于<input>元素的name属性,默认为file。
maxFilesize:最大文件大小,单位是 MB。
maxFiles:默认为null,可以指定为一个数值,限制最多文件数量。
addRemoveLinks:默认false。如果设为true,则会给文件添加一个删除链接。
acceptedFiles:指明允许上传的文件类型,格式是逗号分隔的 MIME
type 或者扩展名。例如:image/*,application/pdf,.psd,.obj
uploadMultiple:指明是否允许 Dropzone 一次提交多个文件。默认为false。如果设为true,则相当于
HTML 表单添加multiple属性。
headers:如果设定,则会作为额外的 header 信息发送到服务器。例如:{"custom-header":
"value"}
init:一个函数,在 Dropzone 初始化的时候调用,可以用来添加自己的事件监听器。
forceFallback:Fallback 是一种机制,当浏览器不支持此插件时,提供一个备选方案。默认为false。如果设为true,则强制
fallback。
fallback:一个函数,如果浏览器不支持此插件则调用。
翻译选项
dictDefaultMessage:没有任何文件被添加的时候的提示文本。
dictFallbackMessage:Fallback 情况下的提示文本。
dictInvalidInputType:文件类型被拒绝时的提示文本。
dictFileTooBig:文件大小过大时的提示文本。
dictCancelUpload:取消上传链接的文本。
dictCancelUploadConfirmation:取消上传确认信息的文本。
dictRemoveFile:移除文件链接的文本。
dictMaxFilesExceeded:超过最大文件数量的提示文本。
添加事件监听
如果你希望在一个事件发生时采取一些额外的操作,而不干扰 Dropzone 的默认行为,那么你应该通过添加事件监听器的办法对事件做出响应,而非重写默认事件函数。
重写默认事件函数的例子如下:
$("#dropz").dropzone({
addedfile: function() {
// actions...
}
});
如果你重写默认事件函数,该事件发生时插件默认采取的动作将被覆盖。大多数情况下你仅仅想在事件发生时添加自己的行为,那么应该使用on方法。
jQuery 版本:
$("#dropz").dropzone({
init: function() {
this.on("addedfile", function(file) {
// actions...
});
}
});
非 jQuery 版本:
dropz.on("addedfile", function(file) {
// actions...
});
常用事件
以下事件接收 file 为第一个参数
addedfile:添加了一个文件时发生。
removedfile:一个文件被移除时发生。你可以监听这个事件并手动从服务器删除这个文件。
uploadprogress:上传时按一定间隔发生这个事件。第二个参数为一个整数,表示进度,从
0 到 100。第三个参数是一个整数,表示发送到服务器的字节数。当一个上传结束时,Dropzone 保证会把进度设为 100。注意:这个函数可能被以同一个进度调用多次。
success:文件成功上传之后发生,第二个参数为服务器响应。
complete:当文件上传成功或失败之后发生。
canceled:当文件在上传时被取消的时候发生。
maxfilesreached:当文件数量达到最大时发生。
maxfilesexceeded:当文件数量超过限制时发生。
以下事件接收一个 file list 作为第一个参数(仅当uploadMultiple被设为true时才会发生)
successmultiple
completemultiple
cancelmultiple
特殊事件
totaluploadprogress:第一个参数为总上传进度,第二个参数为总字节数,第三个参数为总上传字节数。
例子
这里我使用上面的选项、事件等写了一个例子,供参考:
<div class="dropz"></div>
<script>
$(".dropz").dropzone({
url: "handle-upload.php",
addRemoveLinks: true,
dictRemoveLinks: "x",
dictCancelUpload: "x",
maxFiles: 10,
maxFilesize: 5,
acceptedFiles: ".js",
init: function() {
this.on("success", function(file) {
console.log("File " + file.name + "uploaded");
});
this.on("removedfile", function(file) {
console.log("File " + file.name + "removed");
});
}
});
</script>
外观
Dropzone 下载之后没有自带任何 CSS 样式(人家只有一个 js 文件嘛)。我觉得官网提供的 Demo 的外观设计就非常不错,可以供大家参考。
其他教程
Dropzone 的作者在插件的 GitHub Wiki 页面上提供了很多额外教程,非常好,也推荐大家看一看。
彻底搞懂JS的事件原理
事件一、EventTarget 接口事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。DOM 支持大量的事件,本章开始介绍 DOM 的事件编程。1、概述DOM 的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequest、AudioNode、AudioContext)也部署了这个接口。该接口主要提供三个实例方法。addEventListener:绑定事件的监听函数removeEventListener:移除事件的监听函数dispatchEvent:触发事件2、EventTarget.addEventListener()EventTarget.addEventListener()用于在当前节点或对象上,定义一个特定事件的监听函数。一旦这个事件发生,就会执行监听函数。该方法没有返回值。target.addEventListener(type, listener[, useCapture]);
// 事件名称,监听函数 [,是否在捕获阶段触发]该方法接受三个参数。type:事件名称,大小写敏感。listener:监听函数。事件发生时,会调用该监听函数。useCapture:布尔值,表示监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分),默认为false(监听函数只在冒泡阶段被触发)。该参数可选。下面是一个例子。function hello() {
console.log('Hello world');
}
var button = document.getElementById('btn');
button.addEventListener('click', hello, false);上面代码中,button节点的addEventListener方法绑定click事件的监听函数hello,该函数只在冒泡阶段触发。关于参数,有两个地方需要注意。首先,第二个参数除了监听函数,还可以是一个具有handleEvent方法的对象。buttonElement.addEventListener('click', {
handleEvent: function (event) {
console.log('click');
}
});上面代码中,addEventListener方法的第二个参数,就是一个具有handleEvent方法的对象。其次,第三个参数除了布尔值useCapture,还可以是一个属性配置对象。该对象有以下属性。capture:布尔值,表示该事件是否在捕获阶段触发监听函数。once:布尔值,表示监听函数是否只触发一次,然后就自动移除。passive:布尔值,表示监听函数不会调用事件的preventDefault方法。如果监听函数调用了,浏览器将忽略这个要求,并在监控台输出一行警告。如果希望事件监听函数只执行一次,可以打开属性配置对象的once属性。element.addEventListener('click', function (event) {
// 只执行一次的代码
}, {once: true});addEventListener方法可以为针对当前对象的同一个事件,添加多个不同的监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用removeEventListener方法手动去除)。function hello() {
console.log('Hello world');
}
document.addEventListener('click', hello, false);
document.addEventListener('click', hello, false);执行上面代码,点击文档只会输出一行Hello world。如果希望向监听函数传递参数,可以用匿名函数包装一下监听函数。function print(x) {
console.log(x);
}
var el = document.getElementById('div1');
el.addEventListener('click', function () { print('Hello'); }, false);上面代码通过匿名函数,向监听函数print传递了一个参数。监听函数内部的this,指向当前事件所在的那个对象。// HTML 代码如下
// <p id="para">Hello</p>
var para = document.getElementById('para');
para.addEventListener('click', function (e) {
console.log(this.nodeName); // "P"
}, false);上面代码中,监听函数内部的this指向事件所在的对象para。3、EventTarget.removeEventListener()EventTarget.removeEventListener方法用来移除addEventListener方法添加的事件监听函数。该方法没有返回值。div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);removeEventListener方法的参数,与addEventListener方法完全一致。它的第一个参数“事件类型”,大小写敏感。注意,removeEventListener方法移除的监听函数,必须是addEventListener方法添加的那个监听函数,而且必须在同一个元素节点,否则无效。div.addEventListener('click', function (e) {}, false);
div.removeEventListener('click', function (e) {}, false);// 移除无效,因为不是同一个监听函数上面代码中,removeEventListener方法无效,因为监听函数不是同一个匿名函数。element.addEventListener('mousedown', handleMouseDown, true);
element.removeEventListener("mousedown", handleMouseDown, false); // 移除无效,第三个参数不一样上面代码中,removeEventListener方法也是无效的,因为第三个参数不一样。4、EventTarget.dispatchEvent()EventTarget.dispatchEvent方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true。target.dispatchEvent(event)dispatchEvent方法的参数是一个Event对象的实例(详见《Event 对象》章节)。para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);上面代码在当前节点触发了click事件。如果dispatchEvent方法的参数为空,或者不是一个有效的事件对象,将报错。下面代码根据dispatchEvent方法的返回值,判断事件是否被取消了。var canceled = !cb.dispatchEvent(event);
if (canceled) {
console.log('事件取消');
} else {
console.log('事件未取消');
}二、事件模型1、监听函数浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。JavaScript 有三种方法,可以为事件绑定监听函数。1.2 HTML 的 on- 属性HTML 语言允许在元素的属性中,直接定义某些事件的监听代码。<body onload="doSomething()"> <!-- 加圆括号-->
<div onclick="console.log('触发事件')">上面代码为body节点的load事件、div节点的click事件,指定了监听代码。一旦事件发生,就会执行这段代码。元素的事件监听属性,都是on加上事件名,比如onload就是on + load,表示load事件的监听代码。注意,这些属性的值是将会执行的代码,而不是一个函数。<!-- 正确 -->
<body onload="doSomething()">
<!-- 错误 -->
<body onload="doSomething">一旦指定的事件发生,on-属性的值是原样传入 JavaScript 引擎执行。因此如果要执行函数,不要忘记加上一对圆括号。使用这个方法指定的监听代码,只会在冒泡阶段触发。<div onClick="console.log(2)">
<button onClick="console.log(1)">点击</button>
</div>上面代码中, <button> 是 <div> 的子元素。 <button> 的click事件,也会触发 <div> 的click事件。由于on-属性的监听代码,只在冒泡阶段触发,所以点击结果是先输出1,再输出2,即事件从子元素开始冒泡到父元素。直接设置on-属性,与通过元素节点的setAttribute方法设置on-属性,效果是一样的。el.setAttribute('onclick', 'doSomething()');
// 等同于
// <Element onclick="doSomething()">1.2 元素节点的事件属性 (例:el.onclick)元素节点对象的事件属性,同样可以指定监听函数。window.onload = doSomething; // 注意这里是函数名,不加圆括号
div.onclick = function (event) { // onclick是元素的一个属性,它指向一个函数,在触发点击时执行此函数
console.log('触发事件');
};使用这个方法指定的监听函数,也是只会在冒泡阶段触发。注意,这种方法与 HTML 的on-属性的差异是,它的值是函数名(doSomething),而不像后者,必须给出完整的监听代码(doSomething())。1.3 EventTarget.addEventListener()所有 DOM 节点实例都有addEventListener方法,用来为该节点定义事件的监听函数。window.addEventListener('load', doSomething, false); // 注意这里是函数名,不加圆括号addEventListener方法的详细介绍,参见EventTarget章节。1.4 小结上面三种方法,第一种“HTML 的 on- 属性”,违反了 HTML 与 JavaScript 代码相分离的原则,将两者写在一起,不利于代码分工,因此不推荐使用。第二种“元素节点的事件属性”的缺点在于,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次定义会覆盖前一次。因此,也不推荐使用。第三种EventTarget.addEventListener是推荐的指定监听函数的方法。它有如下优点:同一个事件可以添加多个监听函数。能够指定在哪个阶段(捕获阶段还是冒泡阶段)触发监听函数。除了 DOM 节点,其他对象(比如window、XMLHttpRequest等)也有这个接口,它等于是整个 JavaScript 统一的监听函数接口。2、this 的指向 (指向元素节点)监听函数内部的this指向触发事件的那个元素节点。(三种事件绑定的this全指向元素节点)<button id="btn" onclick="console.log(this.id)">点击</button>执行上面代码,点击后会输出btn。其他两种监听函数的写法,this的指向也是如此。// HTML 代码如下
// <button id="btn">点击</button>
var btn = document.getElementById('btn');
// 写法一
btn.onclick = function () {
console.log(this.id);
};
// 写法二
btn.addEventListener(
'click',
function (e) {
console.log(this.id);
},
false
);上面两种写法,点击按钮以后也是输出btn。3、事件的传播(捕获、目标、冒泡)一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。这种三阶段的传播模型,使得同一个事件会在多个节点上触发。<div>
<p>点击</p>
</div>上面代码中, <div> 节点之中有一个 <p> 节点。如果对这两个节点,都设置click事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对 <p> 点击,click事件会触发四次。var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true); // true 表示在捕获阶段触发
p.addEventListener('click', callback, true); // true 表示在捕获阶段触发
div.addEventListener('click', callback, false);// fasle 表示在冒泡阶段触发
p.addEventListener('click', callback, false); // fasle 表示在冒泡阶段触发
function callback(event) {
var tag = event.currentTarget.tagName; // 当前目标对象的标签名
var phase = phases[event.eventPhase]; // 触发的阶段
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 点击以后的结果
// Tag: 'DIV'. EventPhase: 'capture' 捕获阶段
// Tag: 'P'. EventPhase: 'target' 目标阶段
// Tag: 'P'. EventPhase: 'target' 目标阶段
// Tag: 'DIV'. EventPhase: 'bubble' 冒泡阶段上面代码表示,click事件被触发了四次: <div> 节点的捕获阶段和冒泡阶段各1次, <p> 节点的目标阶段触发了2次。捕获阶段:事件从 <div> 向 <p> 传播时,触发 <div> 的click事件;目标阶段:事件从 <div> 到达 <p> 时,触发 <p> 的click事件;冒泡阶段:事件从 <p> 传回 <div> 时,再次触发 <div> 的click事件。其中, <p> 节点有两个监听函数(addEventListener方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为click事件触发一次。所以, <p> 会在target阶段有两次输出。注意,浏览器总是假定click事件的目标节点,就是点击位置嵌套最深的那个节点(本例是 <div> 节点里面的 <p> 节点)。所以, <p> 节点的捕获阶段和冒泡阶段,都会显示为target阶段。事件传播的最上层对象是window,接着依次是document,html(document.documentElement)和body(document.body)。也就是说,上例的事件传播顺序,在捕获阶段依次为window、document、html、body、div、p,在冒泡阶段依次为p、div、body、html、document、window。捕获阶段:
window(浏览器对象)--> document(文档对象) --> html --> body --> div --> p (点击目标,在目标阶段)
冒泡阶段:
p (点击目标,在目标阶段)--> div --> body --> html --> document(文档对象) --> window(浏览器对象)4、事件的代理由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') { // 浏览器假定click嵌套最深的元素是目标元素
// some code
}
});上面代码中,click事件的监听函数定义在 <ul> 节点,但是实际上,它处理的是子节点 <li> 的click事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个 <li> 节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。阻止事件冒泡 event.stopPropagation()如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation方法。// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
event.stopPropagation(); // 该方法在事件对象event上
}, true); // true表示在捕获阶段绑定事件监听函数
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false); // false表示在冒泡阶段(默认值)绑定事件监听函数上面代码中,stopPropagation方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。但是,stopPropagation方法只会阻止事件的传播,不会阻止该事件触发 <p> 节点的其他click事件的监听函数。也就是说,不是彻底取消click事件。p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 会触发
console.log(2);
});上面代码中,p元素绑定了两个click事件的监听函数。stopPropagation方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。如果想要彻底取消该事件,不再触发后面所有click的监听函数,可以使用stopImmediatePropagation()方法。p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不会被触发
console.log(2);
});上面代码中,stopImmediatePropagation方法可以彻底取消这个事件,使得后面绑定的所有click监听函数都不再触发。所以,只会输出1,不会输出2。三、Event 对象1. 概述事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个Event对象,所有的事件都是这个对象的实例,或者说继承了Event.prototype对象。Event对象本身就是一个构造函数,可以用来生成新的实例。event = new Event(type, options);// 参数一,事件名称; 参数二,事件对象的配置对象Event构造函数接受两个参数。第一个参数type是字符串,表示事件的名称;第二个参数options是一个对象,表示事件对象的配置。该对象主要有下面两个属性。bubbles:布尔值,可选,默认为false,表示事件对象是否冒泡。cancelable:布尔值,可选,默认为false,表示事件是否可以被取消,即能否用Event.preventDefault()取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。var ev = new Event( // 创建一个新的事件实例
'look', // 事件名称
{ // 事件配置
'bubbles': true, // 事件是否冒泡
'cancelable': false // 事件是否可以被取消
}
);
document.dispatchEvent(ev); // 触发ev实例,该实例是look事件上面代码新建一个look事件实例,然后使用dispatchEvent方法触发该事件。注意,如果不是显式指定bubbles属性为true,生成的事件就只能在“捕获阶段”触发监听函数。// HTML 代码为
// <div><p>Hello</p></div>
var div = document.querySelector('div');
var p = document.querySelector('p');
function callback(event) {
var tag = event.currentTarget.tagName;
console.log('Tag: ' + tag); // 没有任何输出
}
div.addEventListener('click', callback, false); // 该事件是在冒泡阶段监听函数
var click = new Event('click');
p.dispatchEvent(click); // 给p发出一个click事件,该事件默认不会冒泡,因此不会触发父元素div的click事件上面代码中,p元素发出一个click事件,该事件默认不会冒泡。div.addEventListener方法指定在冒泡阶段监听,因此监听函数不会触发。如果写成div.addEventListener('click', callback, true),那么在“捕获阶段”可以监听到这个事件。另一方面,如果这个事件在div元素上触发。div.dispatchEvent(click);那么,不管div元素是在冒泡阶段监听,还是在捕获阶段监听,都会触发监听函数。因为这时div元素是事件的目标,不存在是否冒泡的问题,div元素总是会接收到事件,因此导致监听函数生效。2. 实例属性2.1 Event.bubbles 是否会冒泡,只读,Event.eventPhase 返回整数,表示事件所处阶段,只读Event.bubbles属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,一般用来了解 Event 实例是否可以冒泡。前面说过,除非显式声明,Event构造函数生成的事件,默认是不冒泡的。Event.eventPhase属性返回一个整数常量,表示事件目前所处的阶段。该属性只读。var phase = event.eventPhase;Event.eventPhase的返回值有四种可能。0,事件目前没有发生。1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。2,事件到达目标节点,即Event.target属性指向的那个节点。3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。2.2 Event.cancelable 是否可取消默认行为,Event.cancelBubble是否阻止冒泡,event.defaultPrevented 是否调用过取消默认行为方法Event.cancelable属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性,一般用来了解 Event 实例的特性。大多数浏览器的原生事件是可以取消的。比如,取消click事件,点击链接将无效。但是除非显式声明,Event构造函数生成的事件,默认是不可以取消的。var evt = new Event('foo');
evt.cancelable // false 默认不可取消当Event.cancelable属性为true时,调用Event.preventDefault()就可以取消这个事件,阻止浏览器对该事件的默认行为。如果事件不能取消,调用Event.preventDefault()会没有任何效果。所以使用这个方法之前,最好用Event.cancelable属性判断一下是否可以取消。function preventEvent(event) {
if (event.cancelable) {
event.preventDefault();
} else {
console.warn('This event couldn\'t be canceled.');
console.dir(event);
}
}Event.cancelBubble属性是一个布尔值,如果设为true,相当于执行Event.stopPropagation(),可以阻止事件的传播。Event.defaultPrevented属性返回一个布尔值,表示该事件是否调用过Event.preventDefault方法。该属性只读。if (event.defaultPrevented) {
console.log('该事件已经取消了');
}2.3 Event.currentTarget当前正在通过的节点,Event.target目标节点事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意时点都有两个与事件相关的节点,一个是事件的原始触发节点(Event.target),另一个是事件当前正在通过的节点(Event.currentTarget)。前者通常是后者的后代节点。Event.currentTarget属性返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。Event.target属性返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。事件传播过程中,不同节点的监听函数内部的Event.target与Event.currentTarget属性的值是不一样的。// HTML 代码为
// <p id="para">Hello <em>World</em></p>
function hide(e) {
// 不管点击 Hello 或 World,总是返回 true
console.log(this === e.currentTarget);
// 点击 Hello,返回 true
// 点击 World,返回 false
console.log(this === e.target);
}
document.getElementById('para').addEventListener('click', hide, false);上面代码中, <em> 是 <p> 的子节点,点击 <em> 或者点击 <p> ,都会导致监听函数执行。这时,e.target总是指向原始点击位置的那个节点,而e.currentTarget指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发,所以e.currentTarget总是等同于监听函数内部的this。2.4 Event.type 事件类型(如:'click')Event.type属性返回一个字符串,表示事件类型。事件的类型是在生成事件的时候指定的。该属性只读。var evt = new Event('foo');
evt.type // "foo"2.5 Event.timeStamp 相对于打开网页后的毫秒时间戳Event.timeStamp属性返回一个毫秒时间戳,表示事件发生的时间。它是相对于网页加载成功开始计算的。var evt = new Event('foo');
evt.timeStamp // 3683.6999999995896它的返回值有可能是整数,也有可能是小数(高精度时间戳),取决于浏览器的设置。下面是一个计算鼠标移动速度的例子,显示每秒移动的像素数量。例子:计算鼠标移动速度var previousX;
var previousY;
var previousT;
window.addEventListener('mousemove', function(event) {
if (
previousX !== undefined &&
previousY !== undefined &&
previousT !== undefined
) {
var deltaX = event.screenX - previousX;
var deltaY = event.screenY - previousY;
var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
var deltaT = event.timeStamp - previousT;
console.log(deltaD / deltaT * 1000);
}
previousX = event.screenX;
previousY = event.screenY;
previousT = event.timeStamp;
});2.6 Event.isTrusted 是否由真实用户行为产生的事件Event.isTrusted属性返回一个布尔值,表示该事件是否由真实的用户行为产生。比如,用户点击链接会产生一个click事件,该事件是用户产生的;Event构造函数生成的事件,则是脚本产生的。var evt = new Event('foo');
evt.isTrusted // false上面代码中,evt对象是脚本产生的,所以isTrusted属性返回false。2.7 Event.detail 事件的细节(单击or双击等)Event.detail属性只有浏览器的 UI (用户界面)事件才具有。该属性返回一个数值,表示事件的某种信息。具体含义与事件类型相关。比如,对于click和dblclick事件,Event.detail是鼠标按下的次数(1表示单击,2表示双击,3表示三击);对于鼠标滚轮事件,Event.detail是滚轮正向滚动的距离,负值就是负向滚动的距离,返回值总是3的倍数。// HTML 代码如下
// <p>Hello</p>
function giveDetails(e) {
console.log(e.detail); // 快速点击的次数
}
document.querySelector('p').onclick = giveDetails;3、实例方法3.1 Event.preventDefault() 取消浏览器对当前事件的默认行为Event.preventDefault方法取消浏览器对当前事件的默认行为。比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。注意,该方法只是取消事件对当前元素的默认影响,不会阻止事件的传播。如果要阻止传播,可以使用stopPropagation()或stopImmediatePropagation()方法。// HTML 代码为
// <input type="checkbox" id="my-checkbox" />
var cb = document.getElementById('my-checkbox');
cb.addEventListener(
'click',
function (e){ e.preventDefault(); },
false
);上面代码中,浏览器的默认行为是单击会选中单选框,取消这个行为,就导致无法选中单选框。利用这个方法,可以为文本输入框设置校验条件。如果用户的输入不符合条件,就无法将字符输入文本框。例子:只能输入字母的输入框// HTML 代码为
// <input type="text" id="my-input" />
var input = document.getElementById('my-input');
input.addEventListener('keypress', checkName, false);
function checkName(e) {
if (e.charCode < 97 || e.charCode > 122) {
e.preventDefault();
}
}上面代码为文本框的keypress事件设定监听函数后,将只能输入小写字母,否则输入事件的默认行为(写入文本框)将被取消,导致不能向文本框输入内容。3.2 Event.stopPropagation() 阻止事件传播stopPropagation方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。function stopEvent(e) {
e.stopPropagation(); // 阻止事件冒泡
}
el.addEventListener('click', stopEvent, false);上面代码中,click事件将不会进一步冒泡到el节点的父节点。3.3 Event.stopImmediatePropagation() 阻止同一个事件的其他监听函数被调用Event.stopImmediatePropagation方法阻止同一个事件的其他监听函数被调用,不管监听函数定义在当前节点还是其他节点。也就是说,该方法阻止事件的传播,比Event.stopPropagation()更彻底。如果同一个节点对于同一个事件指定了多个监听函数,这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了Event.stopImmediatePropagation方法,其他的监听函数就不会再执行了。function l1(e){
e.stopImmediatePropagation();
}
function l2(e){ // 不会被调用
console.log('hello world');
}
el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);上面代码在el节点上,为click事件添加了两个监听函数l1和l2。由于l1调用了event.stopImmediatePropagation方法,所以l2不会被调用。3.4 Event.composedPath() 数组,目标和冒泡的节点Event.composedPath()返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。// HTML 代码如下
// <div>
// <p>Hello</p>
// </div>
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', function (e) {
console.log(e.composedPath());
}, false);
// [p, div, body, html, document, Window]上面代码中,click事件的最底层节点是p,向上依次是div、body、html、document、Window。四、鼠标事件1、鼠标事件的种类鼠标事件指与鼠标相关的事件,继承了MouseEvent接口。具体的事件主要有以下一些。click:按下鼠标(通常是按下主按钮)时触发。【单击】dblclick:在同一个元素上双击鼠标时触发。【双击】mousedown:按下鼠标键时触发。【按下】mouseup:释放按下的鼠标键时触发。【抬起】mousemove:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。【经过(多次)】mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件(详见后文)。【进入(单次)】mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件(详见后文)。【进入+子节点】mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件(详见后文)。【离开(单次)】mouseout:鼠标离开一个节点时触发,离开父节点会触发这个事件(详见后文)。【离开+子节点】contextmenu:按下鼠标右键时(上下文菜单出现前)触发,或者按下“上下文菜单键”时触发。【右键】wheel:滚动鼠标的滚轮时触发,该事件继承的是WheelEvent接口。【滚轮】click事件指的是,用户在同一个位置先完成mousedown动作,再完成mouseup动作。因此,触发顺序是,mousedown首先触发,mouseup接着触发,click最后触发。dblclick事件则会在mousedown、mouseup、click之后触发。mouseover事件和mouseenter事件,都是鼠标进入一个节点时触发。两者的区别是,mouseenter事件只触发一次,而只要鼠标在节点内部移动,mouseover事件会在子节点上触发多次。/* HTML 代码如下
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
*/
var ul = document.querySelector('ul');
// 进入 ul 节点以后,mouseenter 事件只会触发一次
// 以后只要鼠标在节点内移动,都不会再触发这个事件
// event.target 是 ul 节点
ul.addEventListener('mouseenter', function (event) {
event.target.style.color = 'purple';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);
// 进入 ul 节点以后,只要在子节点上移动,mouseover 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseover', function (event) {
event.target.style.color = 'orange';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);上面代码中,在父节点内部进入子节点,不会触发mouseenter事件,但是会触发mouseover事件。mouseout事件和mouseleave事件,都是鼠标离开一个节点时触发。两者的区别是,在父元素内部离开一个子元素时,mouseleave事件不会触发,而mouseout事件会触发。/* HTML 代码如下
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
*/
var ul = document.querySelector('ul');
// 先进入 ul 节点,然后在节点内部移动,不会触发 mouseleave 事件
// 只有离开 ul 节点时,触发一次 mouseleave
// event.target 是 ul 节点
ul.addEventListener('mouseleave', function (event) {
event.target.style.color = 'purple';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);
// 先进入 ul 节点,然后在节点内部移动,mouseout 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseout', function (event) {
event.target.style.color = 'orange';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);上面代码中,在父节点内部离开子节点,不会触发mouseleave事件,但是会触发mouseout事件。2、MouseEvent 接口概述MouseEvent接口代表所有鼠标事件所产生的对象都是MouseEvent实例。此外,滚轮事件和拖拉事件也是MouseEvent实例。MouseEvent接口继承了Event接口,所以拥有Event的所有属性和方法。它还有自己的属性和方法。浏览器原生提供一个MouseEvent构造函数,用于新建一个MouseEvent实例。var event = new MouseEvent(type, options);// 参数一,事件名称字符串;参数二,事件配置对象MouseEvent构造函数接受两个参数。第一个参数是字符串,表示事件名称;第二个参数是一个事件配置对象,该参数可选。除了Event接口的实例配置属性,该对象可以配置以下属性,所有属性都是可选的。screenX:数值,鼠标相对于屏幕的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。screenY:数值,鼠标相对于屏幕的垂直位置(单位像素),其他与screenX相同。clientX:数值,鼠标相对于程序窗口的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。clientY:数值,鼠标相对于程序窗口的垂直位置(单位像素),其他与clientX相同。ctrlKey:布尔值,是否同时按下了 Ctrl 键,默认值为false。shiftKey:布尔值,是否同时按下了 Shift 键,默认值为false。altKey:布尔值,是否同时按下 Alt 键,默认值为false。metaKey:布尔值,是否同时按下 Meta 键(win键),默认值为false。button:数值,表示按下了哪一个鼠标按键,默认值为0,表示按下主键(通常是鼠标的左键)或者当前事件没有定义这个属性;1表示按下辅助键(通常是鼠标的中间键),2表示按下次要键(通常是鼠标的右键)。buttons:数值,表示按下了鼠标的哪些键,是一个三个比特位的二进制值,默认为0(没有按下任何键)。1(二进制001)表示按下主键(通常是左键),2(二进制010)表示按下次要键(通常是右键),4(二进制100)表示按下辅助键(通常是中间键)。因此,如果返回3(二进制011)就表示同时按下了左键和右键。relatedTarget:节点对象,表示事件的相关节点,默认为null。mouseenter和mouseover事件时,表示鼠标刚刚离开的那个元素节点;mouseout和mouseleave事件时,表示鼠标正在进入的那个元素节点。下面是一个例子。var event = new MouseEvent('click2', {
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('checkbox');
cb.addEventListener('click2',function(){ // 绑定事件监听函数
console.log(22) // 被执行
})
cb.dispatchEvent(event);// 触发事件上面代码生成一个鼠标点击事件,并触发该事件。3、MouseEvent 接口的实例属性3.1 MouseEvent.altKey,MouseEvent.ctrlKey,MouseEvent.metaKey,MouseEvent.shiftKeyMouseEvent.altKey、MouseEvent.ctrlKey、MouseEvent.metaKey、MouseEvent.shiftKey这四个属性都返回一个布尔值,表示事件发生时,是否按下对应的键。它们都是只读属性。altKey属性:Alt 键ctrlKey属性:Ctrl 键metaKey属性:Meta 键(Mac 键盘是一个四瓣的小花,Windows 键盘是 Windows 键)shiftKey属性:Shift 键// HTML 代码如下
// <body onclick="showKey(event)">
function showKey(e) {
console.log('ALT key pressed: ' + e.altKey);
console.log('CTRL key pressed: ' + e.ctrlKey);
console.log('META key pressed: ' + e.metaKey);
console.log('SHIFT key pressed: ' + e.shiftKey);
}上面代码中,点击网页会输出是否同时按下对应的键。3.2 MouseEvent.button 鼠标的哪个键,MouseEvent.buttons同时按哪些键MouseEvent.button属性返回一个数值,表示事件发生时按下了鼠标的哪个键。该属性只读。0:按下主键(通常是左键),或者该事件没有初始化这个属性(比如mousemove事件)。1:按下辅助键(通常是中键或者滚轮键)。2:按下次键(通常是右键)。// HTML 代码为
// <button onmouseup="whichButton(event)">点击</button>
var whichButton = function (e) {
switch (e.button) {
case 0:
console.log('Left button clicked.');
break;
case 1:
console.log('Middle button clicked.');
break;
case 2:
console.log('Right button clicked.');
break;
default:
console.log('Unexpected code: ' + e.button);
}
}MouseEvent.buttons属性返回一个三个比特位的值,表示同时按下了哪些键。它用来处理同时按下多个鼠标键的情况。该属性只读。1:二进制为001(十进制的1),表示按下左键。2:二进制为010(十进制的2),表示按下右键。4:二进制为100(十进制的4),表示按下中键或滚轮键。同时按下多个键的时候,每个按下的键对应的比特位都会有值。比如,同时按下左键和右键,会返回3(二进制为011)。document.body.addEventListener('mousemove',function(e){ // 注意,用click时一直都是0
console.log(e.buttons)
})
// 未按下任何键时是 0
// 按下左键 1 (001)
// 按下右键 2 (010)
// 按下中键 4 (100)
// 按下左键和右键 3 (011)
// 按下左键和中键 5 (101)
// 按下右键和中键 6 (110)
// 按下左、中、右键 7 (111)3.3 MouseEvent.clientX 相对浏览器X坐标,MouseEvent.clientY 相对浏览器Y坐标MouseEvent.clientX属性返回鼠标位置相对于浏览器窗口左上角的水平坐标(单位像素),MouseEvent.clientY属性返回垂直坐标。这两个属性都是只读属性。// HTML 代码为
// <body onmousedown="showCoords(event)">
function showCoords(evt){
console.log(
'clientX value: ' + evt.clientX + '\n' +
'clientY value: ' + evt.clientY + '\n'
);
}这两个属性还分别有一个别名MouseEvent.x和MouseEvent.y。3.4 MouseEvent.movementX 上一个鼠标经过事件的X距离,MouseEvent.movementY 上一个鼠标经过事件的Y距离MouseEvent.movementX属性返回当前位置与上一个mousemove事件之间的水平距离(单位像素)。数值上,它等于下面的计算公式。currentEvent.movementX = currentEvent.screenX - previousEvent.screenXMouseEvent.movementY属性返回当前位置与上一个mousemove事件之间的垂直距离(单位像素)。数值上,它等于下面的计算公式。currentEvent.movementY = currentEvent.screenY - previousEvent.screenY。这两个属性都是只读属性。3.5 MouseEvent.screenX 相对屏幕X坐标,MouseEvent.screenY 相对屏幕Y坐标MouseEvent.screenX属性返回鼠标位置相对于屏幕左上角的水平坐标(单位像素),MouseEvent.screenY属性返回垂直坐标。这两个属性都是只读属性。// HTML 代码如下
// <body onmousedown="showCoords(event)">
function showCoords(evt) {
console.log(
'screenX value: ' + evt.screenX + '\n',
'screenY value: ' + evt.screenY + '\n'
);
}3.6 MouseEvent.offsetX 偏移量X,MouseEvent.offsetY 偏移量YMouseEvent.offsetX属性返回鼠标位置与目标节点左侧的padding边缘的水平距离(单位像素),MouseEvent.offsetY属性返回与目标节点上方的padding边缘的垂直距离。这两个属性都是只读属性。/* HTML 代码如下
<style>
p {
width: 100px;
height: 100px;
padding: 100px;
}
</style>
<p>Hello</p>
*/
var p = document.querySelector('p');
p.addEventListener(
'click',
function (e) {
console.log(e.offsetX); // 包含padding
console.log(e.offsetY);
},
false
);上面代码中,鼠标如果在p元素的中心位置点击,会返回150 150。因此中心位置距离左侧和上方的padding边缘,等于padding的宽度(100像素)加上元素内容区域一半的宽度(50像素)。3.7 MouseEvent.pageX 文档X坐标,MouseEvent.pageY 文档Y坐标MouseEvent.pageX属性返回鼠标位置与文档左侧边缘的距离(单位像素),MouseEvent.pageY属性返回与文档上侧边缘的距离(单位像素)。它们的返回值都包括文档不可见的部分。这两个属性都是只读。/* HTML 代码如下
<style>
body {
height: 2000px;
}
</style>
*/
document.body.addEventListener(
'click',
function (e) {
console.log(e.pageX);
console.log(e.pageY);
},
false
);上面代码中,页面高度为2000像素,会产生垂直滚动条。滚动到页面底部,点击鼠标输出的pageY值会接近2000。3.8 MouseEvent.relatedTarget 事件的相关节点MouseEvent.relatedTarget属性返回事件的相关节点。对于那些没有相关节点的事件,该属性返回null。该属性只读。下表列出不同事件的target属性值和relatedTarget属性值义。事件名称target 属性relatedTarget 属性focusin接受焦点的节点丧失焦点的节点focusout丧失焦点的节点接受焦点的节点mouseenter将要进入的节点将要离开的节点mouseleave将要离开的节点将要进入的节点mouseout将要离开的节点将要进入的节点mouseover将要进入的节点将要离开的节点dragenter将要进入的节点将要离开的节点dragexit将要离开的节点将要进入的节点下面是一个例子。/*
HTML 代码如下
<div id="outer" style="height:50px;width:50px;border-width:1px solid black;">
<div id="inner" style="height:25px;width:25px;border:1px solid black;"></div>
</div>
*/
var inner = document.getElementById('inner');
inner.addEventListener('mouseover', function (event) {
console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
}, false);
inner.addEventListener('mouseenter', function (event) {
console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
});
inner.addEventListener('mouseout', function () {
console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});
inner.addEventListener("mouseleave", function (){
console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});
// 鼠标从 outer 进入inner,输出
// 进入inner 离开outer
// 进入inner 离开outer
// 鼠标从 inner进入 outer,输出
// 离开inner 进入outer
// 离开inner 进入outer4、MouseEvent 接口的实例方法4.1 MouseEvent.getModifierState() 是否按下指定功能键MouseEvent.getModifierState方法返回一个布尔值,表示有没有按下特定的功能键。它的参数是一个表示功能键的字符串。document.addEventListener('click', function (e) {
console.log(e.getModifierState('CapsLock'));
}, false);上面的代码可以了解用户是否按下了大写键。5、WheelEvent 接口 (滚轮)5.1 概述WheelEvent 接口继承了 MouseEvent 实例,代表鼠标滚轮事件的实例对象。目前,鼠标滚轮相关的事件只有一个wheel事件,用户滚动鼠标的滚轮,就生成这个事件的实例。浏览器原生提供WheelEvent()构造函数,用来生成WheelEvent实例。var wheelEvent = new WheelEvent(type, options);WheelEvent()构造函数可以接受两个参数,第一个是字符串,表示事件类型,对于滚轮事件来说,这个值目前只能是wheel。第二个参数是事件的配置对象。该对象的属性除了Event、UIEvent的配置属性以外,还可以接受以下几个属性,所有属性都是可选的。deltaX:数值,表示滚轮的水平滚动量,默认值是 0.0。deltaY:数值,表示滚轮的垂直滚动量,默认值是 0.0。deltaZ:数值,表示滚轮的 Z 轴滚动量,默认值是 0.0。deltaMode:数值,表示相关的滚动事件的单位,适用于上面三个属性。0表示滚动单位为像素,1表示单位为行,2表示单位为页,默认为0。5.2 实例属性WheelEvent事件实例除了具有Event和MouseEvent的实例属性和实例方法,还有一些自己的实例属性,但是没有自己的实例方法。下面的属性都是只读属性。WheelEvent.deltaX:数值,表示滚轮的水平滚动量。WheelEvent.deltaY:数值,表示滚轮的垂直滚动量。WheelEvent.deltaZ:数值,表示滚轮的 Z 轴滚动量。WheelEvent.deltaMode:数值,表示上面三个属性的单位,0是像素,1是行,2是页。五、键盘事件1、键盘事件的种类键盘事件由用户击打键盘触发,主要有keydown、keypress、keyup三个事件,它们都继承了KeyboardEvent接口。keydown:按下键盘时触发。【按下】keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个事件。【按下有值的键】keyup:松开键盘时触发该事件。【松开】如果用户一直按键不松开,就会连续触发键盘事件,触发的顺序如下。keydownkeypresskeydownkeypress...(重复以上过程)keyup2、KeyboardEvent 接口概述KeyboardEvent接口用来描述用户与键盘的互动。这个接口继承了Event接口,并且定义了自己的实例属性和实例方法。浏览器原生提供KeyboardEvent构造函数,用来新建键盘事件的实例。new KeyboardEvent(type, options) // 参数一,事件类型;参数二,事件配置对象KeyboardEvent构造函数接受两个参数。第一个参数是字符串,表示事件类型;第二个参数是一个事件配置对象,该参数可选。除了Event接口提供的属性,还可以配置以下字段,它们都是可选。key:字符串,当前按下的键,默认为空字符串。【键名】code:字符串,表示当前按下的键的字符串形式,默认为空字符串。【键码】location:整数,当前按下的键的位置,默认为0。ctrlKey:布尔值,是否按下 Ctrl 键,默认为false。shiftKey:布尔值,是否按下 Shift 键,默认为false。altKey:布尔值,是否按下 Alt 键,默认为false。metaKey:布尔值,是否按下 Meta 键,默认为false。repeat:布尔值,是否重复按键,默认为false。3、KeyboardEvent 的实例属性3.1 KeyboardEvent.altKey,KeyboardEvent.ctrlKey,KeyboardEvent.metaKey,KeyboardEvent.shiftKey 【是否按下对应键,布尔值】以下属性都是只读属性,返回一个布尔值,表示是否按下对应的键。KeyboardEvent.altKey:是否按下 Alt 键KeyboardEvent.ctrlKey:是否按下 Ctrl 键KeyboardEvent.metaKey:是否按下 meta 键(Mac 系统是一个四瓣的小花,Windows 系统是 windows 键)KeyboardEvent.shiftKey:是否按下 Shift 键下面是一个示例。function showChar(e) {
console.log('ALT: ' + e.altKey);
console.log('CTRL: ' + e.ctrlKey);
console.log('Meta: ' + e.metaKey);
console.log('Shift: ' + e.shiftKey);
}
document.body.addEventListener('keydown', showChar, false);3.2 KeyboardEvent.code 键码KeyboardEvent.code属性返回一个字符串,表示当前按下的键的字符串形式。该属性只读。下面是一些常用键的字符串形式,其他键请查文档。数字键0 - 9:返回digital0 - digital9字母键A - z:返回KeyA - KeyZ功能键F1 - F12:返回 F1 - F12方向键:返回ArrowDown、ArrowUp、ArrowLeft、ArrowRightAlt 键:返回AltLeft或AltRightShift 键:返回ShiftLeft或ShiftRightCtrl 键:返回ControlLeft或ControlRight3.3 KeyboardEvent.key 键名KeyboardEvent.key属性返回一个字符串,表示按下的键名。该属性只读。如果按下的键代表可打印字符,则返回这个字符,比如数字、字母。如果按下的键代表不可打印的特殊字符,则返回预定义的键值,比如 Backspace,Tab,Enter,Shift,Control,Alt,CapsLock,Esc,Spacebar,PageUp,PageDown,End,Home,Left,Right,Up,Down,PrintScreen,Insert,Del,Win,F1~F12,NumLock,Scroll 等。如果同时按下一个控制键和一个符号键,则返回符号键的键名。比如,按下 Ctrl + a,则返回a;按下 Shift + a,则返回大写的A。如果无法识别键名,返回字符串Unidentified。3.4 KeyboardEvent.location 键处于哪个位置,整数KeyboardEvent.location属性返回一个整数,表示按下的键处在键盘的哪一个区域。它可能取以下值。0:处在键盘的主区域,或者无法判断处于哪一个区域。1:处在键盘的左侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。2:处在键盘的右侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。3:处在数字小键盘。3.5 KeyboardEvent.repeat 是否长按KeyboardEvent.repeat返回一个布尔值,代表该键是否被按着不放,以便判断是否重复这个键,即浏览器会持续触发keydown和keypress事件,直到用户松开手为止。4、KeyboardEvent 的实例方法4.1 KeyboardEvent.getModifierState() 是否按下指定功能键KeyboardEvent.getModifierState()方法返回一个布尔值,表示是否按下或激活指定的功能键。它的常用参数如下。Alt:Alt 键CapsLock:大写锁定键Control:Ctrl 键Meta:Meta 键NumLock:数字键盘开关键Shift:Shift 键if (
event.getModifierState('Control') +
event.getModifierState('Alt') +
event.getModifierState('Meta') > 1
) {
return;
}上面代码表示,只要Control、Alt、Meta里面,同时按下任意两个或两个以上的键就返回。六、进度事件1、进度事件的种类进度事件用来描述资源加载的进度,主要由 AJAX 请求、 <img> 、 <audio> 、 <video> 、 <style> 、 <link> 等外部资源的加载触发,继承了ProgressEvent接口。它主要包含以下几种事件。abort:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。【中止加载】error:由于错误导致外部资源无法加载时触发。【加载错误】load:外部资源加载成功时触发。【加载成功】loadstart:外部资源开始加载时触发。【开始加载】loadend:外部资源停止加载时触发,发生顺序排在error、abort、load等事件的后面。【停止加载】progress:外部资源加载过程中不断触发。【加载中,不断触发】timeout:加载超时时触发。【加载超时】注意,除了资源下载,文件上传也存在这些事件。下面是一个例子。image.addEventListener('load', function (event) { // 加载成功
image.classList.add('finished');
});
image.addEventListener('error', function (event) { // 加载出错
image.style.display = 'none';
});上面代码在图片元素加载完成后,为图片元素添加一个finished的 Class。如果加载失败,就把图片元素的样式设置为不显示。有时候,图片加载会在脚本运行之前就完成,尤其是当脚本放置在网页底部的时候,因此有可能load和error事件的监听函数根本不会执行。所以,比较可靠的方式,是用complete属性先判断一下是否加载完成。function loaded() {
// ...
}
if (image.complete) { // 是否加载完成
loaded();
} else {
image.addEventListener('load', loaded); // 加载成功事件
}由于 DOM 的元素节点没有提供是否加载错误的属性,所以error事件的监听函数最好放在 <img> 元素的 HTML 代码中,这样才能保证发生加载错误时百分之百会执行。<img src="/wrong/url" onerror="this.style.display='none';" />loadend事件的监听函数,可以用来取代abort事件、load事件、error事件的监听函数,因为它总是在这些事件之后发生。req.addEventListener('loadend', loadEnd, false);
function loadEnd(e) {
console.log('传输结束,成功失败未知');
}loadend事件本身不提供关于进度结束的原因,但可以用它来做所有加载结束场景都需要做的一些操作。另外,error事件有一个特殊的性质,就是不会冒泡。所以,子元素的error事件,不会触发父元素的error事件监听函数。2、ProgressEvent 接口2.1 概述ProgressEvent接口主要用来描述外部资源加载的进度,比如 AJAX 加载、 <img> 、 <video> 、 <style> 、 <link> 等外部资源加载。进度相关的事件都继承了这个接口。这个接口继承了Event接口。浏览器原生提供了ProgressEvent()构造函数,用来生成事件实例。new ProgressEvent(type, options) // 参数一,事件类型;参数二,配置对象ProgressEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的类型,这个参数是必须的。第二个参数是一个配置对象,表示事件的属性,该参数可选。配置对象除了可以使用Event接口的配置属性,还可以使用下面的属性,所有这些属性都是可选的。lengthComputable:布尔值,表示加载的总量是否可以计算,默认是false。loaded:整数,表示已经加载的量,默认是0。total:整数,表示需要加载的总量,默认是0。2.2 ProgressEvent的实例属性。ProgressEvent.lengthComputable 总量是否可以计算ProgressEvent.loaded 已加载的量ProgressEvent.total 需要加载的总量如果ProgressEvent.lengthComputable为false,ProgressEvent.total实际上是没有意义的。下面是一个例子。var p = new ProgressEvent('load', {
lengthComputable: true,
loaded: 30,
total: 100,
});
document.body.addEventListener('load', function (e) {
console.log('已经加载:' + (e.loaded / e.total) * 100 + '%');
});
document.body.dispatchEvent(p);
// 已经加载:30%上面代码先构造一个load事件,抛出后被监听函数捕捉到。下面是一个实际的例子。var xhr = new XMLHttpRequest();
xhr.addEventListener('progress', updateProgress, false); // 加载中
xhr.addEventListener('load', transferComplete, false); // 加载成功
xhr.addEventListener('error', transferFailed, false); // 加载错误
xhr.addEventListener('abort', transferCanceled, false); // 中止加载
xhr.open();
function updateProgress(e) { // 加载中
if (e.lengthComputable) { // 是否可以计算总量
var percentComplete = e.loaded / e.total; // 加载进度计算
} else {
console.log('不能计算进度');
}
}
function transferComplete(e) { // 加载成功
console.log('传输结束');
}
function transferFailed(evt) { // 加载错误
console.log('传输过程中发生错误');
}
function transferCanceled(evt) { // 中止加载
console.log('用户取消了传输');
}上面是下载过程的进度事件,还存在上传过程的进度事件。这时所有监听函数都要放在XMLHttpRequest.upload对象上面。var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', updateProgress, false);
xhr.upload.addEventListener('load', transferComplete, false);
xhr.upload.addEventListener('error', transferFailed, false);
xhr.upload.addEventListener('abort', transferCanceled, false);
xhr.open();七、表单事件1、表单事件的种类1.1 input 事件 (值发生变化触发,会连续)input事件当 <input> 、 <select> 、 <textarea> 的值发生变化时触发。对于复选框( <input type=checkbox> )或单选框( <input type=radio> ),用户改变选项时,也会触发这个事件。另外,对于打开contenteditable属性的元素,只要值发生变化,也会触发input事件。input事件的一个特点,就是会连续触发,比如用户每按下一次按键,就会触发一次input事件。input事件对象继承了InputEvent接口。该事件跟change事件很像,不同之处在于input事件在元素的值发生变化后立即发生,而change在元素失去焦点时发生,而内容此时可能已经变化多次。也就是说,如果有连续变化,input事件会触发多次,而change事件只在失去焦点时触发一次。下面是 <select> 元素的例子。/* HTML 代码如下
<select id="mySelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
*/
function inputHandler(e) {
console.log(e.target.value)
}
var mySelect = document.querySelector('#mySelect');
mySelect.addEventListener('input', inputHandler);上面代码中,改变下拉框选项时,会触发input事件,从而执行回调函数inputHandler。1.2 select 事件 (选中文本时触发)select事件当在 <input> 、 <textarea> 里面选中文本时触发。// HTML 代码如下
// <input id="test" type="text" value="Select me!" />
var elem = document.getElementById('test');
elem.addEventListener('select', function (e) {
console.log(e.type); // "select" 事件类型
var _target = e.target;
console.log(_target.value); // 文本框的全部值
console.log(_target.selectionDirection); // 选择的方向:'forward'正向、'backward'反向
console.log(_target.selectionStart); // 开始选择的索引
console.log(_target.selectionEnd); // 结束选择的索引
// 注意:开始和结束索引是不分选择方向的,开始的索引一直是靠前的
console.log(_target.value.slice(_target.selectionStart, _target.selectionEnd)) // 选中的那部分字符串
}, false);选中的文本可以通过event.target元素的selectionDirection、selectionEnd、selectionStart和value属性拿到。1.3 change 事件 (值发生变化时触发,单次)change事件当 <input> 、 <select> 、 <textarea> 的值发生变化时触发。它与input事件的最大不同,就是不会连续触发,只有当全部修改完成时才会触发,另一方面input事件必然伴随change事件。具体来说,分成以下几种情况。激活单选框(radio)或复选框(checkbox)时触发。用户提交时触发。比如,从下列列表(select)完成选择,在日期或文件输入框完成选择。当文本框或 <textarea> 元素的值发生改变,并且丧失焦点时触发。下面是一个例子。// HTML 代码如下
// <select size="1" onchange="changeEventHandler(event);">
// <option>chocolate</option>
// <option>strawberry</option>
// <option>vanilla</option>
// </select>
function changeEventHandler(event) {
console.log(event.target.value);
}如果比较一下上面input事件的例子,你会发现对于 <select> 元素来说,input和change事件基本是等价的。1.4 invalid 事件 (表单提交不满足条件触发)用户提交表单时,如果表单元素的值不满足校验条件,就会触发invalid事件。<form>
<input type="text" required oninvalid="console.log('invalid input')" />
<button type="submit">提交</button>
</form>上面代码中,输入框是必填的。如果不填,用户点击按钮提交时,就会触发输入框的invalid事件,导致提交被取消。1.5 reset 事件(重置),submit 事件(提交)reset事件当表单重置(所有表单成员变回默认值)时触发。submit事件当表单数据向服务器提交时触发。注意,这两个事件发生在表单对象 <form> 上,而不是发生在表单的成员上,因为提交的是表单,而不是表单成员。<form onreset="console.log('触发了重置事件')" onsubmit="console.log('触发了提交事件')" name="input" action="html_form_action.php" method="get">
<input type="text" name="lname" value="Duck">
<button type="reset">重置</button>
<button type="submit">提交</button>
</form>2、InputEvent 接口(input事件的实例)InputEvent接口主要用来描述input事件的实例。该接口继承了Event接口,还定义了一些自己的实例属性和实例方法。浏览器原生提供InputEvent()构造函数,用来生成实例对象。new InputEvent(type, options) // 参数一,事件名称;参数二,配置对象InputEvent构造函数可以接受两个参数。第一个参数是字符串,表示事件名称,该参数是必需的。第二个参数是一个配置对象,用来设置事件实例的属性,该参数是可选的。配置对象的字段除了Event构造函数的配置属性,还可以设置下面的字段,这些字段都是可选的。inputType:字符串,表示发生变更的类型(详见下文)。data:字符串,表示插入的字符串。如果没有插入的字符串(比如删除操作),则返回null或空字符串。dataTransfer:返回一个 DataTransfer 对象实例,该属性通常只在输入框接受富文本输入时有效。InputEvent的实例属性主要就是上面三个属性,这三个实例属性都是只读的。(1)InputEvent.data 变动的那部分内容InputEvent.data属性返回一个字符串,表示变动的内容。// HTML 代码如下
// <input type="text" id="myInput">
var input = document.getElementById('myInput');
input.addEventListener('input', myFunction, false);
function myFunction(e) {
console.log(e.data);
}上面代码中,如果手动在输入框里面输入abc,控制台会先输出a,再在下一行输出b,再在下一行输出c。然后选中abc,一次性将它们删除,控制台会输出null或一个空字符串。(2)InputEvent.inputType 变更类型InputEvent.inputType属性返回一个字符串,表示字符串发生变更的类型。对于常见情况,Chrome 浏览器的返回值如下。完整列表可以参考文档。手动插入文本:insertText粘贴插入文本:insertFromPaste向后删除:deleteContentBackward向前删除:deleteContentForward(3)InputEvent.dataTransferInputEvent.dataTransfer属性返回一个 DataTransfer 实例。该属性只在文本框接受粘贴内容(insertFromPaste)或拖拽内容(insertFromDrop)时才有效。八、触摸事件1、触摸操作概述浏览器的触摸 API 由三个部分组成。Touch:一个触摸点的实例TouchList:多个触摸点集合的实例TouchEvent:触摸引发的事件实例Touch接口的实例对象用来表示触摸点(一根手指或者一根触摸笔),包括位置、大小、形状、压力、目标元素等属性。有时,触摸动作由多个触摸点(多根手指)组成,多个触摸点的集合由TouchList接口的实例对象表示。TouchEvent接口的实例对象代表由触摸引发的事件,只有触摸屏才会引发这一类事件。很多时候,触摸事件和鼠标事件同时触发,即使这个时候并没有用到鼠标。这是为了让那些只定义鼠标事件、没有定义触摸事件的代码,在触摸屏的情况下仍然能用。如果想避免这种情况,可以用event.preventDefault方法阻止发出鼠标事件。TouchEvent {isTrusted: true, touches: TouchList, targetTouches: TouchList, changedTouches: TouchList, altKey: false, …} // TouchEvent接口 ,继承Event接口属性和方法
altKey: false
bubbles: true
cancelBubble: false
cancelable: false
changedTouches: TouchList // TouchList 接口 (所有触摸点集合)
0: Touch // Touch 接口 (单个触摸点)
clientX: 232
clientY: 96
force: 1 // 触摸压力
identifier: 0 // 唯一ID
pageX: 232
pageY: 96
radiusX: 11.5
radiusY: 11.5
region: null
rotationAngle: 0
screenX: 476
screenY: 266
target: html // 触摸目标元素
__proto__: Touch
length: 1
__proto__: TouchList
composed: true
ctrlKey: false
currentTarget: null
defaultPrevented: false
detail: 0
eventPhase: 0
isTrusted: true
metaKey: false
path: (3) [html, document, Window]
returnValue: true
shiftKey: false
sourceCapabilities: InputDeviceCapabilities {firesTouchEvents: true}
srcElement: html
target: html
targetTouches: TouchList {0: Touch, length: 1} // TouchList 接口 (所有触摸点集合)
timeStamp: 994.1749999998137
touches: TouchList {0: Touch, length: 1} // TouchList 接口 (所有触摸点集合)
type: "touchmove" // 当前触摸事件类型
view: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
which: 0
2、Touch 接口2.1 Touch 接口概述 (单个触摸点)Touch 接口代表单个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。浏览器原生提供Touch构造函数,用来生成Touch实例。var touch = new Touch(touchOptions);Touch构造函数接受一个配置对象作为参数,它有以下属性。identifier:必需,类型为整数,表示触摸点的唯一 ID。target:必需,类型为元素节点,表示触摸点开始时所在的网页元素。clientX:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的水平距离,默认为0。clientY:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的垂直距离,默认为0。screenX:可选,类型为数值,表示触摸点相对于屏幕左上角的水平距离,默认为0。screenY:可选,类型为数值,表示触摸点相对于屏幕左上角的垂直距离,默认为0。pageX:可选,类型为数值,表示触摸点相对于网页左上角的水平位置(即包括页面的滚动距离),默认为0。pageY:可选,类型为数值,表示触摸点相对于网页左上角的垂直位置(即包括页面的滚动距离),默认为0。radiusX:可选,类型为数值,表示触摸点周围受到影响的椭圆范围的 X 轴半径,默认为0。radiusY:可选:类型为数值,表示触摸点周围受到影响的椭圆范围的 Y 轴半径,默认为0。rotationAngle:可选,类型为数值,表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间,默认值为0。force:可选,类型为数值,范围在0到1之间,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力,默认为0。2.2 Touch 接口的实例属性(1)Touch.identifier 触摸点的IDTouch.identifier属性返回一个整数,表示触摸点的唯一 ID。这个值在整个触摸过程保持不变,直到触摸事件结束。someElement.addEventListener('touchmove', function (e) {
for (var i = 0; i < e.changedTouches.length; i++) {
console.log(e.changedTouches[i].identifier);
}
}, false);(2)Touch.screenX,Touch.screenY,Touch.clientX,Touch.clientY,pageX,pageY (相对屏幕、浏览器、文档的坐标)Touch.screenX属性和Touch.screenY属性,分别表示触摸点相对于屏幕左上角的横坐标和纵坐标,与页面是否滚动无关。Touch.clientX属性和Touch.clientY属性,分别表示触摸点相对于浏览器视口左上角的横坐标和纵坐标,与页面是否滚动无关。Touch.pageX属性和Touch.pageY属性,分别表示触摸点相对于当前页面左上角的横坐标和纵坐标,包含了页面滚动带来的位移。(3)Touch.radiusX,Touch.radiusY,Touch.rotationAngle (触摸椭圆区域半径、角度)Touch.radiusX属性和Touch.radiusY属性,分别返回触摸点周围受到影响的椭圆范围的 X 轴半径和 Y 轴半径,单位为像素。乘以 2 就可以得到触摸范围的宽度和高度。Touch.rotationAngle属性表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间。上面这三个属性共同定义了用户与屏幕接触的区域,对于描述手指这一类非精确的触摸,很有帮助。指尖接触屏幕,触摸范围会形成一个椭圆,这三个属性就用来描述这个椭圆区域。下面是一个示例。div.addEventListener('touchstart', rotate);
div.addEventListener('touchmove', rotate);
div.addEventListener('touchend', rotate);
function rotate(e) {
var touch = e.changedTouches.item(0);
e.preventDefault();
src.style.width = touch.radiusX * 2 + 'px';
src.style.height = touch.radiusY * 2 + 'px';
src.style.transform = 'rotate(' + touch.rotationAngle + 'deg)';
};(4)Touch.force 触摸压力Touch.force属性返回一个0到1之间的数值,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力。(5)Touch.target 开始触摸时的元素Touch.target属性返回一个元素节点,代表触摸发生时所在的那个元素节点。即使触摸点已经离开了这个节点,该属性依然不变。3、TouchList 接口TouchList接口表示一组触摸点的集合。它的实例是一个类似数组的对象,成员是Touch的实例对象,表示所有触摸点。用户用三根手指触摸,产生的TouchList实例就会包含三个成员,每根手指的触摸点对应一个Touch实例对象。它的实例主要通过触摸事件的TouchEvent.touches、TouchEvent.changedTouches、TouchEvent.targetTouches这几个属性获取。它的实例属性和实例方法只有两个。TouchList.length:数值,表示成员数量(即触摸点的数量)。TouchList.item():返回指定位置的成员,它的参数是该成员的位置编号(从零开始)。4、TouchEvent 接口4.1 概述TouchEvent 接口继承了 Event 接口,表示由触摸引发的事件实例,通常来自触摸屏或轨迹板。除了被继承的属性以外,它还有一些自己的属性。浏览器原生提供TouchEvent()构造函数,用来生成触摸事件的实例。new TouchEvent(type, options)TouchEvent()构造函数可以接受两个参数,第一个参数是字符串,表示事件类型;第二个参数是事件的配置对象,该参数是可选的,对象的所有属性也是可选的。除了Event接口的配置属性,该接口还有一些自己的配置属性。touches:TouchList实例,代表所有的当前处于活跃状态的触摸点,默认值是一个空数组[]。targetTouches:TouchList实例,代表所有处在触摸的目标元素节点内部、且仍然处于活动状态的触摸点,默认值是一个空数组[]。changedTouches:TouchList实例,代表本次触摸事件的相关触摸点,默认值是一个空数组[]。ctrlKey:布尔值,表示 Ctrl 键是否同时按下,默认值为false。shiftKey:布尔值,表示 Shift 键是否同时按下,默认值为false。altKey:布尔值,表示 Alt 键是否同时按下,默认值为false。metaKey:布尔值,表示 Meta 键(或 Windows 键)是否同时按下,默认值为false。4.2 实例属性TouchEvent 接口的实例具有Event实例的所有属性和方法,此外还有一些它自己的实例属性,这些属性全部都是只读。(1)TouchEvent.altKey,TouchEvent.ctrlKey,TouchEvent.shiftKey,TouchEvent.metaKey (是否同时按某些功能键)TouchEvent.altKey:布尔值,表示触摸时是否按下了 Alt 键。TouchEvent.ctrlKey:布尔值,表示触摸时是否按下了 Ctrl 键。TouchEvent.shiftKey:布尔值:表示触摸时是否按下了 Shift 键。TouchEvent.metaKey:布尔值,表示触摸时是否按下了 Meta 键(或 Windows 键)。下面是一个示例。someElement.addEventListener('touchstart', function (e) {
console.log('altKey = ' + e.altKey);
console.log('ctrlKey = ' + e.ctrlKey);
console.log('metaKey = ' + e.metaKey);
console.log('shiftKey = ' + e.shiftKey);
}, false);(2)TouchEvent.changedTouches (触摸点集合,不同触摸事件,含义不同)TouchEvent.changedTouches属性返回一个TouchList实例,成员是一组Touch实例对象,表示本次触摸事件的相关触摸点。对于不同的事件,该属性的含义有所不同。touchstart事件:被激活的触摸点touchmove事件:发生变化的触摸点touchend事件:消失的触摸点(即不再被触碰的点)下面是一个示例。someElement.addEventListener('touchmove', function (e) {
for (var i = 0; i < e.changedTouches.length; i++) {
console.log(e.changedTouches[i].identifier);
}
}, false);(3)TouchEvent.touches (仍然活动的触摸点集合)TouchEvent.touches属性返回一个TouchList实例,成员是所有仍然处于活动状态(即触摸中)的触摸点。一般来说,一个手指就是一个触摸点。下面是一个示例。someElement.addEventListener('touchstart', function (e) {
switch (e.touches.length) {
// 一根手指触摸
case 1: handle_one_touch(e); break;
// 两根手指触摸
case 2: handle_two_touches(e); break;
// 三根手指触摸
case 3: handle_three_touches(e); break;
// 其他情况
default: console.log('Not supported'); break;
}
}, false);(4)TouchEvent.targetTouches (目标元素内活动的触摸点集合)TouchEvent.targetTouches属性返回一个TouchList实例,成员是触摸事件的目标元素节点内部、所有仍然处于活动状态(即触摸中)的触摸点。function touches_in_target(ev) {
return (ev.touches.length === ev.targetTouches.length ? true : false);
}上面代码用来判断,是否所有触摸点都在目标元素内。5、触摸事件的种类触摸引发的事件,有以下几种。可以通过TouchEvent.type属性,查看到底发生的是哪一种事件。touchstart:用户开始触摸时触发,它的target属性返回发生触摸的元素节点。【开始触摸】touchend:用户不再接触触摸屏时(或者移出屏幕边缘时)触发,它的target属性与touchstart事件一致的,就是开始触摸时所在的元素节点。它的changedTouches属性返回一个TouchList实例,包含所有不再触摸的触摸点(即Touch实例对象)。【触摸结束】touchmove:用户移动触摸点时触发,它的target属性与touchstart事件一致。如果触摸的半径、角度、力度发生变化,也会触发该事件。【触摸移动中】touchcancel:触摸点取消时触发,比如在触摸区域跳出一个模态窗口(modal window)、触摸点离开了文档区域(进入浏览器菜单栏)、用户的触摸点太多,超过了支持的上限(自动取消早先的触摸点)。【触摸点被取消】下面是一个例子。var el = document.getElementsByTagName('canvas')[0];
el.addEventListener('touchstart', handleStart, false);
el.addEventListener('touchmove', handleMove, false);
function handleStart(evt) {
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
console.log(touches[i].pageX, touches[i].pageY);
}
}
function handleMove(evt) {
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
var touch = touches[i];
console.log(touch.pageX, touch.pageY);
}
}九、拖拉事件1、拖拉事件的种类拖拉(drag)指的是,用户在某个对象上按下鼠标键不放,拖动它到另一个位置,然后释放鼠标键,将该对象放在那里。拖拉的对象有好几种,包括元素节点、图片、链接、选中的文字等等。在网页中,除了元素节点默认不可以拖拉,其他(图片、链接、选中的文字)都是可以直接拖拉的。为了让元素节点可拖拉,可以将该节点的draggable属性设为true。<div draggable="true">
此区域可拖拉
</div>draggable属性可用于任何元素节点,但是图片( <img> )和链接( <a> )不加这个属性,就可以拖拉。对于它们,用到这个属性的时候,往往是将其设为false,防止拖拉这两种元素。注意,一旦某个元素节点的draggable属性设为true,就无法再用鼠标选中该节点内部的文字或子节点了。当元素节点或选中的文本被拖拉时,就会持续触发拖拉事件,包括以下一些事件。拖拉的节点上触发:drag:拖拉过程中,在被拖拉的节点上持续触发(相隔几百毫秒)。dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。通常应该在这个事件的监听函数中,指定拖拉的数据。dragend:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。它与dragstart事件,在同一个节点上触发。不管拖拉是否跨窗口,或者中途被取消,dragend事件总是会触发的。拖拉到别的节点上触发:dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。drop:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发。注意,如果当前节点不允许drop,即使在该节点上方松开鼠标键,也不会触发该事件。如果用户按下 ESC 键,取消这个操作,也不会触发该事件。该事件的监听函数负责取出拖拉数据,并进行相关处理。下面的例子展示,如何动态改变被拖动节点的背景色。div.addEventListener('dragstart', function (e) {
this.style.backgroundColor = 'red';
}, false);
div.addEventListener('dragend', function (e) {
this.style.backgroundColor = 'green';
}, false);上面代码中,div节点被拖动时,背景色会变为红色,拖动结束,又变回绿色。<div class="div1" draggable="true">
div1,此区域可拖拉
</div>
<div class="div2">
div2
</div>
<script>
var div1 = document.querySelector('.div1');
var div2 = document.querySelector('.div2');
div1.addEventListener('dragstart', function (e){
console.log('开始拖拉')
});
div1.addEventListener('drag', function (e) {
console.log('拖拉中') // 持续触发
});
div1.addEventListener('dragend', function (e){
console.log('结束拖拉')
});
div2.addEventListener('dragenter', function(){
console.log('拖到了div2')
})
div2.addEventListener('dragover', function(){
//console.log('正在div2上方') // 持续触发
})
div2.addEventListener('dragleave', function(){
console.log('离开div2')
})
div2.addEventListener('drop', function() {
console.log('在div2上释放')
})
</script>下面是一个例子,展示如何实现将一个节点从当前父节点,拖拉到另一个父节点中。/* HTML 代码如下
<div class="dropzone">
<div id="draggable" draggable="true">
该节点可拖拉
</div>
</div>
<div class="dropzone"></div>
<div class="dropzone"></div>
<div class="dropzone"></div>
*/
// 被拖拉节点
var dragged;
document.addEventListener('dragstart', function (event) {
// 保存被拖拉节点
dragged = event.target;
// 被拖拉节点的背景色变透明
event.target.style.opacity = 0.5;
}, false);
document.addEventListener('dragend', function (event) {
// 被拖拉节点的背景色恢复正常
event.target.style.opacity = '';
}, false);
document.addEventListener('dragover', function (event) {
// 防止拖拉效果被重置,允许被拖拉的节点放入目标节点
event.preventDefault();
}, false);
document.addEventListener('dragenter', function (event) {
// 目标节点的背景色变紫色
// 由于该事件会冒泡,所以要过滤节点
if (event.target.className === 'dropzone') {
event.target.style.background = 'purple';
}
}, false);
document.addEventListener('dragleave', function( event ) {
// 目标节点的背景色恢复原样
if (event.target.className === 'dropzone') {
event.target.style.background = '';
}
}, false);
document.addEventListener('drop', function( event ) {
// 防止事件默认行为(比如某些元素节点上可以打开链接),
event.preventDefault();
if (event.target.className === 'dropzone') {
// 恢复目标节点背景色
event.target.style.background = '';
// 将被拖拉节点插入目标节点
dragged.parentNode.removeChild(dragged);
event.target.appendChild( dragged );
}
}, false);关于拖拉事件,有以下几个注意点。拖拉过程只触发以上这些拖拉事件,尽管鼠标在移动,但是鼠标事件不会触发。将文件从操作系统拖拉进浏览器,不会触发dragstart和dragend事件。dragenter和dragover事件的监听函数,用来取出拖拉的数据(即允许放下被拖拉的元素)。由于网页的大部分区域不适合作为放下拖拉元素的目标节点,所以这两个事件的默认设置为当前节点不允许接受被拖拉的元素。如果想要在目标节点上放下的数据,首先必须阻止这两个事件的默认行为。<div ondragover="return false">
<div ondragover="event.preventDefault()">上面代码中,如果不取消拖拉事件或者阻止默认行为,就不能在div节点上放下被拖拉的节点。2、DragEvent 接口拖拉事件都继承了DragEvent接口,这个接口又继承了MouseEvent接口和Event接口。浏览器原生提供一个DragEvent()构造函数,用来生成拖拉事件的实例对象。new DragEvent(type, options)DragEvent()构造函数接受两个参数,第一个参数是字符串,表示事件的类型,该参数必须;第二个参数是事件的配置对象,用来设置事件的属性,该参数可选。配置对象除了接受MouseEvent接口和Event接口的配置属性,还可以设置dataTransfer属性要么是null,要么是一个DataTransfer接口的实例。DataTransfer的实例对象用来读写拖拉事件中传输的数据,详见下文《DataTransfer 接口》的部分。3、DataTransfer 接口概述所有拖拉事件的实例都有一个DragEvent.dataTransfer属性,用来读写需要传递的数据。这个属性的值是一个DataTransfer接口的实例。浏览器原生提供一个DataTransfer()构造函数,用来生成DataTransfer实例对象。var dataTrans = new DataTransfer();DataTransfer()构造函数不接受参数。拖拉的数据分成两方面:数据的种类(又称格式)和数据的值。数据的种类是一个 MIME 字符串(比如text/plain、image/jpeg),数据的值是一个字符串。一般来说,如果拖拉一段文本,则数据默认就是那段文本;如果拖拉一个链接,则数据默认就是链接的 URL。拖拉事件开始时,开发者可以提供数据类型和数据值。拖拉过程中,开发者通过dragenter和dragover事件的监听函数,检查数据类型,以确定是否允许放下(drop)被拖拉的对象。比如,在只允许放下链接的区域,检查拖拉的数据类型是否为text/uri-list。发生drop事件时,监听函数取出拖拉的数据,对其进行处理。4、DataTransfer 的实例属性4.1 DataTransfer.dropEffect 设置接受拖拉的区域的效果DataTransfer.dropEffect属性用来设置放下(drop)被拖拉节点时的效果,会影响到拖拉经过相关区域时鼠标的形状。它可能取下面的值。copy:复制被拖拉的节点move:移动被拖拉的节点link:创建指向被拖拉的节点的链接none:无法放下被拖拉的节点除了上面这些值,设置其他的值都是无效的。target.addEventListener('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
});上面代码中,被拖拉元素一旦drop,接受的区域会复制该节点。dropEffect属性一般在dragenter和dragover事件的监听函数中设置,对于dragstart、drag、dragleave这三个事件,该属性不起作用。因为该属性只对接受被拖拉的节点的区域有效,对被拖拉的节点本身是无效的。进入目标区域后,拖拉行为会初始化成设定的效果。4.2 DataTransfer.effectAllowed 设置被拖拉的节点允许的效果DataTransfer.effectAllowed属性设置本次拖拉中允许的效果。它可能取下面的值。copy:复制被拖拉的节点move:移动被拖拉的节点link:创建指向被拖拉节点的链接copyLink:允许copy或linkcopyMove:允许copy或movelinkMove:允许link或moveall:允许所有效果none:无法放下被拖拉的节点uninitialized:默认值,等同于all如果某种效果是不允许的,用户就无法在目标节点中达成这种效果。这个属性与dropEffect属性是同一件事的两个方面。前者设置被拖拉的节点允许的效果,后者设置接受拖拉的区域的效果,它们往往配合使用。dragstart事件的监听函数,可以用来设置这个属性。其他事件的监听函数里面设置这个属性是无效的。source.addEventListener('dragstart', function (e) { // 被拖拉节点上设置
e.dataTransfer.effectAllowed = 'move';
});
target.addEventListener('dragover', function (e) { // 接受区域节点上设置
e.dataTransfer.dropEffect = 'move';
});只要dropEffect属性和effectAllowed属性之中,有一个为none,就无法在目标节点上完成drop操作。4.3 DataTransfer.files 本地文件DataTransfer.files属性是一个 FileList 对象,包含一组本地文件,可以用来在拖拉操作中传送。如果本次拖拉不涉及文件,则该属性为空的 FileList 对象。例子:接收拖拉文件// HTML 代码如下
// <div id="output" style="min-height: 200px;border: 1px solid black;">
// 文件拖拉到这里
// </div>
var div = document.getElementById('output');
div.addEventListener("dragenter", function( event ) {
div.textContent = '';
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("dragover", function( event ) {
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("drop", function( event ) {
event.stopPropagation();
event.preventDefault();
var files = event.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
div.textContent += files[i].name + ' ' + files[i].size + '字节\n';
}
}, false);上面代码中,通过dataTransfer.files属性读取被拖拉的文件的信息。如果想要读取文件内容,就要使用FileReader对象。div.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
var fileList = e.dataTransfer.files;
if (fileList.length > 0) {
var file = fileList[0];
var reader = new FileReader();
reader.readAsDataURL(file);// 解析为url
reader.onloadend = function(e) {
if (e.target.readyState === FileReader.DONE) {
var content = reader.result;
div.innerHTML = '<img src="'+ content +'"> File: ' + file.name + '\n\n' + content;
}
}
reader.readAsBinaryString(file);
}
});4.4 DataTransfer.types 数据格式DataTransfer.types属性是一个只读的数组,每个成员是一个字符串,里面是拖拉的数据格式(通常是 MIME 值)。比如,如果拖拉的是文字,对应的成员就是text/plain。下面是一个例子,通过检查dataTransfer属性的类型,决定是否允许在当前节点执行drop操作。function contains(list, value){
for (var i = 0; i < list.length; ++i) {
if(list[i] === value) return true;
}
return false;
}
function doDragOver(event) {
var isLink = contains(event.dataTransfer.types, 'text/uri-list');
if (isLink) event.preventDefault();
}上面代码中,只有当被拖拉的节点是一个链接时,才允许在当前节点放下。4.5 DataTransfer.itemsDataTransfer.items属性返回一个类似数组的只读对象(DataTransferItemList 实例),每个成员就是本次拖拉的一个对象(DataTransferItem 实例)。如果本次拖拉不包含对象,则返回一个空对象。DataTransferItemList 实例具有以下的属性和方法。length:返回成员的数量add(data, type):增加一个指定内容和类型(比如text/html和text/plain)的字符串作为成员add(file):add方法的另一种用法,增加一个文件作为成员remove(index):移除指定位置的成员clear():移除所有的成员DataTransferItem 实例具有以下的属性和方法。kind:返回成员的种类(string还是file)。type:返回成员的类型(通常是 MIME 值)。getAsFile():如果被拖拉是文件,返回该文件,否则返回null。getAsString(callback):如果被拖拉的是字符串,将该字符传入指定的回调函数处理。该方法是异步的,所以需要传入回调函数。下面是一个例子。div.addEventListener('drop', function (e) {
e.preventDefault();
if (e.dataTransfer.items != null) {
for (var i = 0; i < e.dataTransfer.items.length; i++) {
console.log(e.dataTransfer.items[i].kind + ': ' + e.dataTransfer.items[i].type);
}
}
});5、DataTransfer 的实例方法5.1 DataTransfer.setData() 设置拖拉事件所带有的数据DataTransfer.setData()方法用来设置拖拉事件所带有的数据。该方法没有返回值。event.dataTransfer.setData('text/plain', 'Text to drag');上面代码为当前的拖拉事件加入纯文本数据。该方法接受两个参数,都是字符串。第一个参数表示数据类型(比如text/plain),第二个参数是具体数据。如果指定类型的数据在dataTransfer属性不存在,那么这些数据将被加入,否则原有的数据将被新数据替换。如果是拖拉文本框或者拖拉选中的文本,会默认将对应的文本数据,添加到dataTransfer属性,不用手动指定。<div draggable="true">
aaa
</div>上面代码中,拖拉这个 <div> 元素会自动带上文本数据aaa。使用setData方法,可以替换到原有数据。<div
draggable="true"
ondragstart="event.dataTransfer.setData('text/plain', 'bbb')"
>
aaa
</div>上面代码中,拖拉数据实际上是bbb,而不是aaa。下面是添加其他类型的数据。由于text/plain是最普遍支持的格式,为了保证兼容性,建议最后总是保存一份纯文本格式的数据。var dt = event.dataTransfer;
// 添加链接
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');
// 添加 HTML 代码
dt.setData('text/html', 'Hello there, <strong>stranger</strong>');
dt.setData('text/plain', 'Hello there, <strong>stranger</strong>');
// 添加图像的 URL
dt.setData('text/uri-list', imageurl);
dt.setData('text/plain', imageurl);可以一次提供多种格式的数据。var dt = event.dataTransfer;
dt.setData('application/x-bookmark', bookmarkString);
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');上面代码中,通过在同一个事件上面,存放三种类型的数据,使得拖拉事件可以在不同的对象上面,drop不同的值。注意,第一种格式是一个自定义格式,浏览器默认无法读取,这意味着,只有某个部署了特定代码的节点,才可能drop(读取到)这个数据。5.2 DataTransfer.getData() 返回指定类型的数据DataTransfer.getData()方法接受一个字符串(表示数据类型)作为参数,返回事件所带的指定类型的数据(通常是用setData方法添加的数据)。如果指定类型的数据不存在,则返回空字符串。通常只有drop事件触发后,才能取出数据。下面是一个drop事件的监听函数,用来取出指定类型的数据。function onDrop(event) {
var data = event.dataTransfer.getData('text/plain');
event.target.textContent = data;
event.preventDefault();
}上面代码取出拖拉事件的文本数据,将其替换成当前节点的文本内容。注意,这时还必须取消浏览器的默认行为,因为假如用户拖拉的是一个链接,浏览器默认会在当前窗口打开这个链接。getData方法返回的是一个字符串,如果其中包含多项数据,就必须手动解析。function doDrop(event) {
var lines = event.dataTransfer.getData('text/uri-list').split('\n');
for (let line of lines) {
let link = document.createElement('a');
link.href = line;
link.textContent = line;
event.target.appendChild(link);
}
event.preventDefault();
}上面代码中,getData方法返回的是一组链接,就必须自行解析。类型值指定为URL,可以取出第一个有效链接。var link = event.dataTransfer.getData('URL');下面的例子是从多种类型的数据里面取出数据。function doDrop(event) {
var types = event.dataTransfer.types;
var supportedTypes = ['text/uri-list', 'text/plain'];
types = supportedTypes.filter(function (value) { types.includes(value) });
if (types.length) {
var data = event.dataTransfer.getData(types[0]);
}
event.preventDefault();
}5.3 DataTransfer.clearData() 清除指定或全部数据DataTransfer.clearData()方法接受一个字符串(表示数据类型)作为参数,删除事件所带的指定类型的数据。如果没有指定类型,则删除所有数据。如果指定类型不存在,则调用该方法不会产生任何效果。event.dataTransfer.clearData('text/uri-list');上面代码清除事件所带的text/uri-list类型的数据。该方法不会移除拖拉的文件,因此调用该方法后,DataTransfer.types属性可能依然会返回Files类型(前提是存在文件拖拉)。注意,该方法只能在dragstart事件的监听函数之中使用,因为这是拖拉操作的数据唯一可写的时机。5.4 DataTransfer.setDragImage() 设置拖动过程中的图片拖动过程中(dragstart事件触发后),浏览器会显示一张图片跟随鼠标一起移动,表示被拖动的节点。这张图片是自动创造的,通常显示为被拖动节点的外观,不需要自己动手设置。DataTransfer.setDragImage()方法可以自定义这张图片。它接受三个参数。第一个是 <img> 节点或者 <canvas> 节点,如果省略或为null,则使用被拖动的节点的外观;第二个和第三个参数为鼠标相对于该图片左上角的横坐标和右坐标。下面是一个例子。/* HTML 代码如下
<div id="drag-with-image" class="dragdemo" draggable="true">
drag me
</div>
*/
var div = document.getElementById('drag-with-image');
div.addEventListener('dragstart', function (e) {
var img = document.createElement('img');
img.src = 'http://path/to/img';
e.dataTransfer.setDragImage(img, 0, 0);
}, false);十、其他常见事件1、资源事件1.1 beforeunload 事件 (关闭窗口前调用)beforeunload事件在窗口、文档、各种资源将要卸载前触发。它可以用来防止用户不小心卸载资源。如果该事件对象的returnValue属性是一个非空字符串,那么浏览器就会弹出一个对话框,询问用户是否要卸载该资源。但是,用户指定的字符串可能无法显示,浏览器会展示预定义的字符串。如果用户点击“取消”按钮,资源就不会卸载。window.addEventListener('beforeunload', function (event) {
event.returnValue = '你确定离开吗?';
});上面代码中,用户如果关闭窗口,浏览器会弹出一个窗口,要求用户确认。浏览器对这个事件的行为很不一致,有的浏览器调用event.preventDefault(),也会弹出对话框。IE 浏览器需要显式返回一个非空的字符串,才会弹出对话框。而且,大多数浏览器在对话框中不显示指定文本,只显示默认文本。因此,可以采用下面的写法,取得最大的兼容性。window.addEventListener('beforeunload', function (e) {
var confirmationMessage = '确认关闭窗口?';
e.returnValue = confirmationMessage;
return confirmationMessage;
});注意,许多手机浏览器默认忽略这个事件,桌面浏览器也有办法忽略这个事件。所以,它可能根本不会生效,不能依赖它来阻止用户关闭窗口。另外,一旦使用了beforeunload事件,浏览器就不会缓存当前网页,使用“回退”按钮将重新向服务器请求网页。这是因为监听这个事件的目的,一般是修改初始状态,这时缓存初始页面就没意义了。基本上,只有一种场合可以监听unload事件,其他情况都不应该监听:用户修改了表单,还没有保存就要离开。1.2 unload 事件 (即将关闭窗口时调用)unload事件在窗口关闭或者document对象将要卸载时触发。它的触发顺序排在beforeunload、pagehide事件后面。unload事件发生时,文档处于一个特殊状态。所有资源依然存在,但是对用户来说都不可见,UI 互动全部无效。这个事件是无法取消的,即使在监听函数里面抛出错误,也不能停止文档的卸载。window.addEventListener('unload', function(event) {
console.log('文档将要卸载');
});手机上,浏览器或系统可能会直接丢弃网页,这时该事件根本不会发生。而且跟beforeunload事件一样,一旦使用了unload事件,浏览器就不会缓存当前网页,理由同上。因此,任何情况下都不应该依赖这个事件,指定网页卸载时要执行的代码,可以考虑完全不使用这个事件。1.3 load 事件,error 事件 ,abort事件 (页面或某个资源加载成功/失败/取消时调用)load事件在页面或某个资源加载成功时触发。注意,页面或资源从浏览器缓存加载,并不会触发load事件。window.addEventListener('load', function(event) {
console.log('所有资源都加载完成');
});error事件是在页面或资源加载失败时触发。abort事件是在用户取消加载时触发。这三个事件实际上属于进度事件,不仅发生在document对象,还发生在各种外部资源上面。浏览网页就是一个加载各种资源的过程,图像(image)、样式表(style sheet)、脚本(script)、视频(video)、音频(audio)、Ajax请求(XMLHttpRequest)等等。这些资源和document对象、window对象、XMLHttpRequestUpload 对象,都会触发load事件和error事件。触发这三个事件的对象有:windowdocumentbodyimgstylescriptvideoaudioAjax等等2、session 历史事件2.1 pageshow 事件(页面显示,加载页面后执行),pagehide 事件(退出当前页面触发)默认情况下,浏览器会在当前会话(session)缓存页面,当用户点击“前进/后退”按钮时,浏览器就会从缓存中加载页面。pageshow 事件在页面加载时触发,包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载(不管是不是从浏览器缓存)时都运行的代码,可以放在这个事件的监听函数。第一次加载时,它的触发顺序排在load事件后面。从缓存加载时,load事件不会触发,因为网页在缓存中的样子通常是load事件的监听函数运行后的样子,所以不必重复执行。同理,如果是从缓存中加载页面,网页内初始化的 JavaScript 脚本(比如 DOMContentLoaded 事件的监听函数)也不会执行。window.addEventListener('pageshow', function(event) {
console.log('pageshow: ', event);
});pageshow 事件有一个persisted属性,返回一个布尔值。页面第一次加载时,这个属性是false;当页面从缓存加载时,这个属性是true。window.addEventListener('pageshow', function(event){
if (event.persisted) { // 是否从缓存加载
// ...
}
});pagehide事件与pageshow事件类似,当用户通过“前进/后退”按钮,离开当前页面时触发。它与 unload 事件的区别在于,如果在 window 对象上定义unload事件的监听函数之后,页面不会保存在缓存中,而使用pagehide事件,页面会保存在缓存中。pagehide事件实例也有一个persisted属性,将这个属性设为true,就表示页面要保存在缓存中;设为false,表示网页不保存在缓存中,这时如果设置了unload 事件的监听函数,该函数将在 pagehide 事件后立即运行。如果页面包含 <frame> 或 <iframe> 元素,则 <frame> 页面的pageshow事件和pagehide事件,都会在主页面之前触发。注意,这两个事件只在浏览器的history(历史)对象发生变化时触发,跟网页是否可见没有关系。2.2 popstate 事件(在浏览器的history对象的当前记录发生显式切换时触发)popstate事件在浏览器的history对象的当前记录发生显式切换时触发。注意,调用history.pushState()或history.replaceState(),并不会触发popstate事件。该事件只在用户在history记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用history.back()、history.forward()、history.go()时触发。该事件对象有一个state属性,保存history.pushState方法和history.replaceState方法为当前记录添加的state对象。window.onpopstate = function (event) {
console.log('state: ' + event.state);
};
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back(); // state: {"page":1}
history.back(); // state: null
history.go(2); // state: {"page":3}上面代码中,pushState方法向history添加了两条记录,然后replaceState方法替换掉当前记录。因此,连续两次back方法,会让当前条目退回到原始网址,它没有附带state对象,所以事件的state属性为null,然后前进两条记录,又回到replaceState方法添加的记录。浏览器对于页面首次加载,是否触发popstate事件,处理不一样,Firefox 不触发该事件。2.3 hashchange 事件 (hash发生变化时触发)hashchange事件在 URL 的 hash 部分(即#号后面的部分,包括#号)发生变化时触发。该事件一般在window对象上监听。hashchange的事件实例具有两个特有属性:oldURL属性和newURL属性,分别表示变化前后的完整 URL。// URL 是 http://www.example.com/
window.addEventListener('hashchange', myFunction);
function myFunction(e) {
console.log(e.oldURL);
console.log(e.newURL);
}
location.hash = 'part2';
// http://www.example.com/
// http://www.example.com/#part23、网页状态事件3.1 DOMContentLoaded 事件 (DOM内容加载完成后触发)网页下载并解析完成以后,浏览器就会在document对象上触发 DOMContentLoaded 事件。这时,仅仅完成了网页的解析(整张页面的 DOM 生成了),所有外部资源(样式表、脚本、iframe 等等)可能还没有下载结束。也就是说,这个事件比load事件,发生时间早得多。document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM生成');
});注意,网页的 JavaScript 脚本是同步执行的,脚本一旦发生堵塞,将推迟触发DOMContentLoaded事件。document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM 生成');
});
// 这段代码会推迟触发 DOMContentLoaded 事件
for(var i = 0; i < 1000000000; i++) {
// ...
}3.2 readystatechange 事件(Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发)readystatechange事件当 Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发。document.readyState有三个可能的值:loading(网页正在加载)、interactive(网页已经解析完成,但是外部资源仍然处在加载状态)和complete(网页和所有外部资源已经结束加载,load事件即将触发)。document.onreadystatechange = function () {
if (document.readyState === 'interactive') {
// ...
}
}这个事件可以看作DOMContentLoaded事件的另一种实现方法。4、窗口事件4.1 scroll 事件 (文档或文档元素滚动时触发)scroll事件在文档或文档元素滚动时触发,主要出现在用户拖动滚动条。window.addEventListener('scroll', callback);该事件会连续地大量触发,所以它的监听函数之中不应该有非常耗费计算的操作。推荐的做法是使用requestAnimationFrame或setTimeout控制该事件的触发频率,然后可以结合customEvent抛出一个新事件。(function () {
var throttle = function (type, name, obj) { // 节流函数 控制触发频率
var obj = obj || window;
var running = false;
var func = function () {
if (running) { return; }
running = true;
requestAnimationFrame(function() { // 结合此方法控制在每秒60次
obj.dispatchEvent(new CustomEvent(name));
running = false;
});
};
obj.addEventListener(type, func);
};
// 将 scroll 事件重定义为 optimizedScroll 事件
throttle('scroll', 'optimizedScroll');
})();
window.addEventListener('optimizedScroll', function() {
console.log('Resource conscious scroll callback!');
});上面代码中,throttle函数用于控制事件触发频率,requestAnimationFrame方法保证每次页面重绘(每秒60次),只会触发一次scroll事件的监听函数。也就是说,上面方法将scroll事件的触发频率,限制在每秒60次。具体来说,就是scroll事件只要频率低于每秒60次,就会触发optimizedScroll事件,从而执行optimizedScroll事件的监听函数。改用setTimeout方法,可以放置更大的时间间隔。(function() {
window.addEventListener('scroll', scrollThrottler, false);
var scrollTimeout;
function scrollThrottler() {
if (!scrollTimeout) {
scrollTimeout = setTimeout(function () {
scrollTimeout = null;
actualScrollHandler();
}, 66);
}
}
function actualScrollHandler() {
// ...
}
}());上面代码中,每次scroll事件都会执行scrollThrottler函数。该函数里面有一个定时器setTimeout,每66毫秒触发一次(每秒15次)真正执行的任务actualScrollHandler。下面是一个更一般的throttle函数的写法。function throttle(fn, wait) {
var time = Date.now();
return function() {
if ((time + wait - Date.now()) < 0) {
fn();
time = Date.now();
}
}
}
window.addEventListener('scroll', throttle(callback, 1000));上面的代码将scroll事件的触发频率,限制在一秒一次。lodash函数库提供了现成的throttle函数,可以直接使用。window.addEventListener('scroll', _.throttle(callback, 1000));本书前面介绍过debounce的概念,throttle与它区别在于,throttle是“节流”,确保一段时间内只执行一次,而debounce是“防抖”,要连续操作结束后再执行。以网页滚动为例,debounce要等到用户停止滚动后才执行,throttle则是如果用户一直在滚动网页,那么在滚动过程中还是会执行。4.2 resize 事件(窗口大小变化时触发)resize事件在改变浏览器窗口大小时触发,主要发生在window对象上面。var resizeMethod = function () {
if (document.body.clientWidth < 768) {
console.log('移动设备的视口');
}
};
window.addEventListener('resize', resizeMethod, true);该事件也会连续地大量触发,所以最好像上面的scroll事件一样,通过throttle函数控制事件触发频率。4.3 fullscreenchange 事件(元素进入/退出全屏时触发),fullscreenerror 事件(无法切换全屏时触发)fullscreenchange事件在元素进入或退出全屏状态时触发,该事件发生在document对象上面。注意,此事件非浏览器的进入/退出全屏时触发的事件。document.addEventListener('fullscreenchange', function (event) {
console.log(document.fullscreenElement); // 全屏的元素,如果为null时表示已退出全屏
});
document.addEventListener('click', function (event) {
document.querySelector('div').requestFullscreen(); // 设置元素为全屏展示
});fullscreenerror事件在浏览器无法切换到全屏状态时触发。5、剪贴板事件以下三个事件属于剪贴板操作的相关事件。cut:将选中的内容从文档中移除,加入剪贴板时触发。【剪切】copy:进行复制动作时触发。【拷贝】paste:剪贴板内容粘贴到文档后触发。【粘贴】这三个事件都是ClipboardEvent接口的实例。ClipboardEvent有一个实例属性clipboardData,是一个 DataTransfer 对象,存放剪贴的数据。具体的 API 接口和操作方法,请参见《拖拉事件》的 DataTransfer 对象部分。document.addEventListener('copy', function (e) {
e.clipboardData.setData('text/plain', 'Hello, world!');
e.clipboardData.setData('text/html', '<b>Hello, world!</b>');
e.preventDefault();
});上面的代码使得复制进入剪贴板的,都是开发者指定的数据,而不是用户想要拷贝的数据。6、焦点事件焦点事件发生在元素节点和document对象上面,与获得或失去焦点相关。它主要包括以下四个事件。focus:元素节点获得焦点后触发,该事件不会冒泡。blur:元素节点失去焦点后触发,该事件不会冒泡。focusin:元素节点将要获得焦点时触发,发生在focus事件之前。该事件会冒泡。focusout:元素节点将要失去焦点时触发,发生在blur事件之前。该事件会冒泡。这四个事件都继承了FocusEvent接口。FocusEvent实例具有以下属性。FocusEvent.target:事件的目标节点。FocusEvent.relatedTarget:对于focusin事件,返回失去焦点的节点;对于focusout事件,返回将要接受焦点的节点;对于focus和blur事件,返回null。由于focus和blur事件不会冒泡,只能在捕获阶段触发,所以addEventListener方法的第三个参数需要设为true。form.addEventListener('focus', function (event) {
event.target.style.background = 'pink';
}, true);
form.addEventListener('blur', function (event) {
event.target.style.background = '';
}, true);上面代码针对表单的文本输入框,接受焦点时设置背景色,失去焦点时去除背景色。7、CustomEvent 接口(自定义事件)CustomEvent 接口用于生成自定义的事件实例。那些浏览器预定义的事件,虽然可以手动生成,但是往往不能在事件上绑定数据。如果需要在触发事件的同时,传入指定的数据,就可以使用 CustomEvent 接口生成的自定义事件对象。浏览器原生提供CustomEvent()构造函数,用来生成 CustomEvent 事件实例。new CustomEvent(type, options)CustomEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的名字,这是必须的。第二个参数是事件的配置对象,这个参数是可选的。CustomEvent的配置对象除了接受 Event 事件的配置属性,只有一个自己的属性。detail:表示事件的附带数据,默认为null。下面是一个例子。var event = new CustomEvent('build', { 'detail': 'hello' });
function eventHandler(e) {
console.log(e.detail);
}
document.body.addEventListener('build', function (e) {
console.log(e.detail);
});
document.body.dispatchEvent(event);上面代码中,我们手动定义了build事件。该事件触发后,会被监听到,从而输出该事件实例的detail属性(即字符串hello)。下面是另一个例子。var myEvent = new CustomEvent('myevent', {
detail: {
foo: 'bar'
},
bubbles: true,
cancelable: false
});
el.addEventListener('myevent', function (event) {
console.log('Hello ' + event.detail.foo);
});
el.dispatchEvent(myEvent);上面代码也说明,CustomEvent 的事件实例,除了具有 Event 接口的实例属性,还具有detail属性。十一、GlobalEventHandlers 接口 (全局事件处理接口)指定事件的回调函数,推荐使用的方法是元素的addEventListener方法。div.addEventListener('click', clickHandler, false);除了之外,还有一种方法可以直接指定事件的回调函数。div.onclick = clickHandler;这个接口是由GlobalEventHandlers接口提供的。它的优点是使用比较方便,缺点是只能为每个事件指定一个回调函数,并且无法指定事件触发的阶段(捕获阶段还是冒泡阶段)。HTMLElement、Document和Window都继承了这个接口,也就是说,各种 HTML 元素、document对象、window对象上面都可以使用GlobalEventHandlers接口提供的属性。下面就列出这个接口提供的主要的事件属性。1、GlobalEventHandlers.onabort (中断事件)某个对象的abort事件(停止加载)发生时,就会调用onabort属性指定的回调函数。各种元素的停止加载事件,到底如何触发,目前并没有统一的规定。因此实际上,这个属性现在一般只用在<img>元素上面。// HTML 代码如下
// <img src="example.jpg" id="img">
var img = document.getElementById('img');
img.onabort = function () {
console.log('图片加载中断');
}2、GlobalEventHandlers.onerror (错误事件)error事件发生时,就会调用onerror属性指定的回调函数。error事件分成两种。一种是 JavaScript 的运行时错误,这会传到window对象,导致window.onerror()。window.onerror = function (message, source, lineno, colno, error) {
// ...
}window.onerror的处理函数共接受五个参数,含义如下。message:错误信息字符串source:报错脚本的 URLlineno:报错的行号,是一个整数colno:报错的列号,是一个整数error: 错误对象另一种是资源加载错误,比如 <img> 或 <script> 加载的资源出现加载错误。这时,Error 对象会传到对应的元素,导致该元素的onerror属性开始执行。element.onerror = function (event) {
// ...
}注意,一般来说,资源的加载错误不会触发window.onerror。3、GlobalEventHandlers.onload(加载完成事件)、GlobalEventHandlers.onloadstart(开始加载事件)元素完成加载时,会触发load事件,执行onload()。它的典型使用场景是window对象和 <img> 元素。对于window对象来说,只有页面的所有资源加载完成(包括图片、脚本、样式表、字体等所有外部资源),才会触发load事件。对于 <img> 和 <video> 等元素,加载开始时还会触发loadstart事件,导致执行onloadstart。4、GlobalEventHandlers.onfocus(获取焦点事件),GlobalEventHandlers.onblur(失去焦点事件)当前元素获得焦点时,会触发element.onfocus;失去焦点时,会触发element.onblur。element.onfocus = function () {
console.log("onfocus event detected!");
};
element.onblur = function () {
console.log("onblur event detected!");
};注意,如果不是可以接受用户输入的元素,要触发onfocus,该元素必须有tabindex属性。5、GlobalEventHandlers.onscroll(滚动事件)页面或元素滚动时,会触发scroll事件,导致执行onscroll()。6、GlobalEventHandlers.oncontextmenu(右键菜单事件),GlobalEventHandlers.onshow(显示右键菜单时触发)用户在页面上按下鼠标的右键,会触发contextmenu事件,导致执行oncontextmenu()。如果该属性执行后返回false,就等于禁止了右键菜单。document.oncontextmenu与window.oncontextmenu效果一样。document.oncontextmenu = function () {// 禁用右键菜单
return false;
};上面代码中,oncontextmenu属性执行后返回false,右键菜单就不会出现。元素的右键菜单显示时,会触发该元素的onshow监听函数。7、其他的事件属性鼠标的事件属性。onclickondblclickonmousedownonmouseenteronmouseleaveonmousemoveonmouseoutonmouseoveronmouseuponwheel键盘的事件属性。onkeydownonkeypressonkeyup焦点的事件属性。onbluronfocus表单的事件属性。oninputonchangeonsubmitonresetoninvalidonselect触摸的事件属性。ontouchcancelontouchendontouchmoveontouchstart拖动的事件属性分成两类:一类与被拖动元素相关,另一类与接收被拖动元素的容器元素相关。被拖动元素的事件属性。ondragstart:拖动开始ondrag:拖动过程中,每隔几百毫秒触发一次ondragend:拖动结束接收被拖动元素的容器元素的事件属性。ondragenter:被拖动元素进入容器元素。ondragleave:被拖动元素离开容器元素。ondragover:被拖动元素在容器元素上方,每隔几百毫秒触发一次。ondrop:松开鼠标后,被拖动元素放入容器元素。 <dialog> 对话框元素的事件属性。oncancelonclose文档学习文档:https://wangdoc.com/javascript/
彻底搞懂JS的事件原理
事件一、EventTarget 接口事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。DOM 支持大量的事件,本章开始介绍 DOM 的事件编程。1、概述DOM 的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequest、AudioNode、AudioContext)也部署了这个接口。该接口主要提供三个实例方法。addEventListener:绑定事件的监听函数removeEventListener:移除事件的监听函数dispatchEvent:触发事件2、EventTarget.addEventListener()EventTarget.addEventListener()用于在当前节点或对象上,定义一个特定事件的监听函数。一旦这个事件发生,就会执行监听函数。该方法没有返回值。target.addEventListener(type, listener[, useCapture]);
// 事件名称,监听函数 [,是否在捕获阶段触发]该方法接受三个参数。type:事件名称,大小写敏感。listener:监听函数。事件发生时,会调用该监听函数。useCapture:布尔值,表示监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分),默认为false(监听函数只在冒泡阶段被触发)。该参数可选。下面是一个例子。function hello() {
console.log('Hello world');
}
var button = document.getElementById('btn');
button.addEventListener('click', hello, false);上面代码中,button节点的addEventListener方法绑定click事件的监听函数hello,该函数只在冒泡阶段触发。关于参数,有两个地方需要注意。首先,第二个参数除了监听函数,还可以是一个具有handleEvent方法的对象。buttonElement.addEventListener('click', {
handleEvent: function (event) {
console.log('click');
}
});上面代码中,addEventListener方法的第二个参数,就是一个具有handleEvent方法的对象。其次,第三个参数除了布尔值useCapture,还可以是一个属性配置对象。该对象有以下属性。capture:布尔值,表示该事件是否在捕获阶段触发监听函数。once:布尔值,表示监听函数是否只触发一次,然后就自动移除。passive:布尔值,表示监听函数不会调用事件的preventDefault方法。如果监听函数调用了,浏览器将忽略这个要求,并在监控台输出一行警告。如果希望事件监听函数只执行一次,可以打开属性配置对象的once属性。element.addEventListener('click', function (event) {
// 只执行一次的代码
}, {once: true});addEventListener方法可以为针对当前对象的同一个事件,添加多个不同的监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用removeEventListener方法手动去除)。function hello() {
console.log('Hello world');
}
document.addEventListener('click', hello, false);
document.addEventListener('click', hello, false);执行上面代码,点击文档只会输出一行Hello world。如果希望向监听函数传递参数,可以用匿名函数包装一下监听函数。function print(x) {
console.log(x);
}
var el = document.getElementById('div1');
el.addEventListener('click', function () { print('Hello'); }, false);上面代码通过匿名函数,向监听函数print传递了一个参数。监听函数内部的this,指向当前事件所在的那个对象。// HTML 代码如下
// <p id="para">Hello</p>
var para = document.getElementById('para');
para.addEventListener('click', function (e) {
console.log(this.nodeName); // "P"
}, false);上面代码中,监听函数内部的this指向事件所在的对象para。3、EventTarget.removeEventListener()EventTarget.removeEventListener方法用来移除addEventListener方法添加的事件监听函数。该方法没有返回值。div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);removeEventListener方法的参数,与addEventListener方法完全一致。它的第一个参数“事件类型”,大小写敏感。注意,removeEventListener方法移除的监听函数,必须是addEventListener方法添加的那个监听函数,而且必须在同一个元素节点,否则无效。div.addEventListener('click', function (e) {}, false);
div.removeEventListener('click', function (e) {}, false);// 移除无效,因为不是同一个监听函数上面代码中,removeEventListener方法无效,因为监听函数不是同一个匿名函数。element.addEventListener('mousedown', handleMouseDown, true);
element.removeEventListener("mousedown", handleMouseDown, false); // 移除无效,第三个参数不一样上面代码中,removeEventListener方法也是无效的,因为第三个参数不一样。4、EventTarget.dispatchEvent()EventTarget.dispatchEvent方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true。target.dispatchEvent(event)dispatchEvent方法的参数是一个Event对象的实例(详见《Event 对象》章节)。para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);上面代码在当前节点触发了click事件。如果dispatchEvent方法的参数为空,或者不是一个有效的事件对象,将报错。下面代码根据dispatchEvent方法的返回值,判断事件是否被取消了。var canceled = !cb.dispatchEvent(event);
if (canceled) {
console.log('事件取消');
} else {
console.log('事件未取消');
}二、事件模型1、监听函数浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。JavaScript 有三种方法,可以为事件绑定监听函数。1.2 HTML 的 on- 属性HTML 语言允许在元素的属性中,直接定义某些事件的监听代码。<body onload="doSomething()"> <!-- 加圆括号-->
<div onclick="console.log('触发事件')">上面代码为body节点的load事件、div节点的click事件,指定了监听代码。一旦事件发生,就会执行这段代码。元素的事件监听属性,都是on加上事件名,比如onload就是on + load,表示load事件的监听代码。注意,这些属性的值是将会执行的代码,而不是一个函数。<!-- 正确 -->
<body onload="doSomething()">
<!-- 错误 -->
<body onload="doSomething">一旦指定的事件发生,on-属性的值是原样传入 JavaScript 引擎执行。因此如果要执行函数,不要忘记加上一对圆括号。使用这个方法指定的监听代码,只会在冒泡阶段触发。<div onClick="console.log(2)">
<button onClick="console.log(1)">点击</button>
</div>上面代码中, <button> 是 <div> 的子元素。 <button> 的click事件,也会触发 <div> 的click事件。由于on-属性的监听代码,只在冒泡阶段触发,所以点击结果是先输出1,再输出2,即事件从子元素开始冒泡到父元素。直接设置on-属性,与通过元素节点的setAttribute方法设置on-属性,效果是一样的。el.setAttribute('onclick', 'doSomething()');
// 等同于
// <Element onclick="doSomething()">1.2 元素节点的事件属性 (例:el.onclick)元素节点对象的事件属性,同样可以指定监听函数。window.onload = doSomething; // 注意这里是函数名,不加圆括号
div.onclick = function (event) { // onclick是元素的一个属性,它指向一个函数,在触发点击时执行此函数
console.log('触发事件');
};使用这个方法指定的监听函数,也是只会在冒泡阶段触发。注意,这种方法与 HTML 的on-属性的差异是,它的值是函数名(doSomething),而不像后者,必须给出完整的监听代码(doSomething())。1.3 EventTarget.addEventListener()所有 DOM 节点实例都有addEventListener方法,用来为该节点定义事件的监听函数。window.addEventListener('load', doSomething, false); // 注意这里是函数名,不加圆括号addEventListener方法的详细介绍,参见EventTarget章节。1.4 小结上面三种方法,第一种“HTML 的 on- 属性”,违反了 HTML 与 JavaScript 代码相分离的原则,将两者写在一起,不利于代码分工,因此不推荐使用。第二种“元素节点的事件属性”的缺点在于,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次定义会覆盖前一次。因此,也不推荐使用。第三种EventTarget.addEventListener是推荐的指定监听函数的方法。它有如下优点:同一个事件可以添加多个监听函数。能够指定在哪个阶段(捕获阶段还是冒泡阶段)触发监听函数。除了 DOM 节点,其他对象(比如window、XMLHttpRequest等)也有这个接口,它等于是整个 JavaScript 统一的监听函数接口。2、this 的指向 (指向元素节点)监听函数内部的this指向触发事件的那个元素节点。(三种事件绑定的this全指向元素节点)<button id="btn" onclick="console.log(this.id)">点击</button>执行上面代码,点击后会输出btn。其他两种监听函数的写法,this的指向也是如此。// HTML 代码如下
// <button id="btn">点击</button>
var btn = document.getElementById('btn');
// 写法一
btn.onclick = function () {
console.log(this.id);
};
// 写法二
btn.addEventListener(
'click',
function (e) {
console.log(this.id);
},
false
);上面两种写法,点击按钮以后也是输出btn。3、事件的传播(捕获、目标、冒泡)一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。这种三阶段的传播模型,使得同一个事件会在多个节点上触发。<div>
<p>点击</p>
</div>上面代码中, <div> 节点之中有一个 <p> 节点。如果对这两个节点,都设置click事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对 <p> 点击,click事件会触发四次。var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true); // true 表示在捕获阶段触发
p.addEventListener('click', callback, true); // true 表示在捕获阶段触发
div.addEventListener('click', callback, false);// fasle 表示在冒泡阶段触发
p.addEventListener('click', callback, false); // fasle 表示在冒泡阶段触发
function callback(event) {
var tag = event.currentTarget.tagName; // 当前目标对象的标签名
var phase = phases[event.eventPhase]; // 触发的阶段
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 点击以后的结果
// Tag: 'DIV'. EventPhase: 'capture' 捕获阶段
// Tag: 'P'. EventPhase: 'target' 目标阶段
// Tag: 'P'. EventPhase: 'target' 目标阶段
// Tag: 'DIV'. EventPhase: 'bubble' 冒泡阶段上面代码表示,click事件被触发了四次: <div> 节点的捕获阶段和冒泡阶段各1次, <p> 节点的目标阶段触发了2次。捕获阶段:事件从 <div> 向 <p> 传播时,触发 <div> 的click事件;目标阶段:事件从 <div> 到达 <p> 时,触发 <p> 的click事件;冒泡阶段:事件从 <p> 传回 <div> 时,再次触发 <div> 的click事件。其中, <p> 节点有两个监听函数(addEventListener方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为click事件触发一次。所以, <p> 会在target阶段有两次输出。注意,浏览器总是假定click事件的目标节点,就是点击位置嵌套最深的那个节点(本例是 <div> 节点里面的 <p> 节点)。所以, <p> 节点的捕获阶段和冒泡阶段,都会显示为target阶段。事件传播的最上层对象是window,接着依次是document,html(document.documentElement)和body(document.body)。也就是说,上例的事件传播顺序,在捕获阶段依次为window、document、html、body、div、p,在冒泡阶段依次为p、div、body、html、document、window。捕获阶段:
window(浏览器对象)--> document(文档对象) --> html --> body --> div --> p (点击目标,在目标阶段)
冒泡阶段:
p (点击目标,在目标阶段)--> div --> body --> html --> document(文档对象) --> window(浏览器对象)4、事件的代理由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') { // 浏览器假定click嵌套最深的元素是目标元素
// some code
}
});上面代码中,click事件的监听函数定义在 <ul> 节点,但是实际上,它处理的是子节点 <li> 的click事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个 <li> 节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。阻止事件冒泡 event.stopPropagation()如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation方法。// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
event.stopPropagation(); // 该方法在事件对象event上
}, true); // true表示在捕获阶段绑定事件监听函数
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false); // false表示在冒泡阶段(默认值)绑定事件监听函数上面代码中,stopPropagation方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。但是,stopPropagation方法只会阻止事件的传播,不会阻止该事件触发 <p> 节点的其他click事件的监听函数。也就是说,不是彻底取消click事件。p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 会触发
console.log(2);
});上面代码中,p元素绑定了两个click事件的监听函数。stopPropagation方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。如果想要彻底取消该事件,不再触发后面所有click的监听函数,可以使用stopImmediatePropagation()方法。p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不会被触发
console.log(2);
});上面代码中,stopImmediatePropagation方法可以彻底取消这个事件,使得后面绑定的所有click监听函数都不再触发。所以,只会输出1,不会输出2。三、Event 对象1. 概述事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个Event对象,所有的事件都是这个对象的实例,或者说继承了Event.prototype对象。Event对象本身就是一个构造函数,可以用来生成新的实例。event = new Event(type, options);// 参数一,事件名称; 参数二,事件对象的配置对象Event构造函数接受两个参数。第一个参数type是字符串,表示事件的名称;第二个参数options是一个对象,表示事件对象的配置。该对象主要有下面两个属性。bubbles:布尔值,可选,默认为false,表示事件对象是否冒泡。cancelable:布尔值,可选,默认为false,表示事件是否可以被取消,即能否用Event.preventDefault()取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。var ev = new Event( // 创建一个新的事件实例
'look', // 事件名称
{ // 事件配置
'bubbles': true, // 事件是否冒泡
'cancelable': false // 事件是否可以被取消
}
);
document.dispatchEvent(ev); // 触发ev实例,该实例是look事件上面代码新建一个look事件实例,然后使用dispatchEvent方法触发该事件。注意,如果不是显式指定bubbles属性为true,生成的事件就只能在“捕获阶段”触发监听函数。// HTML 代码为
// <div><p>Hello</p></div>
var div = document.querySelector('div');
var p = document.querySelector('p');
function callback(event) {
var tag = event.currentTarget.tagName;
console.log('Tag: ' + tag); // 没有任何输出
}
div.addEventListener('click', callback, false); // 该事件是在冒泡阶段监听函数
var click = new Event('click');
p.dispatchEvent(click); // 给p发出一个click事件,该事件默认不会冒泡,因此不会触发父元素div的click事件上面代码中,p元素发出一个click事件,该事件默认不会冒泡。div.addEventListener方法指定在冒泡阶段监听,因此监听函数不会触发。如果写成div.addEventListener('click', callback, true),那么在“捕获阶段”可以监听到这个事件。另一方面,如果这个事件在div元素上触发。div.dispatchEvent(click);那么,不管div元素是在冒泡阶段监听,还是在捕获阶段监听,都会触发监听函数。因为这时div元素是事件的目标,不存在是否冒泡的问题,div元素总是会接收到事件,因此导致监听函数生效。2. 实例属性2.1 Event.bubbles 是否会冒泡,只读,Event.eventPhase 返回整数,表示事件所处阶段,只读Event.bubbles属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,一般用来了解 Event 实例是否可以冒泡。前面说过,除非显式声明,Event构造函数生成的事件,默认是不冒泡的。Event.eventPhase属性返回一个整数常量,表示事件目前所处的阶段。该属性只读。var phase = event.eventPhase;Event.eventPhase的返回值有四种可能。0,事件目前没有发生。1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。2,事件到达目标节点,即Event.target属性指向的那个节点。3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。2.2 Event.cancelable 是否可取消默认行为,Event.cancelBubble是否阻止冒泡,event.defaultPrevented 是否调用过取消默认行为方法Event.cancelable属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性,一般用来了解 Event 实例的特性。大多数浏览器的原生事件是可以取消的。比如,取消click事件,点击链接将无效。但是除非显式声明,Event构造函数生成的事件,默认是不可以取消的。var evt = new Event('foo');
evt.cancelable // false 默认不可取消当Event.cancelable属性为true时,调用Event.preventDefault()就可以取消这个事件,阻止浏览器对该事件的默认行为。如果事件不能取消,调用Event.preventDefault()会没有任何效果。所以使用这个方法之前,最好用Event.cancelable属性判断一下是否可以取消。function preventEvent(event) {
if (event.cancelable) {
event.preventDefault();
} else {
console.warn('This event couldn\'t be canceled.');
console.dir(event);
}
}Event.cancelBubble属性是一个布尔值,如果设为true,相当于执行Event.stopPropagation(),可以阻止事件的传播。Event.defaultPrevented属性返回一个布尔值,表示该事件是否调用过Event.preventDefault方法。该属性只读。if (event.defaultPrevented) {
console.log('该事件已经取消了');
}2.3 Event.currentTarget当前正在通过的节点,Event.target目标节点事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意时点都有两个与事件相关的节点,一个是事件的原始触发节点(Event.target),另一个是事件当前正在通过的节点(Event.currentTarget)。前者通常是后者的后代节点。Event.currentTarget属性返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。Event.target属性返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。事件传播过程中,不同节点的监听函数内部的Event.target与Event.currentTarget属性的值是不一样的。// HTML 代码为
// <p id="para">Hello <em>World</em></p>
function hide(e) {
// 不管点击 Hello 或 World,总是返回 true
console.log(this === e.currentTarget);
// 点击 Hello,返回 true
// 点击 World,返回 false
console.log(this === e.target);
}
document.getElementById('para').addEventListener('click', hide, false);上面代码中, <em> 是 <p> 的子节点,点击 <em> 或者点击 <p> ,都会导致监听函数执行。这时,e.target总是指向原始点击位置的那个节点,而e.currentTarget指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发,所以e.currentTarget总是等同于监听函数内部的this。2.4 Event.type 事件类型(如:'click')Event.type属性返回一个字符串,表示事件类型。事件的类型是在生成事件的时候指定的。该属性只读。var evt = new Event('foo');
evt.type // "foo"2.5 Event.timeStamp 相对于打开网页后的毫秒时间戳Event.timeStamp属性返回一个毫秒时间戳,表示事件发生的时间。它是相对于网页加载成功开始计算的。var evt = new Event('foo');
evt.timeStamp // 3683.6999999995896它的返回值有可能是整数,也有可能是小数(高精度时间戳),取决于浏览器的设置。下面是一个计算鼠标移动速度的例子,显示每秒移动的像素数量。例子:计算鼠标移动速度var previousX;
var previousY;
var previousT;
window.addEventListener('mousemove', function(event) {
if (
previousX !== undefined &&
previousY !== undefined &&
previousT !== undefined
) {
var deltaX = event.screenX - previousX;
var deltaY = event.screenY - previousY;
var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
var deltaT = event.timeStamp - previousT;
console.log(deltaD / deltaT * 1000);
}
previousX = event.screenX;
previousY = event.screenY;
previousT = event.timeStamp;
});2.6 Event.isTrusted 是否由真实用户行为产生的事件Event.isTrusted属性返回一个布尔值,表示该事件是否由真实的用户行为产生。比如,用户点击链接会产生一个click事件,该事件是用户产生的;Event构造函数生成的事件,则是脚本产生的。var evt = new Event('foo');
evt.isTrusted // false上面代码中,evt对象是脚本产生的,所以isTrusted属性返回false。2.7 Event.detail 事件的细节(单击or双击等)Event.detail属性只有浏览器的 UI (用户界面)事件才具有。该属性返回一个数值,表示事件的某种信息。具体含义与事件类型相关。比如,对于click和dblclick事件,Event.detail是鼠标按下的次数(1表示单击,2表示双击,3表示三击);对于鼠标滚轮事件,Event.detail是滚轮正向滚动的距离,负值就是负向滚动的距离,返回值总是3的倍数。// HTML 代码如下
// <p>Hello</p>
function giveDetails(e) {
console.log(e.detail); // 快速点击的次数
}
document.querySelector('p').onclick = giveDetails;3、实例方法3.1 Event.preventDefault() 取消浏览器对当前事件的默认行为Event.preventDefault方法取消浏览器对当前事件的默认行为。比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。注意,该方法只是取消事件对当前元素的默认影响,不会阻止事件的传播。如果要阻止传播,可以使用stopPropagation()或stopImmediatePropagation()方法。// HTML 代码为
// <input type="checkbox" id="my-checkbox" />
var cb = document.getElementById('my-checkbox');
cb.addEventListener(
'click',
function (e){ e.preventDefault(); },
false
);上面代码中,浏览器的默认行为是单击会选中单选框,取消这个行为,就导致无法选中单选框。利用这个方法,可以为文本输入框设置校验条件。如果用户的输入不符合条件,就无法将字符输入文本框。例子:只能输入字母的输入框// HTML 代码为
// <input type="text" id="my-input" />
var input = document.getElementById('my-input');
input.addEventListener('keypress', checkName, false);
function checkName(e) {
if (e.charCode < 97 || e.charCode > 122) {
e.preventDefault();
}
}上面代码为文本框的keypress事件设定监听函数后,将只能输入小写字母,否则输入事件的默认行为(写入文本框)将被取消,导致不能向文本框输入内容。3.2 Event.stopPropagation() 阻止事件传播stopPropagation方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。function stopEvent(e) {
e.stopPropagation(); // 阻止事件冒泡
}
el.addEventListener('click', stopEvent, false);上面代码中,click事件将不会进一步冒泡到el节点的父节点。3.3 Event.stopImmediatePropagation() 阻止同一个事件的其他监听函数被调用Event.stopImmediatePropagation方法阻止同一个事件的其他监听函数被调用,不管监听函数定义在当前节点还是其他节点。也就是说,该方法阻止事件的传播,比Event.stopPropagation()更彻底。如果同一个节点对于同一个事件指定了多个监听函数,这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了Event.stopImmediatePropagation方法,其他的监听函数就不会再执行了。function l1(e){
e.stopImmediatePropagation();
}
function l2(e){ // 不会被调用
console.log('hello world');
}
el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);上面代码在el节点上,为click事件添加了两个监听函数l1和l2。由于l1调用了event.stopImmediatePropagation方法,所以l2不会被调用。3.4 Event.composedPath() 数组,目标和冒泡的节点Event.composedPath()返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。// HTML 代码如下
// <div>
// <p>Hello</p>
// </div>
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', function (e) {
console.log(e.composedPath());
}, false);
// [p, div, body, html, document, Window]上面代码中,click事件的最底层节点是p,向上依次是div、body、html、document、Window。四、鼠标事件1、鼠标事件的种类鼠标事件指与鼠标相关的事件,继承了MouseEvent接口。具体的事件主要有以下一些。click:按下鼠标(通常是按下主按钮)时触发。【单击】dblclick:在同一个元素上双击鼠标时触发。【双击】mousedown:按下鼠标键时触发。【按下】mouseup:释放按下的鼠标键时触发。【抬起】mousemove:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。【经过(多次)】mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件(详见后文)。【进入(单次)】mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件(详见后文)。【进入+子节点】mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件(详见后文)。【离开(单次)】mouseout:鼠标离开一个节点时触发,离开父节点会触发这个事件(详见后文)。【离开+子节点】contextmenu:按下鼠标右键时(上下文菜单出现前)触发,或者按下“上下文菜单键”时触发。【右键】wheel:滚动鼠标的滚轮时触发,该事件继承的是WheelEvent接口。【滚轮】click事件指的是,用户在同一个位置先完成mousedown动作,再完成mouseup动作。因此,触发顺序是,mousedown首先触发,mouseup接着触发,click最后触发。dblclick事件则会在mousedown、mouseup、click之后触发。mouseover事件和mouseenter事件,都是鼠标进入一个节点时触发。两者的区别是,mouseenter事件只触发一次,而只要鼠标在节点内部移动,mouseover事件会在子节点上触发多次。/* HTML 代码如下
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
*/
var ul = document.querySelector('ul');
// 进入 ul 节点以后,mouseenter 事件只会触发一次
// 以后只要鼠标在节点内移动,都不会再触发这个事件
// event.target 是 ul 节点
ul.addEventListener('mouseenter', function (event) {
event.target.style.color = 'purple';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);
// 进入 ul 节点以后,只要在子节点上移动,mouseover 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseover', function (event) {
event.target.style.color = 'orange';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);上面代码中,在父节点内部进入子节点,不会触发mouseenter事件,但是会触发mouseover事件。mouseout事件和mouseleave事件,都是鼠标离开一个节点时触发。两者的区别是,在父元素内部离开一个子元素时,mouseleave事件不会触发,而mouseout事件会触发。/* HTML 代码如下
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
*/
var ul = document.querySelector('ul');
// 先进入 ul 节点,然后在节点内部移动,不会触发 mouseleave 事件
// 只有离开 ul 节点时,触发一次 mouseleave
// event.target 是 ul 节点
ul.addEventListener('mouseleave', function (event) {
event.target.style.color = 'purple';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);
// 先进入 ul 节点,然后在节点内部移动,mouseout 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseout', function (event) {
event.target.style.color = 'orange';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);上面代码中,在父节点内部离开子节点,不会触发mouseleave事件,但是会触发mouseout事件。2、MouseEvent 接口概述MouseEvent接口代表所有鼠标事件所产生的对象都是MouseEvent实例。此外,滚轮事件和拖拉事件也是MouseEvent实例。MouseEvent接口继承了Event接口,所以拥有Event的所有属性和方法。它还有自己的属性和方法。浏览器原生提供一个MouseEvent构造函数,用于新建一个MouseEvent实例。var event = new MouseEvent(type, options);// 参数一,事件名称字符串;参数二,事件配置对象MouseEvent构造函数接受两个参数。第一个参数是字符串,表示事件名称;第二个参数是一个事件配置对象,该参数可选。除了Event接口的实例配置属性,该对象可以配置以下属性,所有属性都是可选的。screenX:数值,鼠标相对于屏幕的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。screenY:数值,鼠标相对于屏幕的垂直位置(单位像素),其他与screenX相同。clientX:数值,鼠标相对于程序窗口的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。clientY:数值,鼠标相对于程序窗口的垂直位置(单位像素),其他与clientX相同。ctrlKey:布尔值,是否同时按下了 Ctrl 键,默认值为false。shiftKey:布尔值,是否同时按下了 Shift 键,默认值为false。altKey:布尔值,是否同时按下 Alt 键,默认值为false。metaKey:布尔值,是否同时按下 Meta 键(win键),默认值为false。button:数值,表示按下了哪一个鼠标按键,默认值为0,表示按下主键(通常是鼠标的左键)或者当前事件没有定义这个属性;1表示按下辅助键(通常是鼠标的中间键),2表示按下次要键(通常是鼠标的右键)。buttons:数值,表示按下了鼠标的哪些键,是一个三个比特位的二进制值,默认为0(没有按下任何键)。1(二进制001)表示按下主键(通常是左键),2(二进制010)表示按下次要键(通常是右键),4(二进制100)表示按下辅助键(通常是中间键)。因此,如果返回3(二进制011)就表示同时按下了左键和右键。relatedTarget:节点对象,表示事件的相关节点,默认为null。mouseenter和mouseover事件时,表示鼠标刚刚离开的那个元素节点;mouseout和mouseleave事件时,表示鼠标正在进入的那个元素节点。下面是一个例子。var event = new MouseEvent('click2', {
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('checkbox');
cb.addEventListener('click2',function(){ // 绑定事件监听函数
console.log(22) // 被执行
})
cb.dispatchEvent(event);// 触发事件上面代码生成一个鼠标点击事件,并触发该事件。3、MouseEvent 接口的实例属性3.1 MouseEvent.altKey,MouseEvent.ctrlKey,MouseEvent.metaKey,MouseEvent.shiftKeyMouseEvent.altKey、MouseEvent.ctrlKey、MouseEvent.metaKey、MouseEvent.shiftKey这四个属性都返回一个布尔值,表示事件发生时,是否按下对应的键。它们都是只读属性。altKey属性:Alt 键ctrlKey属性:Ctrl 键metaKey属性:Meta 键(Mac 键盘是一个四瓣的小花,Windows 键盘是 Windows 键)shiftKey属性:Shift 键// HTML 代码如下
// <body onclick="showKey(event)">
function showKey(e) {
console.log('ALT key pressed: ' + e.altKey);
console.log('CTRL key pressed: ' + e.ctrlKey);
console.log('META key pressed: ' + e.metaKey);
console.log('SHIFT key pressed: ' + e.shiftKey);
}上面代码中,点击网页会输出是否同时按下对应的键。3.2 MouseEvent.button 鼠标的哪个键,MouseEvent.buttons同时按哪些键MouseEvent.button属性返回一个数值,表示事件发生时按下了鼠标的哪个键。该属性只读。0:按下主键(通常是左键),或者该事件没有初始化这个属性(比如mousemove事件)。1:按下辅助键(通常是中键或者滚轮键)。2:按下次键(通常是右键)。// HTML 代码为
// <button onmouseup="whichButton(event)">点击</button>
var whichButton = function (e) {
switch (e.button) {
case 0:
console.log('Left button clicked.');
break;
case 1:
console.log('Middle button clicked.');
break;
case 2:
console.log('Right button clicked.');
break;
default:
console.log('Unexpected code: ' + e.button);
}
}MouseEvent.buttons属性返回一个三个比特位的值,表示同时按下了哪些键。它用来处理同时按下多个鼠标键的情况。该属性只读。1:二进制为001(十进制的1),表示按下左键。2:二进制为010(十进制的2),表示按下右键。4:二进制为100(十进制的4),表示按下中键或滚轮键。同时按下多个键的时候,每个按下的键对应的比特位都会有值。比如,同时按下左键和右键,会返回3(二进制为011)。document.body.addEventListener('mousemove',function(e){ // 注意,用click时一直都是0
console.log(e.buttons)
})
// 未按下任何键时是 0
// 按下左键 1 (001)
// 按下右键 2 (010)
// 按下中键 4 (100)
// 按下左键和右键 3 (011)
// 按下左键和中键 5 (101)
// 按下右键和中键 6 (110)
// 按下左、中、右键 7 (111)3.3 MouseEvent.clientX 相对浏览器X坐标,MouseEvent.clientY 相对浏览器Y坐标MouseEvent.clientX属性返回鼠标位置相对于浏览器窗口左上角的水平坐标(单位像素),MouseEvent.clientY属性返回垂直坐标。这两个属性都是只读属性。// HTML 代码为
// <body onmousedown="showCoords(event)">
function showCoords(evt){
console.log(
'clientX value: ' + evt.clientX + '\n' +
'clientY value: ' + evt.clientY + '\n'
);
}这两个属性还分别有一个别名MouseEvent.x和MouseEvent.y。3.4 MouseEvent.movementX 上一个鼠标经过事件的X距离,MouseEvent.movementY 上一个鼠标经过事件的Y距离MouseEvent.movementX属性返回当前位置与上一个mousemove事件之间的水平距离(单位像素)。数值上,它等于下面的计算公式。currentEvent.movementX = currentEvent.screenX - previousEvent.screenXMouseEvent.movementY属性返回当前位置与上一个mousemove事件之间的垂直距离(单位像素)。数值上,它等于下面的计算公式。currentEvent.movementY = currentEvent.screenY - previousEvent.screenY。这两个属性都是只读属性。3.5 MouseEvent.screenX 相对屏幕X坐标,MouseEvent.screenY 相对屏幕Y坐标MouseEvent.screenX属性返回鼠标位置相对于屏幕左上角的水平坐标(单位像素),MouseEvent.screenY属性返回垂直坐标。这两个属性都是只读属性。// HTML 代码如下
// <body onmousedown="showCoords(event)">
function showCoords(evt) {
console.log(
'screenX value: ' + evt.screenX + '\n',
'screenY value: ' + evt.screenY + '\n'
);
}3.6 MouseEvent.offsetX 偏移量X,MouseEvent.offsetY 偏移量YMouseEvent.offsetX属性返回鼠标位置与目标节点左侧的padding边缘的水平距离(单位像素),MouseEvent.offsetY属性返回与目标节点上方的padding边缘的垂直距离。这两个属性都是只读属性。/* HTML 代码如下
<style>
p {
width: 100px;
height: 100px;
padding: 100px;
}
</style>
<p>Hello</p>
*/
var p = document.querySelector('p');
p.addEventListener(
'click',
function (e) {
console.log(e.offsetX); // 包含padding
console.log(e.offsetY);
},
false
);上面代码中,鼠标如果在p元素的中心位置点击,会返回150 150。因此中心位置距离左侧和上方的padding边缘,等于padding的宽度(100像素)加上元素内容区域一半的宽度(50像素)。3.7 MouseEvent.pageX 文档X坐标,MouseEvent.pageY 文档Y坐标MouseEvent.pageX属性返回鼠标位置与文档左侧边缘的距离(单位像素),MouseEvent.pageY属性返回与文档上侧边缘的距离(单位像素)。它们的返回值都包括文档不可见的部分。这两个属性都是只读。/* HTML 代码如下
<style>
body {
height: 2000px;
}
</style>
*/
document.body.addEventListener(
'click',
function (e) {
console.log(e.pageX);
console.log(e.pageY);
},
false
);上面代码中,页面高度为2000像素,会产生垂直滚动条。滚动到页面底部,点击鼠标输出的pageY值会接近2000。3.8 MouseEvent.relatedTarget 事件的相关节点MouseEvent.relatedTarget属性返回事件的相关节点。对于那些没有相关节点的事件,该属性返回null。该属性只读。下表列出不同事件的target属性值和relatedTarget属性值义。事件名称target 属性relatedTarget 属性focusin接受焦点的节点丧失焦点的节点focusout丧失焦点的节点接受焦点的节点mouseenter将要进入的节点将要离开的节点mouseleave将要离开的节点将要进入的节点mouseout将要离开的节点将要进入的节点mouseover将要进入的节点将要离开的节点dragenter将要进入的节点将要离开的节点dragexit将要离开的节点将要进入的节点下面是一个例子。/*
HTML 代码如下
<div id="outer" style="height:50px;width:50px;border-width:1px solid black;">
<div id="inner" style="height:25px;width:25px;border:1px solid black;"></div>
</div>
*/
var inner = document.getElementById('inner');
inner.addEventListener('mouseover', function (event) {
console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
}, false);
inner.addEventListener('mouseenter', function (event) {
console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
});
inner.addEventListener('mouseout', function () {
console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});
inner.addEventListener("mouseleave", function (){
console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});
// 鼠标从 outer 进入inner,输出
// 进入inner 离开outer
// 进入inner 离开outer
// 鼠标从 inner进入 outer,输出
// 离开inner 进入outer
// 离开inner 进入outer4、MouseEvent 接口的实例方法4.1 MouseEvent.getModifierState() 是否按下指定功能键MouseEvent.getModifierState方法返回一个布尔值,表示有没有按下特定的功能键。它的参数是一个表示功能键的字符串。document.addEventListener('click', function (e) {
console.log(e.getModifierState('CapsLock'));
}, false);上面的代码可以了解用户是否按下了大写键。5、WheelEvent 接口 (滚轮)5.1 概述WheelEvent 接口继承了 MouseEvent 实例,代表鼠标滚轮事件的实例对象。目前,鼠标滚轮相关的事件只有一个wheel事件,用户滚动鼠标的滚轮,就生成这个事件的实例。浏览器原生提供WheelEvent()构造函数,用来生成WheelEvent实例。var wheelEvent = new WheelEvent(type, options);WheelEvent()构造函数可以接受两个参数,第一个是字符串,表示事件类型,对于滚轮事件来说,这个值目前只能是wheel。第二个参数是事件的配置对象。该对象的属性除了Event、UIEvent的配置属性以外,还可以接受以下几个属性,所有属性都是可选的。deltaX:数值,表示滚轮的水平滚动量,默认值是 0.0。deltaY:数值,表示滚轮的垂直滚动量,默认值是 0.0。deltaZ:数值,表示滚轮的 Z 轴滚动量,默认值是 0.0。deltaMode:数值,表示相关的滚动事件的单位,适用于上面三个属性。0表示滚动单位为像素,1表示单位为行,2表示单位为页,默认为0。5.2 实例属性WheelEvent事件实例除了具有Event和MouseEvent的实例属性和实例方法,还有一些自己的实例属性,但是没有自己的实例方法。下面的属性都是只读属性。WheelEvent.deltaX:数值,表示滚轮的水平滚动量。WheelEvent.deltaY:数值,表示滚轮的垂直滚动量。WheelEvent.deltaZ:数值,表示滚轮的 Z 轴滚动量。WheelEvent.deltaMode:数值,表示上面三个属性的单位,0是像素,1是行,2是页。五、键盘事件1、键盘事件的种类键盘事件由用户击打键盘触发,主要有keydown、keypress、keyup三个事件,它们都继承了KeyboardEvent接口。keydown:按下键盘时触发。【按下】keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个事件。【按下有值的键】keyup:松开键盘时触发该事件。【松开】如果用户一直按键不松开,就会连续触发键盘事件,触发的顺序如下。keydownkeypresskeydownkeypress...(重复以上过程)keyup2、KeyboardEvent 接口概述KeyboardEvent接口用来描述用户与键盘的互动。这个接口继承了Event接口,并且定义了自己的实例属性和实例方法。浏览器原生提供KeyboardEvent构造函数,用来新建键盘事件的实例。new KeyboardEvent(type, options) // 参数一,事件类型;参数二,事件配置对象KeyboardEvent构造函数接受两个参数。第一个参数是字符串,表示事件类型;第二个参数是一个事件配置对象,该参数可选。除了Event接口提供的属性,还可以配置以下字段,它们都是可选。key:字符串,当前按下的键,默认为空字符串。【键名】code:字符串,表示当前按下的键的字符串形式,默认为空字符串。【键码】location:整数,当前按下的键的位置,默认为0。ctrlKey:布尔值,是否按下 Ctrl 键,默认为false。shiftKey:布尔值,是否按下 Shift 键,默认为false。altKey:布尔值,是否按下 Alt 键,默认为false。metaKey:布尔值,是否按下 Meta 键,默认为false。repeat:布尔值,是否重复按键,默认为false。3、KeyboardEvent 的实例属性3.1 KeyboardEvent.altKey,KeyboardEvent.ctrlKey,KeyboardEvent.metaKey,KeyboardEvent.shiftKey 【是否按下对应键,布尔值】以下属性都是只读属性,返回一个布尔值,表示是否按下对应的键。KeyboardEvent.altKey:是否按下 Alt 键KeyboardEvent.ctrlKey:是否按下 Ctrl 键KeyboardEvent.metaKey:是否按下 meta 键(Mac 系统是一个四瓣的小花,Windows 系统是 windows 键)KeyboardEvent.shiftKey:是否按下 Shift 键下面是一个示例。function showChar(e) {
console.log('ALT: ' + e.altKey);
console.log('CTRL: ' + e.ctrlKey);
console.log('Meta: ' + e.metaKey);
console.log('Shift: ' + e.shiftKey);
}
document.body.addEventListener('keydown', showChar, false);3.2 KeyboardEvent.code 键码KeyboardEvent.code属性返回一个字符串,表示当前按下的键的字符串形式。该属性只读。下面是一些常用键的字符串形式,其他键请查文档。数字键0 - 9:返回digital0 - digital9字母键A - z:返回KeyA - KeyZ功能键F1 - F12:返回 F1 - F12方向键:返回ArrowDown、ArrowUp、ArrowLeft、ArrowRightAlt 键:返回AltLeft或AltRightShift 键:返回ShiftLeft或ShiftRightCtrl 键:返回ControlLeft或ControlRight3.3 KeyboardEvent.key 键名KeyboardEvent.key属性返回一个字符串,表示按下的键名。该属性只读。如果按下的键代表可打印字符,则返回这个字符,比如数字、字母。如果按下的键代表不可打印的特殊字符,则返回预定义的键值,比如 Backspace,Tab,Enter,Shift,Control,Alt,CapsLock,Esc,Spacebar,PageUp,PageDown,End,Home,Left,Right,Up,Down,PrintScreen,Insert,Del,Win,F1~F12,NumLock,Scroll 等。如果同时按下一个控制键和一个符号键,则返回符号键的键名。比如,按下 Ctrl + a,则返回a;按下 Shift + a,则返回大写的A。如果无法识别键名,返回字符串Unidentified。3.4 KeyboardEvent.location 键处于哪个位置,整数KeyboardEvent.location属性返回一个整数,表示按下的键处在键盘的哪一个区域。它可能取以下值。0:处在键盘的主区域,或者无法判断处于哪一个区域。1:处在键盘的左侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。2:处在键盘的右侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。3:处在数字小键盘。3.5 KeyboardEvent.repeat 是否长按KeyboardEvent.repeat返回一个布尔值,代表该键是否被按着不放,以便判断是否重复这个键,即浏览器会持续触发keydown和keypress事件,直到用户松开手为止。4、KeyboardEvent 的实例方法4.1 KeyboardEvent.getModifierState() 是否按下指定功能键KeyboardEvent.getModifierState()方法返回一个布尔值,表示是否按下或激活指定的功能键。它的常用参数如下。Alt:Alt 键CapsLock:大写锁定键Control:Ctrl 键Meta:Meta 键NumLock:数字键盘开关键Shift:Shift 键if (
event.getModifierState('Control') +
event.getModifierState('Alt') +
event.getModifierState('Meta') > 1
) {
return;
}上面代码表示,只要Control、Alt、Meta里面,同时按下任意两个或两个以上的键就返回。六、进度事件1、进度事件的种类进度事件用来描述资源加载的进度,主要由 AJAX 请求、 <img> 、 <audio> 、 <video> 、 <style> 、 <link> 等外部资源的加载触发,继承了ProgressEvent接口。它主要包含以下几种事件。abort:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。【中止加载】error:由于错误导致外部资源无法加载时触发。【加载错误】load:外部资源加载成功时触发。【加载成功】loadstart:外部资源开始加载时触发。【开始加载】loadend:外部资源停止加载时触发,发生顺序排在error、abort、load等事件的后面。【停止加载】progress:外部资源加载过程中不断触发。【加载中,不断触发】timeout:加载超时时触发。【加载超时】注意,除了资源下载,文件上传也存在这些事件。下面是一个例子。image.addEventListener('load', function (event) { // 加载成功
image.classList.add('finished');
});
image.addEventListener('error', function (event) { // 加载出错
image.style.display = 'none';
});上面代码在图片元素加载完成后,为图片元素添加一个finished的 Class。如果加载失败,就把图片元素的样式设置为不显示。有时候,图片加载会在脚本运行之前就完成,尤其是当脚本放置在网页底部的时候,因此有可能load和error事件的监听函数根本不会执行。所以,比较可靠的方式,是用complete属性先判断一下是否加载完成。function loaded() {
// ...
}
if (image.complete) { // 是否加载完成
loaded();
} else {
image.addEventListener('load', loaded); // 加载成功事件
}由于 DOM 的元素节点没有提供是否加载错误的属性,所以error事件的监听函数最好放在 <img> 元素的 HTML 代码中,这样才能保证发生加载错误时百分之百会执行。<img src="/wrong/url" onerror="this.style.display='none';" />loadend事件的监听函数,可以用来取代abort事件、load事件、error事件的监听函数,因为它总是在这些事件之后发生。req.addEventListener('loadend', loadEnd, false);
function loadEnd(e) {
console.log('传输结束,成功失败未知');
}loadend事件本身不提供关于进度结束的原因,但可以用它来做所有加载结束场景都需要做的一些操作。另外,error事件有一个特殊的性质,就是不会冒泡。所以,子元素的error事件,不会触发父元素的error事件监听函数。2、ProgressEvent 接口2.1 概述ProgressEvent接口主要用来描述外部资源加载的进度,比如 AJAX 加载、 <img> 、 <video> 、 <style> 、 <link> 等外部资源加载。进度相关的事件都继承了这个接口。这个接口继承了Event接口。浏览器原生提供了ProgressEvent()构造函数,用来生成事件实例。new ProgressEvent(type, options) // 参数一,事件类型;参数二,配置对象ProgressEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的类型,这个参数是必须的。第二个参数是一个配置对象,表示事件的属性,该参数可选。配置对象除了可以使用Event接口的配置属性,还可以使用下面的属性,所有这些属性都是可选的。lengthComputable:布尔值,表示加载的总量是否可以计算,默认是false。loaded:整数,表示已经加载的量,默认是0。total:整数,表示需要加载的总量,默认是0。2.2 ProgressEvent的实例属性。ProgressEvent.lengthComputable 总量是否可以计算ProgressEvent.loaded 已加载的量ProgressEvent.total 需要加载的总量如果ProgressEvent.lengthComputable为false,ProgressEvent.total实际上是没有意义的。下面是一个例子。var p = new ProgressEvent('load', {
lengthComputable: true,
loaded: 30,
total: 100,
});
document.body.addEventListener('load', function (e) {
console.log('已经加载:' + (e.loaded / e.total) * 100 + '%');
});
document.body.dispatchEvent(p);
// 已经加载:30%上面代码先构造一个load事件,抛出后被监听函数捕捉到。下面是一个实际的例子。var xhr = new XMLHttpRequest();
xhr.addEventListener('progress', updateProgress, false); // 加载中
xhr.addEventListener('load', transferComplete, false); // 加载成功
xhr.addEventListener('error', transferFailed, false); // 加载错误
xhr.addEventListener('abort', transferCanceled, false); // 中止加载
xhr.open();
function updateProgress(e) { // 加载中
if (e.lengthComputable) { // 是否可以计算总量
var percentComplete = e.loaded / e.total; // 加载进度计算
} else {
console.log('不能计算进度');
}
}
function transferComplete(e) { // 加载成功
console.log('传输结束');
}
function transferFailed(evt) { // 加载错误
console.log('传输过程中发生错误');
}
function transferCanceled(evt) { // 中止加载
console.log('用户取消了传输');
}上面是下载过程的进度事件,还存在上传过程的进度事件。这时所有监听函数都要放在XMLHttpRequest.upload对象上面。var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', updateProgress, false);
xhr.upload.addEventListener('load', transferComplete, false);
xhr.upload.addEventListener('error', transferFailed, false);
xhr.upload.addEventListener('abort', transferCanceled, false);
xhr.open();七、表单事件1、表单事件的种类1.1 input 事件 (值发生变化触发,会连续)input事件当 <input> 、 <select> 、 <textarea> 的值发生变化时触发。对于复选框( <input type=checkbox> )或单选框( <input type=radio> ),用户改变选项时,也会触发这个事件。另外,对于打开contenteditable属性的元素,只要值发生变化,也会触发input事件。input事件的一个特点,就是会连续触发,比如用户每按下一次按键,就会触发一次input事件。input事件对象继承了InputEvent接口。该事件跟change事件很像,不同之处在于input事件在元素的值发生变化后立即发生,而change在元素失去焦点时发生,而内容此时可能已经变化多次。也就是说,如果有连续变化,input事件会触发多次,而change事件只在失去焦点时触发一次。下面是 <select> 元素的例子。/* HTML 代码如下
<select id="mySelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
*/
function inputHandler(e) {
console.log(e.target.value)
}
var mySelect = document.querySelector('#mySelect');
mySelect.addEventListener('input', inputHandler);上面代码中,改变下拉框选项时,会触发input事件,从而执行回调函数inputHandler。1.2 select 事件 (选中文本时触发)select事件当在 <input> 、 <textarea> 里面选中文本时触发。// HTML 代码如下
// <input id="test" type="text" value="Select me!" />
var elem = document.getElementById('test');
elem.addEventListener('select', function (e) {
console.log(e.type); // "select" 事件类型
var _target = e.target;
console.log(_target.value); // 文本框的全部值
console.log(_target.selectionDirection); // 选择的方向:'forward'正向、'backward'反向
console.log(_target.selectionStart); // 开始选择的索引
console.log(_target.selectionEnd); // 结束选择的索引
// 注意:开始和结束索引是不分选择方向的,开始的索引一直是靠前的
console.log(_target.value.slice(_target.selectionStart, _target.selectionEnd)) // 选中的那部分字符串
}, false);选中的文本可以通过event.target元素的selectionDirection、selectionEnd、selectionStart和value属性拿到。1.3 change 事件 (值发生变化时触发,单次)change事件当 <input> 、 <select> 、 <textarea> 的值发生变化时触发。它与input事件的最大不同,就是不会连续触发,只有当全部修改完成时才会触发,另一方面input事件必然伴随change事件。具体来说,分成以下几种情况。激活单选框(radio)或复选框(checkbox)时触发。用户提交时触发。比如,从下列列表(select)完成选择,在日期或文件输入框完成选择。当文本框或 <textarea> 元素的值发生改变,并且丧失焦点时触发。下面是一个例子。// HTML 代码如下
// <select size="1" onchange="changeEventHandler(event);">
// <option>chocolate</option>
// <option>strawberry</option>
// <option>vanilla</option>
// </select>
function changeEventHandler(event) {
console.log(event.target.value);
}如果比较一下上面input事件的例子,你会发现对于 <select> 元素来说,input和change事件基本是等价的。1.4 invalid 事件 (表单提交不满足条件触发)用户提交表单时,如果表单元素的值不满足校验条件,就会触发invalid事件。<form>
<input type="text" required oninvalid="console.log('invalid input')" />
<button type="submit">提交</button>
</form>上面代码中,输入框是必填的。如果不填,用户点击按钮提交时,就会触发输入框的invalid事件,导致提交被取消。1.5 reset 事件(重置),submit 事件(提交)reset事件当表单重置(所有表单成员变回默认值)时触发。submit事件当表单数据向服务器提交时触发。注意,这两个事件发生在表单对象 <form> 上,而不是发生在表单的成员上,因为提交的是表单,而不是表单成员。<form onreset="console.log('触发了重置事件')" onsubmit="console.log('触发了提交事件')" name="input" action="html_form_action.php" method="get">
<input type="text" name="lname" value="Duck">
<button type="reset">重置</button>
<button type="submit">提交</button>
</form>2、InputEvent 接口(input事件的实例)InputEvent接口主要用来描述input事件的实例。该接口继承了Event接口,还定义了一些自己的实例属性和实例方法。浏览器原生提供InputEvent()构造函数,用来生成实例对象。new InputEvent(type, options) // 参数一,事件名称;参数二,配置对象InputEvent构造函数可以接受两个参数。第一个参数是字符串,表示事件名称,该参数是必需的。第二个参数是一个配置对象,用来设置事件实例的属性,该参数是可选的。配置对象的字段除了Event构造函数的配置属性,还可以设置下面的字段,这些字段都是可选的。inputType:字符串,表示发生变更的类型(详见下文)。data:字符串,表示插入的字符串。如果没有插入的字符串(比如删除操作),则返回null或空字符串。dataTransfer:返回一个 DataTransfer 对象实例,该属性通常只在输入框接受富文本输入时有效。InputEvent的实例属性主要就是上面三个属性,这三个实例属性都是只读的。(1)InputEvent.data 变动的那部分内容InputEvent.data属性返回一个字符串,表示变动的内容。// HTML 代码如下
// <input type="text" id="myInput">
var input = document.getElementById('myInput');
input.addEventListener('input', myFunction, false);
function myFunction(e) {
console.log(e.data);
}上面代码中,如果手动在输入框里面输入abc,控制台会先输出a,再在下一行输出b,再在下一行输出c。然后选中abc,一次性将它们删除,控制台会输出null或一个空字符串。(2)InputEvent.inputType 变更类型InputEvent.inputType属性返回一个字符串,表示字符串发生变更的类型。对于常见情况,Chrome 浏览器的返回值如下。完整列表可以参考文档。手动插入文本:insertText粘贴插入文本:insertFromPaste向后删除:deleteContentBackward向前删除:deleteContentForward(3)InputEvent.dataTransferInputEvent.dataTransfer属性返回一个 DataTransfer 实例。该属性只在文本框接受粘贴内容(insertFromPaste)或拖拽内容(insertFromDrop)时才有效。八、触摸事件1、触摸操作概述浏览器的触摸 API 由三个部分组成。Touch:一个触摸点的实例TouchList:多个触摸点集合的实例TouchEvent:触摸引发的事件实例Touch接口的实例对象用来表示触摸点(一根手指或者一根触摸笔),包括位置、大小、形状、压力、目标元素等属性。有时,触摸动作由多个触摸点(多根手指)组成,多个触摸点的集合由TouchList接口的实例对象表示。TouchEvent接口的实例对象代表由触摸引发的事件,只有触摸屏才会引发这一类事件。很多时候,触摸事件和鼠标事件同时触发,即使这个时候并没有用到鼠标。这是为了让那些只定义鼠标事件、没有定义触摸事件的代码,在触摸屏的情况下仍然能用。如果想避免这种情况,可以用event.preventDefault方法阻止发出鼠标事件。TouchEvent {isTrusted: true, touches: TouchList, targetTouches: TouchList, changedTouches: TouchList, altKey: false, …} // TouchEvent接口 ,继承Event接口属性和方法
altKey: false
bubbles: true
cancelBubble: false
cancelable: false
changedTouches: TouchList // TouchList 接口 (所有触摸点集合)
0: Touch // Touch 接口 (单个触摸点)
clientX: 232
clientY: 96
force: 1 // 触摸压力
identifier: 0 // 唯一ID
pageX: 232
pageY: 96
radiusX: 11.5
radiusY: 11.5
region: null
rotationAngle: 0
screenX: 476
screenY: 266
target: html // 触摸目标元素
__proto__: Touch
length: 1
__proto__: TouchList
composed: true
ctrlKey: false
currentTarget: null
defaultPrevented: false
detail: 0
eventPhase: 0
isTrusted: true
metaKey: false
path: (3) [html, document, Window]
returnValue: true
shiftKey: false
sourceCapabilities: InputDeviceCapabilities {firesTouchEvents: true}
srcElement: html
target: html
targetTouches: TouchList {0: Touch, length: 1} // TouchList 接口 (所有触摸点集合)
timeStamp: 994.1749999998137
touches: TouchList {0: Touch, length: 1} // TouchList 接口 (所有触摸点集合)
type: "touchmove" // 当前触摸事件类型
view: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
which: 0
2、Touch 接口2.1 Touch 接口概述 (单个触摸点)Touch 接口代表单个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。浏览器原生提供Touch构造函数,用来生成Touch实例。var touch = new Touch(touchOptions);Touch构造函数接受一个配置对象作为参数,它有以下属性。identifier:必需,类型为整数,表示触摸点的唯一 ID。target:必需,类型为元素节点,表示触摸点开始时所在的网页元素。clientX:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的水平距离,默认为0。clientY:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的垂直距离,默认为0。screenX:可选,类型为数值,表示触摸点相对于屏幕左上角的水平距离,默认为0。screenY:可选,类型为数值,表示触摸点相对于屏幕左上角的垂直距离,默认为0。pageX:可选,类型为数值,表示触摸点相对于网页左上角的水平位置(即包括页面的滚动距离),默认为0。pageY:可选,类型为数值,表示触摸点相对于网页左上角的垂直位置(即包括页面的滚动距离),默认为0。radiusX:可选,类型为数值,表示触摸点周围受到影响的椭圆范围的 X 轴半径,默认为0。radiusY:可选:类型为数值,表示触摸点周围受到影响的椭圆范围的 Y 轴半径,默认为0。rotationAngle:可选,类型为数值,表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间,默认值为0。force:可选,类型为数值,范围在0到1之间,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力,默认为0。2.2 Touch 接口的实例属性(1)Touch.identifier 触摸点的IDTouch.identifier属性返回一个整数,表示触摸点的唯一 ID。这个值在整个触摸过程保持不变,直到触摸事件结束。someElement.addEventListener('touchmove', function (e) {
for (var i = 0; i < e.changedTouches.length; i++) {
console.log(e.changedTouches[i].identifier);
}
}, false);(2)Touch.screenX,Touch.screenY,Touch.clientX,Touch.clientY,pageX,pageY (相对屏幕、浏览器、文档的坐标)Touch.screenX属性和Touch.screenY属性,分别表示触摸点相对于屏幕左上角的横坐标和纵坐标,与页面是否滚动无关。Touch.clientX属性和Touch.clientY属性,分别表示触摸点相对于浏览器视口左上角的横坐标和纵坐标,与页面是否滚动无关。Touch.pageX属性和Touch.pageY属性,分别表示触摸点相对于当前页面左上角的横坐标和纵坐标,包含了页面滚动带来的位移。(3)Touch.radiusX,Touch.radiusY,Touch.rotationAngle (触摸椭圆区域半径、角度)Touch.radiusX属性和Touch.radiusY属性,分别返回触摸点周围受到影响的椭圆范围的 X 轴半径和 Y 轴半径,单位为像素。乘以 2 就可以得到触摸范围的宽度和高度。Touch.rotationAngle属性表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间。上面这三个属性共同定义了用户与屏幕接触的区域,对于描述手指这一类非精确的触摸,很有帮助。指尖接触屏幕,触摸范围会形成一个椭圆,这三个属性就用来描述这个椭圆区域。下面是一个示例。div.addEventListener('touchstart', rotate);
div.addEventListener('touchmove', rotate);
div.addEventListener('touchend', rotate);
function rotate(e) {
var touch = e.changedTouches.item(0);
e.preventDefault();
src.style.width = touch.radiusX * 2 + 'px';
src.style.height = touch.radiusY * 2 + 'px';
src.style.transform = 'rotate(' + touch.rotationAngle + 'deg)';
};(4)Touch.force 触摸压力Touch.force属性返回一个0到1之间的数值,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力。(5)Touch.target 开始触摸时的元素Touch.target属性返回一个元素节点,代表触摸发生时所在的那个元素节点。即使触摸点已经离开了这个节点,该属性依然不变。3、TouchList 接口TouchList接口表示一组触摸点的集合。它的实例是一个类似数组的对象,成员是Touch的实例对象,表示所有触摸点。用户用三根手指触摸,产生的TouchList实例就会包含三个成员,每根手指的触摸点对应一个Touch实例对象。它的实例主要通过触摸事件的TouchEvent.touches、TouchEvent.changedTouches、TouchEvent.targetTouches这几个属性获取。它的实例属性和实例方法只有两个。TouchList.length:数值,表示成员数量(即触摸点的数量)。TouchList.item():返回指定位置的成员,它的参数是该成员的位置编号(从零开始)。4、TouchEvent 接口4.1 概述TouchEvent 接口继承了 Event 接口,表示由触摸引发的事件实例,通常来自触摸屏或轨迹板。除了被继承的属性以外,它还有一些自己的属性。浏览器原生提供TouchEvent()构造函数,用来生成触摸事件的实例。new TouchEvent(type, options)TouchEvent()构造函数可以接受两个参数,第一个参数是字符串,表示事件类型;第二个参数是事件的配置对象,该参数是可选的,对象的所有属性也是可选的。除了Event接口的配置属性,该接口还有一些自己的配置属性。touches:TouchList实例,代表所有的当前处于活跃状态的触摸点,默认值是一个空数组[]。targetTouches:TouchList实例,代表所有处在触摸的目标元素节点内部、且仍然处于活动状态的触摸点,默认值是一个空数组[]。changedTouches:TouchList实例,代表本次触摸事件的相关触摸点,默认值是一个空数组[]。ctrlKey:布尔值,表示 Ctrl 键是否同时按下,默认值为false。shiftKey:布尔值,表示 Shift 键是否同时按下,默认值为false。altKey:布尔值,表示 Alt 键是否同时按下,默认值为false。metaKey:布尔值,表示 Meta 键(或 Windows 键)是否同时按下,默认值为false。4.2 实例属性TouchEvent 接口的实例具有Event实例的所有属性和方法,此外还有一些它自己的实例属性,这些属性全部都是只读。(1)TouchEvent.altKey,TouchEvent.ctrlKey,TouchEvent.shiftKey,TouchEvent.metaKey (是否同时按某些功能键)TouchEvent.altKey:布尔值,表示触摸时是否按下了 Alt 键。TouchEvent.ctrlKey:布尔值,表示触摸时是否按下了 Ctrl 键。TouchEvent.shiftKey:布尔值:表示触摸时是否按下了 Shift 键。TouchEvent.metaKey:布尔值,表示触摸时是否按下了 Meta 键(或 Windows 键)。下面是一个示例。someElement.addEventListener('touchstart', function (e) {
console.log('altKey = ' + e.altKey);
console.log('ctrlKey = ' + e.ctrlKey);
console.log('metaKey = ' + e.metaKey);
console.log('shiftKey = ' + e.shiftKey);
}, false);(2)TouchEvent.changedTouches (触摸点集合,不同触摸事件,含义不同)TouchEvent.changedTouches属性返回一个TouchList实例,成员是一组Touch实例对象,表示本次触摸事件的相关触摸点。对于不同的事件,该属性的含义有所不同。touchstart事件:被激活的触摸点touchmove事件:发生变化的触摸点touchend事件:消失的触摸点(即不再被触碰的点)下面是一个示例。someElement.addEventListener('touchmove', function (e) {
for (var i = 0; i < e.changedTouches.length; i++) {
console.log(e.changedTouches[i].identifier);
}
}, false);(3)TouchEvent.touches (仍然活动的触摸点集合)TouchEvent.touches属性返回一个TouchList实例,成员是所有仍然处于活动状态(即触摸中)的触摸点。一般来说,一个手指就是一个触摸点。下面是一个示例。someElement.addEventListener('touchstart', function (e) {
switch (e.touches.length) {
// 一根手指触摸
case 1: handle_one_touch(e); break;
// 两根手指触摸
case 2: handle_two_touches(e); break;
// 三根手指触摸
case 3: handle_three_touches(e); break;
// 其他情况
default: console.log('Not supported'); break;
}
}, false);(4)TouchEvent.targetTouches (目标元素内活动的触摸点集合)TouchEvent.targetTouches属性返回一个TouchList实例,成员是触摸事件的目标元素节点内部、所有仍然处于活动状态(即触摸中)的触摸点。function touches_in_target(ev) {
return (ev.touches.length === ev.targetTouches.length ? true : false);
}上面代码用来判断,是否所有触摸点都在目标元素内。5、触摸事件的种类触摸引发的事件,有以下几种。可以通过TouchEvent.type属性,查看到底发生的是哪一种事件。touchstart:用户开始触摸时触发,它的target属性返回发生触摸的元素节点。【开始触摸】touchend:用户不再接触触摸屏时(或者移出屏幕边缘时)触发,它的target属性与touchstart事件一致的,就是开始触摸时所在的元素节点。它的changedTouches属性返回一个TouchList实例,包含所有不再触摸的触摸点(即Touch实例对象)。【触摸结束】touchmove:用户移动触摸点时触发,它的target属性与touchstart事件一致。如果触摸的半径、角度、力度发生变化,也会触发该事件。【触摸移动中】touchcancel:触摸点取消时触发,比如在触摸区域跳出一个模态窗口(modal window)、触摸点离开了文档区域(进入浏览器菜单栏)、用户的触摸点太多,超过了支持的上限(自动取消早先的触摸点)。【触摸点被取消】下面是一个例子。var el = document.getElementsByTagName('canvas')[0];
el.addEventListener('touchstart', handleStart, false);
el.addEventListener('touchmove', handleMove, false);
function handleStart(evt) {
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
console.log(touches[i].pageX, touches[i].pageY);
}
}
function handleMove(evt) {
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
var touch = touches[i];
console.log(touch.pageX, touch.pageY);
}
}九、拖拉事件1、拖拉事件的种类拖拉(drag)指的是,用户在某个对象上按下鼠标键不放,拖动它到另一个位置,然后释放鼠标键,将该对象放在那里。拖拉的对象有好几种,包括元素节点、图片、链接、选中的文字等等。在网页中,除了元素节点默认不可以拖拉,其他(图片、链接、选中的文字)都是可以直接拖拉的。为了让元素节点可拖拉,可以将该节点的draggable属性设为true。<div draggable="true">
此区域可拖拉
</div>draggable属性可用于任何元素节点,但是图片( <img> )和链接( <a> )不加这个属性,就可以拖拉。对于它们,用到这个属性的时候,往往是将其设为false,防止拖拉这两种元素。注意,一旦某个元素节点的draggable属性设为true,就无法再用鼠标选中该节点内部的文字或子节点了。当元素节点或选中的文本被拖拉时,就会持续触发拖拉事件,包括以下一些事件。拖拉的节点上触发:drag:拖拉过程中,在被拖拉的节点上持续触发(相隔几百毫秒)。dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。通常应该在这个事件的监听函数中,指定拖拉的数据。dragend:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。它与dragstart事件,在同一个节点上触发。不管拖拉是否跨窗口,或者中途被取消,dragend事件总是会触发的。拖拉到别的节点上触发:dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。drop:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发。注意,如果当前节点不允许drop,即使在该节点上方松开鼠标键,也不会触发该事件。如果用户按下 ESC 键,取消这个操作,也不会触发该事件。该事件的监听函数负责取出拖拉数据,并进行相关处理。下面的例子展示,如何动态改变被拖动节点的背景色。div.addEventListener('dragstart', function (e) {
this.style.backgroundColor = 'red';
}, false);
div.addEventListener('dragend', function (e) {
this.style.backgroundColor = 'green';
}, false);上面代码中,div节点被拖动时,背景色会变为红色,拖动结束,又变回绿色。<div class="div1" draggable="true">
div1,此区域可拖拉
</div>
<div class="div2">
div2
</div>
<script>
var div1 = document.querySelector('.div1');
var div2 = document.querySelector('.div2');
div1.addEventListener('dragstart', function (e){
console.log('开始拖拉')
});
div1.addEventListener('drag', function (e) {
console.log('拖拉中') // 持续触发
});
div1.addEventListener('dragend', function (e){
console.log('结束拖拉')
});
div2.addEventListener('dragenter', function(){
console.log('拖到了div2')
})
div2.addEventListener('dragover', function(){
//console.log('正在div2上方') // 持续触发
})
div2.addEventListener('dragleave', function(){
console.log('离开div2')
})
div2.addEventListener('drop', function() {
console.log('在div2上释放')
})
</script>下面是一个例子,展示如何实现将一个节点从当前父节点,拖拉到另一个父节点中。/* HTML 代码如下
<div class="dropzone">
<div id="draggable" draggable="true">
该节点可拖拉
</div>
</div>
<div class="dropzone"></div>
<div class="dropzone"></div>
<div class="dropzone"></div>
*/
// 被拖拉节点
var dragged;
document.addEventListener('dragstart', function (event) {
// 保存被拖拉节点
dragged = event.target;
// 被拖拉节点的背景色变透明
event.target.style.opacity = 0.5;
}, false);
document.addEventListener('dragend', function (event) {
// 被拖拉节点的背景色恢复正常
event.target.style.opacity = '';
}, false);
document.addEventListener('dragover', function (event) {
// 防止拖拉效果被重置,允许被拖拉的节点放入目标节点
event.preventDefault();
}, false);
document.addEventListener('dragenter', function (event) {
// 目标节点的背景色变紫色
// 由于该事件会冒泡,所以要过滤节点
if (event.target.className === 'dropzone') {
event.target.style.background = 'purple';
}
}, false);
document.addEventListener('dragleave', function( event ) {
// 目标节点的背景色恢复原样
if (event.target.className === 'dropzone') {
event.target.style.background = '';
}
}, false);
document.addEventListener('drop', function( event ) {
// 防止事件默认行为(比如某些元素节点上可以打开链接),
event.preventDefault();
if (event.target.className === 'dropzone') {
// 恢复目标节点背景色
event.target.style.background = '';
// 将被拖拉节点插入目标节点
dragged.parentNode.removeChild(dragged);
event.target.appendChild( dragged );
}
}, false);关于拖拉事件,有以下几个注意点。拖拉过程只触发以上这些拖拉事件,尽管鼠标在移动,但是鼠标事件不会触发。将文件从操作系统拖拉进浏览器,不会触发dragstart和dragend事件。dragenter和dragover事件的监听函数,用来取出拖拉的数据(即允许放下被拖拉的元素)。由于网页的大部分区域不适合作为放下拖拉元素的目标节点,所以这两个事件的默认设置为当前节点不允许接受被拖拉的元素。如果想要在目标节点上放下的数据,首先必须阻止这两个事件的默认行为。<div ondragover="return false">
<div ondragover="event.preventDefault()">上面代码中,如果不取消拖拉事件或者阻止默认行为,就不能在div节点上放下被拖拉的节点。2、DragEvent 接口拖拉事件都继承了DragEvent接口,这个接口又继承了MouseEvent接口和Event接口。浏览器原生提供一个DragEvent()构造函数,用来生成拖拉事件的实例对象。new DragEvent(type, options)DragEvent()构造函数接受两个参数,第一个参数是字符串,表示事件的类型,该参数必须;第二个参数是事件的配置对象,用来设置事件的属性,该参数可选。配置对象除了接受MouseEvent接口和Event接口的配置属性,还可以设置dataTransfer属性要么是null,要么是一个DataTransfer接口的实例。DataTransfer的实例对象用来读写拖拉事件中传输的数据,详见下文《DataTransfer 接口》的部分。3、DataTransfer 接口概述所有拖拉事件的实例都有一个DragEvent.dataTransfer属性,用来读写需要传递的数据。这个属性的值是一个DataTransfer接口的实例。浏览器原生提供一个DataTransfer()构造函数,用来生成DataTransfer实例对象。var dataTrans = new DataTransfer();DataTransfer()构造函数不接受参数。拖拉的数据分成两方面:数据的种类(又称格式)和数据的值。数据的种类是一个 MIME 字符串(比如text/plain、image/jpeg),数据的值是一个字符串。一般来说,如果拖拉一段文本,则数据默认就是那段文本;如果拖拉一个链接,则数据默认就是链接的 URL。拖拉事件开始时,开发者可以提供数据类型和数据值。拖拉过程中,开发者通过dragenter和dragover事件的监听函数,检查数据类型,以确定是否允许放下(drop)被拖拉的对象。比如,在只允许放下链接的区域,检查拖拉的数据类型是否为text/uri-list。发生drop事件时,监听函数取出拖拉的数据,对其进行处理。4、DataTransfer 的实例属性4.1 DataTransfer.dropEffect 设置接受拖拉的区域的效果DataTransfer.dropEffect属性用来设置放下(drop)被拖拉节点时的效果,会影响到拖拉经过相关区域时鼠标的形状。它可能取下面的值。copy:复制被拖拉的节点move:移动被拖拉的节点link:创建指向被拖拉的节点的链接none:无法放下被拖拉的节点除了上面这些值,设置其他的值都是无效的。target.addEventListener('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
});上面代码中,被拖拉元素一旦drop,接受的区域会复制该节点。dropEffect属性一般在dragenter和dragover事件的监听函数中设置,对于dragstart、drag、dragleave这三个事件,该属性不起作用。因为该属性只对接受被拖拉的节点的区域有效,对被拖拉的节点本身是无效的。进入目标区域后,拖拉行为会初始化成设定的效果。4.2 DataTransfer.effectAllowed 设置被拖拉的节点允许的效果DataTransfer.effectAllowed属性设置本次拖拉中允许的效果。它可能取下面的值。copy:复制被拖拉的节点move:移动被拖拉的节点link:创建指向被拖拉节点的链接copyLink:允许copy或linkcopyMove:允许copy或movelinkMove:允许link或moveall:允许所有效果none:无法放下被拖拉的节点uninitialized:默认值,等同于all如果某种效果是不允许的,用户就无法在目标节点中达成这种效果。这个属性与dropEffect属性是同一件事的两个方面。前者设置被拖拉的节点允许的效果,后者设置接受拖拉的区域的效果,它们往往配合使用。dragstart事件的监听函数,可以用来设置这个属性。其他事件的监听函数里面设置这个属性是无效的。source.addEventListener('dragstart', function (e) { // 被拖拉节点上设置
e.dataTransfer.effectAllowed = 'move';
});
target.addEventListener('dragover', function (e) { // 接受区域节点上设置
e.dataTransfer.dropEffect = 'move';
});只要dropEffect属性和effectAllowed属性之中,有一个为none,就无法在目标节点上完成drop操作。4.3 DataTransfer.files 本地文件DataTransfer.files属性是一个 FileList 对象,包含一组本地文件,可以用来在拖拉操作中传送。如果本次拖拉不涉及文件,则该属性为空的 FileList 对象。例子:接收拖拉文件// HTML 代码如下
// <div id="output" style="min-height: 200px;border: 1px solid black;">
// 文件拖拉到这里
// </div>
var div = document.getElementById('output');
div.addEventListener("dragenter", function( event ) {
div.textContent = '';
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("dragover", function( event ) {
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("drop", function( event ) {
event.stopPropagation();
event.preventDefault();
var files = event.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
div.textContent += files[i].name + ' ' + files[i].size + '字节\n';
}
}, false);上面代码中,通过dataTransfer.files属性读取被拖拉的文件的信息。如果想要读取文件内容,就要使用FileReader对象。div.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
var fileList = e.dataTransfer.files;
if (fileList.length > 0) {
var file = fileList[0];
var reader = new FileReader();
reader.readAsDataURL(file);// 解析为url
reader.onloadend = function(e) {
if (e.target.readyState === FileReader.DONE) {
var content = reader.result;
div.innerHTML = '<img src="'+ content +'"> File: ' + file.name + '\n\n' + content;
}
}
reader.readAsBinaryString(file);
}
});4.4 DataTransfer.types 数据格式DataTransfer.types属性是一个只读的数组,每个成员是一个字符串,里面是拖拉的数据格式(通常是 MIME 值)。比如,如果拖拉的是文字,对应的成员就是text/plain。下面是一个例子,通过检查dataTransfer属性的类型,决定是否允许在当前节点执行drop操作。function contains(list, value){
for (var i = 0; i < list.length; ++i) {
if(list[i] === value) return true;
}
return false;
}
function doDragOver(event) {
var isLink = contains(event.dataTransfer.types, 'text/uri-list');
if (isLink) event.preventDefault();
}上面代码中,只有当被拖拉的节点是一个链接时,才允许在当前节点放下。4.5 DataTransfer.itemsDataTransfer.items属性返回一个类似数组的只读对象(DataTransferItemList 实例),每个成员就是本次拖拉的一个对象(DataTransferItem 实例)。如果本次拖拉不包含对象,则返回一个空对象。DataTransferItemList 实例具有以下的属性和方法。length:返回成员的数量add(data, type):增加一个指定内容和类型(比如text/html和text/plain)的字符串作为成员add(file):add方法的另一种用法,增加一个文件作为成员remove(index):移除指定位置的成员clear():移除所有的成员DataTransferItem 实例具有以下的属性和方法。kind:返回成员的种类(string还是file)。type:返回成员的类型(通常是 MIME 值)。getAsFile():如果被拖拉是文件,返回该文件,否则返回null。getAsString(callback):如果被拖拉的是字符串,将该字符传入指定的回调函数处理。该方法是异步的,所以需要传入回调函数。下面是一个例子。div.addEventListener('drop', function (e) {
e.preventDefault();
if (e.dataTransfer.items != null) {
for (var i = 0; i < e.dataTransfer.items.length; i++) {
console.log(e.dataTransfer.items[i].kind + ': ' + e.dataTransfer.items[i].type);
}
}
});5、DataTransfer 的实例方法5.1 DataTransfer.setData() 设置拖拉事件所带有的数据DataTransfer.setData()方法用来设置拖拉事件所带有的数据。该方法没有返回值。event.dataTransfer.setData('text/plain', 'Text to drag');上面代码为当前的拖拉事件加入纯文本数据。该方法接受两个参数,都是字符串。第一个参数表示数据类型(比如text/plain),第二个参数是具体数据。如果指定类型的数据在dataTransfer属性不存在,那么这些数据将被加入,否则原有的数据将被新数据替换。如果是拖拉文本框或者拖拉选中的文本,会默认将对应的文本数据,添加到dataTransfer属性,不用手动指定。<div draggable="true">
aaa
</div>上面代码中,拖拉这个 <div> 元素会自动带上文本数据aaa。使用setData方法,可以替换到原有数据。<div
draggable="true"
ondragstart="event.dataTransfer.setData('text/plain', 'bbb')"
>
aaa
</div>上面代码中,拖拉数据实际上是bbb,而不是aaa。下面是添加其他类型的数据。由于text/plain是最普遍支持的格式,为了保证兼容性,建议最后总是保存一份纯文本格式的数据。var dt = event.dataTransfer;
// 添加链接
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');
// 添加 HTML 代码
dt.setData('text/html', 'Hello there, <strong>stranger</strong>');
dt.setData('text/plain', 'Hello there, <strong>stranger</strong>');
// 添加图像的 URL
dt.setData('text/uri-list', imageurl);
dt.setData('text/plain', imageurl);可以一次提供多种格式的数据。var dt = event.dataTransfer;
dt.setData('application/x-bookmark', bookmarkString);
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');上面代码中,通过在同一个事件上面,存放三种类型的数据,使得拖拉事件可以在不同的对象上面,drop不同的值。注意,第一种格式是一个自定义格式,浏览器默认无法读取,这意味着,只有某个部署了特定代码的节点,才可能drop(读取到)这个数据。5.2 DataTransfer.getData() 返回指定类型的数据DataTransfer.getData()方法接受一个字符串(表示数据类型)作为参数,返回事件所带的指定类型的数据(通常是用setData方法添加的数据)。如果指定类型的数据不存在,则返回空字符串。通常只有drop事件触发后,才能取出数据。下面是一个drop事件的监听函数,用来取出指定类型的数据。function onDrop(event) {
var data = event.dataTransfer.getData('text/plain');
event.target.textContent = data;
event.preventDefault();
}上面代码取出拖拉事件的文本数据,将其替换成当前节点的文本内容。注意,这时还必须取消浏览器的默认行为,因为假如用户拖拉的是一个链接,浏览器默认会在当前窗口打开这个链接。getData方法返回的是一个字符串,如果其中包含多项数据,就必须手动解析。function doDrop(event) {
var lines = event.dataTransfer.getData('text/uri-list').split('\n');
for (let line of lines) {
let link = document.createElement('a');
link.href = line;
link.textContent = line;
event.target.appendChild(link);
}
event.preventDefault();
}上面代码中,getData方法返回的是一组链接,就必须自行解析。类型值指定为URL,可以取出第一个有效链接。var link = event.dataTransfer.getData('URL');下面的例子是从多种类型的数据里面取出数据。function doDrop(event) {
var types = event.dataTransfer.types;
var supportedTypes = ['text/uri-list', 'text/plain'];
types = supportedTypes.filter(function (value) { types.includes(value) });
if (types.length) {
var data = event.dataTransfer.getData(types[0]);
}
event.preventDefault();
}5.3 DataTransfer.clearData() 清除指定或全部数据DataTransfer.clearData()方法接受一个字符串(表示数据类型)作为参数,删除事件所带的指定类型的数据。如果没有指定类型,则删除所有数据。如果指定类型不存在,则调用该方法不会产生任何效果。event.dataTransfer.clearData('text/uri-list');上面代码清除事件所带的text/uri-list类型的数据。该方法不会移除拖拉的文件,因此调用该方法后,DataTransfer.types属性可能依然会返回Files类型(前提是存在文件拖拉)。注意,该方法只能在dragstart事件的监听函数之中使用,因为这是拖拉操作的数据唯一可写的时机。5.4 DataTransfer.setDragImage() 设置拖动过程中的图片拖动过程中(dragstart事件触发后),浏览器会显示一张图片跟随鼠标一起移动,表示被拖动的节点。这张图片是自动创造的,通常显示为被拖动节点的外观,不需要自己动手设置。DataTransfer.setDragImage()方法可以自定义这张图片。它接受三个参数。第一个是 <img> 节点或者 <canvas> 节点,如果省略或为null,则使用被拖动的节点的外观;第二个和第三个参数为鼠标相对于该图片左上角的横坐标和右坐标。下面是一个例子。/* HTML 代码如下
<div id="drag-with-image" class="dragdemo" draggable="true">
drag me
</div>
*/
var div = document.getElementById('drag-with-image');
div.addEventListener('dragstart', function (e) {
var img = document.createElement('img');
img.src = 'http://path/to/img';
e.dataTransfer.setDragImage(img, 0, 0);
}, false);十、其他常见事件1、资源事件1.1 beforeunload 事件 (关闭窗口前调用)beforeunload事件在窗口、文档、各种资源将要卸载前触发。它可以用来防止用户不小心卸载资源。如果该事件对象的returnValue属性是一个非空字符串,那么浏览器就会弹出一个对话框,询问用户是否要卸载该资源。但是,用户指定的字符串可能无法显示,浏览器会展示预定义的字符串。如果用户点击“取消”按钮,资源就不会卸载。window.addEventListener('beforeunload', function (event) {
event.returnValue = '你确定离开吗?';
});上面代码中,用户如果关闭窗口,浏览器会弹出一个窗口,要求用户确认。浏览器对这个事件的行为很不一致,有的浏览器调用event.preventDefault(),也会弹出对话框。IE 浏览器需要显式返回一个非空的字符串,才会弹出对话框。而且,大多数浏览器在对话框中不显示指定文本,只显示默认文本。因此,可以采用下面的写法,取得最大的兼容性。window.addEventListener('beforeunload', function (e) {
var confirmationMessage = '确认关闭窗口?';
e.returnValue = confirmationMessage;
return confirmationMessage;
});注意,许多手机浏览器默认忽略这个事件,桌面浏览器也有办法忽略这个事件。所以,它可能根本不会生效,不能依赖它来阻止用户关闭窗口。另外,一旦使用了beforeunload事件,浏览器就不会缓存当前网页,使用“回退”按钮将重新向服务器请求网页。这是因为监听这个事件的目的,一般是修改初始状态,这时缓存初始页面就没意义了。基本上,只有一种场合可以监听unload事件,其他情况都不应该监听:用户修改了表单,还没有保存就要离开。1.2 unload 事件 (即将关闭窗口时调用)unload事件在窗口关闭或者document对象将要卸载时触发。它的触发顺序排在beforeunload、pagehide事件后面。unload事件发生时,文档处于一个特殊状态。所有资源依然存在,但是对用户来说都不可见,UI 互动全部无效。这个事件是无法取消的,即使在监听函数里面抛出错误,也不能停止文档的卸载。window.addEventListener('unload', function(event) {
console.log('文档将要卸载');
});手机上,浏览器或系统可能会直接丢弃网页,这时该事件根本不会发生。而且跟beforeunload事件一样,一旦使用了unload事件,浏览器就不会缓存当前网页,理由同上。因此,任何情况下都不应该依赖这个事件,指定网页卸载时要执行的代码,可以考虑完全不使用这个事件。1.3 load 事件,error 事件 ,abort事件 (页面或某个资源加载成功/失败/取消时调用)load事件在页面或某个资源加载成功时触发。注意,页面或资源从浏览器缓存加载,并不会触发load事件。window.addEventListener('load', function(event) {
console.log('所有资源都加载完成');
});error事件是在页面或资源加载失败时触发。abort事件是在用户取消加载时触发。这三个事件实际上属于进度事件,不仅发生在document对象,还发生在各种外部资源上面。浏览网页就是一个加载各种资源的过程,图像(image)、样式表(style sheet)、脚本(script)、视频(video)、音频(audio)、Ajax请求(XMLHttpRequest)等等。这些资源和document对象、window对象、XMLHttpRequestUpload 对象,都会触发load事件和error事件。触发这三个事件的对象有:windowdocumentbodyimgstylescriptvideoaudioAjax等等2、session 历史事件2.1 pageshow 事件(页面显示,加载页面后执行),pagehide 事件(退出当前页面触发)默认情况下,浏览器会在当前会话(session)缓存页面,当用户点击“前进/后退”按钮时,浏览器就会从缓存中加载页面。pageshow 事件在页面加载时触发,包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载(不管是不是从浏览器缓存)时都运行的代码,可以放在这个事件的监听函数。第一次加载时,它的触发顺序排在load事件后面。从缓存加载时,load事件不会触发,因为网页在缓存中的样子通常是load事件的监听函数运行后的样子,所以不必重复执行。同理,如果是从缓存中加载页面,网页内初始化的 JavaScript 脚本(比如 DOMContentLoaded 事件的监听函数)也不会执行。window.addEventListener('pageshow', function(event) {
console.log('pageshow: ', event);
});pageshow 事件有一个persisted属性,返回一个布尔值。页面第一次加载时,这个属性是false;当页面从缓存加载时,这个属性是true。window.addEventListener('pageshow', function(event){
if (event.persisted) { // 是否从缓存加载
// ...
}
});pagehide事件与pageshow事件类似,当用户通过“前进/后退”按钮,离开当前页面时触发。它与 unload 事件的区别在于,如果在 window 对象上定义unload事件的监听函数之后,页面不会保存在缓存中,而使用pagehide事件,页面会保存在缓存中。pagehide事件实例也有一个persisted属性,将这个属性设为true,就表示页面要保存在缓存中;设为false,表示网页不保存在缓存中,这时如果设置了unload 事件的监听函数,该函数将在 pagehide 事件后立即运行。如果页面包含 <frame> 或 <iframe> 元素,则 <frame> 页面的pageshow事件和pagehide事件,都会在主页面之前触发。注意,这两个事件只在浏览器的history(历史)对象发生变化时触发,跟网页是否可见没有关系。2.2 popstate 事件(在浏览器的history对象的当前记录发生显式切换时触发)popstate事件在浏览器的history对象的当前记录发生显式切换时触发。注意,调用history.pushState()或history.replaceState(),并不会触发popstate事件。该事件只在用户在history记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用history.back()、history.forward()、history.go()时触发。该事件对象有一个state属性,保存history.pushState方法和history.replaceState方法为当前记录添加的state对象。window.onpopstate = function (event) {
console.log('state: ' + event.state);
};
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back(); // state: {"page":1}
history.back(); // state: null
history.go(2); // state: {"page":3}上面代码中,pushState方法向history添加了两条记录,然后replaceState方法替换掉当前记录。因此,连续两次back方法,会让当前条目退回到原始网址,它没有附带state对象,所以事件的state属性为null,然后前进两条记录,又回到replaceState方法添加的记录。浏览器对于页面首次加载,是否触发popstate事件,处理不一样,Firefox 不触发该事件。2.3 hashchange 事件 (hash发生变化时触发)hashchange事件在 URL 的 hash 部分(即#号后面的部分,包括#号)发生变化时触发。该事件一般在window对象上监听。hashchange的事件实例具有两个特有属性:oldURL属性和newURL属性,分别表示变化前后的完整 URL。// URL 是 http://www.example.com/
window.addEventListener('hashchange', myFunction);
function myFunction(e) {
console.log(e.oldURL);
console.log(e.newURL);
}
location.hash = 'part2';
// http://www.example.com/
// http://www.example.com/#part23、网页状态事件3.1 DOMContentLoaded 事件 (DOM内容加载完成后触发)网页下载并解析完成以后,浏览器就会在document对象上触发 DOMContentLoaded 事件。这时,仅仅完成了网页的解析(整张页面的 DOM 生成了),所有外部资源(样式表、脚本、iframe 等等)可能还没有下载结束。也就是说,这个事件比load事件,发生时间早得多。document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM生成');
});注意,网页的 JavaScript 脚本是同步执行的,脚本一旦发生堵塞,将推迟触发DOMContentLoaded事件。document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM 生成');
});
// 这段代码会推迟触发 DOMContentLoaded 事件
for(var i = 0; i < 1000000000; i++) {
// ...
}3.2 readystatechange 事件(Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发)readystatechange事件当 Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发。document.readyState有三个可能的值:loading(网页正在加载)、interactive(网页已经解析完成,但是外部资源仍然处在加载状态)和complete(网页和所有外部资源已经结束加载,load事件即将触发)。document.onreadystatechange = function () {
if (document.readyState === 'interactive') {
// ...
}
}这个事件可以看作DOMContentLoaded事件的另一种实现方法。4、窗口事件4.1 scroll 事件 (文档或文档元素滚动时触发)scroll事件在文档或文档元素滚动时触发,主要出现在用户拖动滚动条。window.addEventListener('scroll', callback);该事件会连续地大量触发,所以它的监听函数之中不应该有非常耗费计算的操作。推荐的做法是使用requestAnimationFrame或setTimeout控制该事件的触发频率,然后可以结合customEvent抛出一个新事件。(function () {
var throttle = function (type, name, obj) { // 节流函数 控制触发频率
var obj = obj || window;
var running = false;
var func = function () {
if (running) { return; }
running = true;
requestAnimationFrame(function() { // 结合此方法控制在每秒60次
obj.dispatchEvent(new CustomEvent(name));
running = false;
});
};
obj.addEventListener(type, func);
};
// 将 scroll 事件重定义为 optimizedScroll 事件
throttle('scroll', 'optimizedScroll');
})();
window.addEventListener('optimizedScroll', function() {
console.log('Resource conscious scroll callback!');
});上面代码中,throttle函数用于控制事件触发频率,requestAnimationFrame方法保证每次页面重绘(每秒60次),只会触发一次scroll事件的监听函数。也就是说,上面方法将scroll事件的触发频率,限制在每秒60次。具体来说,就是scroll事件只要频率低于每秒60次,就会触发optimizedScroll事件,从而执行optimizedScroll事件的监听函数。改用setTimeout方法,可以放置更大的时间间隔。(function() {
window.addEventListener('scroll', scrollThrottler, false);
var scrollTimeout;
function scrollThrottler() {
if (!scrollTimeout) {
scrollTimeout = setTimeout(function () {
scrollTimeout = null;
actualScrollHandler();
}, 66);
}
}
function actualScrollHandler() {
// ...
}
}());上面代码中,每次scroll事件都会执行scrollThrottler函数。该函数里面有一个定时器setTimeout,每66毫秒触发一次(每秒15次)真正执行的任务actualScrollHandler。下面是一个更一般的throttle函数的写法。function throttle(fn, wait) {
var time = Date.now();
return function() {
if ((time + wait - Date.now()) < 0) {
fn();
time = Date.now();
}
}
}
window.addEventListener('scroll', throttle(callback, 1000));上面的代码将scroll事件的触发频率,限制在一秒一次。lodash函数库提供了现成的throttle函数,可以直接使用。window.addEventListener('scroll', _.throttle(callback, 1000));本书前面介绍过debounce的概念,throttle与它区别在于,throttle是“节流”,确保一段时间内只执行一次,而debounce是“防抖”,要连续操作结束后再执行。以网页滚动为例,debounce要等到用户停止滚动后才执行,throttle则是如果用户一直在滚动网页,那么在滚动过程中还是会执行。4.2 resize 事件(窗口大小变化时触发)resize事件在改变浏览器窗口大小时触发,主要发生在window对象上面。var resizeMethod = function () {
if (document.body.clientWidth < 768) {
console.log('移动设备的视口');
}
};
window.addEventListener('resize', resizeMethod, true);该事件也会连续地大量触发,所以最好像上面的scroll事件一样,通过throttle函数控制事件触发频率。4.3 fullscreenchange 事件(元素进入/退出全屏时触发),fullscreenerror 事件(无法切换全屏时触发)fullscreenchange事件在元素进入或退出全屏状态时触发,该事件发生在document对象上面。注意,此事件非浏览器的进入/退出全屏时触发的事件。document.addEventListener('fullscreenchange', function (event) {
console.log(document.fullscreenElement); // 全屏的元素,如果为null时表示已退出全屏
});
document.addEventListener('click', function (event) {
document.querySelector('div').requestFullscreen(); // 设置元素为全屏展示
});fullscreenerror事件在浏览器无法切换到全屏状态时触发。5、剪贴板事件以下三个事件属于剪贴板操作的相关事件。cut:将选中的内容从文档中移除,加入剪贴板时触发。【剪切】copy:进行复制动作时触发。【拷贝】paste:剪贴板内容粘贴到文档后触发。【粘贴】这三个事件都是ClipboardEvent接口的实例。ClipboardEvent有一个实例属性clipboardData,是一个 DataTransfer 对象,存放剪贴的数据。具体的 API 接口和操作方法,请参见《拖拉事件》的 DataTransfer 对象部分。document.addEventListener('copy', function (e) {
e.clipboardData.setData('text/plain', 'Hello, world!');
e.clipboardData.setData('text/html', '<b>Hello, world!</b>');
e.preventDefault();
});上面的代码使得复制进入剪贴板的,都是开发者指定的数据,而不是用户想要拷贝的数据。6、焦点事件焦点事件发生在元素节点和document对象上面,与获得或失去焦点相关。它主要包括以下四个事件。focus:元素节点获得焦点后触发,该事件不会冒泡。blur:元素节点失去焦点后触发,该事件不会冒泡。focusin:元素节点将要获得焦点时触发,发生在focus事件之前。该事件会冒泡。focusout:元素节点将要失去焦点时触发,发生在blur事件之前。该事件会冒泡。这四个事件都继承了FocusEvent接口。FocusEvent实例具有以下属性。FocusEvent.target:事件的目标节点。FocusEvent.relatedTarget:对于focusin事件,返回失去焦点的节点;对于focusout事件,返回将要接受焦点的节点;对于focus和blur事件,返回null。由于focus和blur事件不会冒泡,只能在捕获阶段触发,所以addEventListener方法的第三个参数需要设为true。form.addEventListener('focus', function (event) {
event.target.style.background = 'pink';
}, true);
form.addEventListener('blur', function (event) {
event.target.style.background = '';
}, true);上面代码针对表单的文本输入框,接受焦点时设置背景色,失去焦点时去除背景色。7、CustomEvent 接口(自定义事件)CustomEvent 接口用于生成自定义的事件实例。那些浏览器预定义的事件,虽然可以手动生成,但是往往不能在事件上绑定数据。如果需要在触发事件的同时,传入指定的数据,就可以使用 CustomEvent 接口生成的自定义事件对象。浏览器原生提供CustomEvent()构造函数,用来生成 CustomEvent 事件实例。new CustomEvent(type, options)CustomEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的名字,这是必须的。第二个参数是事件的配置对象,这个参数是可选的。CustomEvent的配置对象除了接受 Event 事件的配置属性,只有一个自己的属性。detail:表示事件的附带数据,默认为null。下面是一个例子。var event = new CustomEvent('build', { 'detail': 'hello' });
function eventHandler(e) {
console.log(e.detail);
}
document.body.addEventListener('build', function (e) {
console.log(e.detail);
});
document.body.dispatchEvent(event);上面代码中,我们手动定义了build事件。该事件触发后,会被监听到,从而输出该事件实例的detail属性(即字符串hello)。下面是另一个例子。var myEvent = new CustomEvent('myevent', {
detail: {
foo: 'bar'
},
bubbles: true,
cancelable: false
});
el.addEventListener('myevent', function (event) {
console.log('Hello ' + event.detail.foo);
});
el.dispatchEvent(myEvent);上面代码也说明,CustomEvent 的事件实例,除了具有 Event 接口的实例属性,还具有detail属性。十一、GlobalEventHandlers 接口 (全局事件处理接口)指定事件的回调函数,推荐使用的方法是元素的addEventListener方法。div.addEventListener('click', clickHandler, false);除了之外,还有一种方法可以直接指定事件的回调函数。div.onclick = clickHandler;这个接口是由GlobalEventHandlers接口提供的。它的优点是使用比较方便,缺点是只能为每个事件指定一个回调函数,并且无法指定事件触发的阶段(捕获阶段还是冒泡阶段)。HTMLElement、Document和Window都继承了这个接口,也就是说,各种 HTML 元素、document对象、window对象上面都可以使用GlobalEventHandlers接口提供的属性。下面就列出这个接口提供的主要的事件属性。1、GlobalEventHandlers.onabort (中断事件)某个对象的abort事件(停止加载)发生时,就会调用onabort属性指定的回调函数。各种元素的停止加载事件,到底如何触发,目前并没有统一的规定。因此实际上,这个属性现在一般只用在<img>元素上面。// HTML 代码如下
// <img src="example.jpg" id="img">
var img = document.getElementById('img');
img.onabort = function () {
console.log('图片加载中断');
}2、GlobalEventHandlers.onerror (错误事件)error事件发生时,就会调用onerror属性指定的回调函数。error事件分成两种。一种是 JavaScript 的运行时错误,这会传到window对象,导致window.onerror()。window.onerror = function (message, source, lineno, colno, error) {
// ...
}window.onerror的处理函数共接受五个参数,含义如下。message:错误信息字符串source:报错脚本的 URLlineno:报错的行号,是一个整数colno:报错的列号,是一个整数error: 错误对象另一种是资源加载错误,比如 <img> 或 <script> 加载的资源出现加载错误。这时,Error 对象会传到对应的元素,导致该元素的onerror属性开始执行。element.onerror = function (event) {
// ...
}注意,一般来说,资源的加载错误不会触发window.onerror。3、GlobalEventHandlers.onload(加载完成事件)、GlobalEventHandlers.onloadstart(开始加载事件)元素完成加载时,会触发load事件,执行onload()。它的典型使用场景是window对象和 <img> 元素。对于window对象来说,只有页面的所有资源加载完成(包括图片、脚本、样式表、字体等所有外部资源),才会触发load事件。对于 <img> 和 <video> 等元素,加载开始时还会触发loadstart事件,导致执行onloadstart。4、GlobalEventHandlers.onfocus(获取焦点事件),GlobalEventHandlers.onblur(失去焦点事件)当前元素获得焦点时,会触发element.onfocus;失去焦点时,会触发element.onblur。element.onfocus = function () {
console.log("onfocus event detected!");
};
element.onblur = function () {
console.log("onblur event detected!");
};注意,如果不是可以接受用户输入的元素,要触发onfocus,该元素必须有tabindex属性。5、GlobalEventHandlers.onscroll(滚动事件)页面或元素滚动时,会触发scroll事件,导致执行onscroll()。6、GlobalEventHandlers.oncontextmenu(右键菜单事件),GlobalEventHandlers.onshow(显示右键菜单时触发)用户在页面上按下鼠标的右键,会触发contextmenu事件,导致执行oncontextmenu()。如果该属性执行后返回false,就等于禁止了右键菜单。document.oncontextmenu与window.oncontextmenu效果一样。document.oncontextmenu = function () {// 禁用右键菜单
return false;
};上面代码中,oncontextmenu属性执行后返回false,右键菜单就不会出现。元素的右键菜单显示时,会触发该元素的onshow监听函数。7、其他的事件属性鼠标的事件属性。onclickondblclickonmousedownonmouseenteronmouseleaveonmousemoveonmouseoutonmouseoveronmouseuponwheel键盘的事件属性。onkeydownonkeypressonkeyup焦点的事件属性。onbluronfocus表单的事件属性。oninputonchangeonsubmitonresetoninvalidonselect触摸的事件属性。ontouchcancelontouchendontouchmoveontouchstart拖动的事件属性分成两类:一类与被拖动元素相关,另一类与接收被拖动元素的容器元素相关。被拖动元素的事件属性。ondragstart:拖动开始ondrag:拖动过程中,每隔几百毫秒触发一次ondragend:拖动结束接收被拖动元素的容器元素的事件属性。ondragenter:被拖动元素进入容器元素。ondragleave:被拖动元素离开容器元素。ondragover:被拖动元素在容器元素上方,每隔几百毫秒触发一次。ondrop:松开鼠标后,被拖动元素放入容器元素。 <dialog> 对话框元素的事件属性。oncancelonclose文档学习文档:https://wangdoc.com/javascript/
彻底搞懂JS的事件原理
事件一、EventTarget 接口事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。DOM 支持大量的事件,本章开始介绍 DOM 的事件编程。1、概述DOM 的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequest、AudioNode、AudioContext)也部署了这个接口。该接口主要提供三个实例方法。addEventListener:绑定事件的监听函数removeEventListener:移除事件的监听函数dispatchEvent:触发事件2、EventTarget.addEventListener()EventTarget.addEventListener()用于在当前节点或对象上,定义一个特定事件的监听函数。一旦这个事件发生,就会执行监听函数。该方法没有返回值。target.addEventListener(type, listener[, useCapture]);
// 事件名称,监听函数 [,是否在捕获阶段触发]该方法接受三个参数。type:事件名称,大小写敏感。listener:监听函数。事件发生时,会调用该监听函数。useCapture:布尔值,表示监听函数是否在捕获阶段(capture)触发(参见后文《事件的传播》部分),默认为false(监听函数只在冒泡阶段被触发)。该参数可选。下面是一个例子。function hello() {
console.log('Hello world');
}
var button = document.getElementById('btn');
button.addEventListener('click', hello, false);上面代码中,button节点的addEventListener方法绑定click事件的监听函数hello,该函数只在冒泡阶段触发。关于参数,有两个地方需要注意。首先,第二个参数除了监听函数,还可以是一个具有handleEvent方法的对象。buttonElement.addEventListener('click', {
handleEvent: function (event) {
console.log('click');
}
});上面代码中,addEventListener方法的第二个参数,就是一个具有handleEvent方法的对象。其次,第三个参数除了布尔值useCapture,还可以是一个属性配置对象。该对象有以下属性。capture:布尔值,表示该事件是否在捕获阶段触发监听函数。once:布尔值,表示监听函数是否只触发一次,然后就自动移除。passive:布尔值,表示监听函数不会调用事件的preventDefault方法。如果监听函数调用了,浏览器将忽略这个要求,并在监控台输出一行警告。如果希望事件监听函数只执行一次,可以打开属性配置对象的once属性。element.addEventListener('click', function (event) {
// 只执行一次的代码
}, {once: true});addEventListener方法可以为针对当前对象的同一个事件,添加多个不同的监听函数。这些函数按照添加顺序触发,即先添加先触发。如果为同一个事件多次添加同一个监听函数,该函数只会执行一次,多余的添加将自动被去除(不必使用removeEventListener方法手动去除)。function hello() {
console.log('Hello world');
}
document.addEventListener('click', hello, false);
document.addEventListener('click', hello, false);执行上面代码,点击文档只会输出一行Hello world。如果希望向监听函数传递参数,可以用匿名函数包装一下监听函数。function print(x) {
console.log(x);
}
var el = document.getElementById('div1');
el.addEventListener('click', function () { print('Hello'); }, false);上面代码通过匿名函数,向监听函数print传递了一个参数。监听函数内部的this,指向当前事件所在的那个对象。// HTML 代码如下
// <p id="para">Hello</p>
var para = document.getElementById('para');
para.addEventListener('click', function (e) {
console.log(this.nodeName); // "P"
}, false);上面代码中,监听函数内部的this指向事件所在的对象para。3、EventTarget.removeEventListener()EventTarget.removeEventListener方法用来移除addEventListener方法添加的事件监听函数。该方法没有返回值。div.addEventListener('click', listener, false);
div.removeEventListener('click', listener, false);removeEventListener方法的参数,与addEventListener方法完全一致。它的第一个参数“事件类型”,大小写敏感。注意,removeEventListener方法移除的监听函数,必须是addEventListener方法添加的那个监听函数,而且必须在同一个元素节点,否则无效。div.addEventListener('click', function (e) {}, false);
div.removeEventListener('click', function (e) {}, false);// 移除无效,因为不是同一个监听函数上面代码中,removeEventListener方法无效,因为监听函数不是同一个匿名函数。element.addEventListener('mousedown', handleMouseDown, true);
element.removeEventListener("mousedown", handleMouseDown, false); // 移除无效,第三个参数不一样上面代码中,removeEventListener方法也是无效的,因为第三个参数不一样。4、EventTarget.dispatchEvent()EventTarget.dispatchEvent方法在当前节点上触发指定事件,从而触发监听函数的执行。该方法返回一个布尔值,只要有一个监听函数调用了Event.preventDefault(),则返回值为false,否则为true。target.dispatchEvent(event)dispatchEvent方法的参数是一个Event对象的实例(详见《Event 对象》章节)。para.addEventListener('click', hello, false);
var event = new Event('click');
para.dispatchEvent(event);上面代码在当前节点触发了click事件。如果dispatchEvent方法的参数为空,或者不是一个有效的事件对象,将报错。下面代码根据dispatchEvent方法的返回值,判断事件是否被取消了。var canceled = !cb.dispatchEvent(event);
if (canceled) {
console.log('事件取消');
} else {
console.log('事件未取消');
}二、事件模型1、监听函数浏览器的事件模型,就是通过监听函数(listener)对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。JavaScript 有三种方法,可以为事件绑定监听函数。1.2 HTML 的 on- 属性HTML 语言允许在元素的属性中,直接定义某些事件的监听代码。<body onload="doSomething()"> <!-- 加圆括号-->
<div onclick="console.log('触发事件')">上面代码为body节点的load事件、div节点的click事件,指定了监听代码。一旦事件发生,就会执行这段代码。元素的事件监听属性,都是on加上事件名,比如onload就是on + load,表示load事件的监听代码。注意,这些属性的值是将会执行的代码,而不是一个函数。<!-- 正确 -->
<body onload="doSomething()">
<!-- 错误 -->
<body onload="doSomething">一旦指定的事件发生,on-属性的值是原样传入 JavaScript 引擎执行。因此如果要执行函数,不要忘记加上一对圆括号。使用这个方法指定的监听代码,只会在冒泡阶段触发。<div onClick="console.log(2)">
<button onClick="console.log(1)">点击</button>
</div>上面代码中, <button> 是 <div> 的子元素。 <button> 的click事件,也会触发 <div> 的click事件。由于on-属性的监听代码,只在冒泡阶段触发,所以点击结果是先输出1,再输出2,即事件从子元素开始冒泡到父元素。直接设置on-属性,与通过元素节点的setAttribute方法设置on-属性,效果是一样的。el.setAttribute('onclick', 'doSomething()');
// 等同于
// <Element onclick="doSomething()">1.2 元素节点的事件属性 (例:el.onclick)元素节点对象的事件属性,同样可以指定监听函数。window.onload = doSomething; // 注意这里是函数名,不加圆括号
div.onclick = function (event) { // onclick是元素的一个属性,它指向一个函数,在触发点击时执行此函数
console.log('触发事件');
};使用这个方法指定的监听函数,也是只会在冒泡阶段触发。注意,这种方法与 HTML 的on-属性的差异是,它的值是函数名(doSomething),而不像后者,必须给出完整的监听代码(doSomething())。1.3 EventTarget.addEventListener()所有 DOM 节点实例都有addEventListener方法,用来为该节点定义事件的监听函数。window.addEventListener('load', doSomething, false); // 注意这里是函数名,不加圆括号addEventListener方法的详细介绍,参见EventTarget章节。1.4 小结上面三种方法,第一种“HTML 的 on- 属性”,违反了 HTML 与 JavaScript 代码相分离的原则,将两者写在一起,不利于代码分工,因此不推荐使用。第二种“元素节点的事件属性”的缺点在于,同一个事件只能定义一个监听函数,也就是说,如果定义两次onclick属性,后一次定义会覆盖前一次。因此,也不推荐使用。第三种EventTarget.addEventListener是推荐的指定监听函数的方法。它有如下优点:同一个事件可以添加多个监听函数。能够指定在哪个阶段(捕获阶段还是冒泡阶段)触发监听函数。除了 DOM 节点,其他对象(比如window、XMLHttpRequest等)也有这个接口,它等于是整个 JavaScript 统一的监听函数接口。2、this 的指向 (指向元素节点)监听函数内部的this指向触发事件的那个元素节点。(三种事件绑定的this全指向元素节点)<button id="btn" onclick="console.log(this.id)">点击</button>执行上面代码,点击后会输出btn。其他两种监听函数的写法,this的指向也是如此。// HTML 代码如下
// <button id="btn">点击</button>
var btn = document.getElementById('btn');
// 写法一
btn.onclick = function () {
console.log(this.id);
};
// 写法二
btn.addEventListener(
'click',
function (e) {
console.log(this.id);
},
false
);上面两种写法,点击按钮以后也是输出btn。3、事件的传播(捕获、目标、冒泡)一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。第一阶段:从window对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。第三阶段:从目标节点传导回window对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。这种三阶段的传播模型,使得同一个事件会在多个节点上触发。<div>
<p>点击</p>
</div>上面代码中, <div> 节点之中有一个 <p> 节点。如果对这两个节点,都设置click事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对 <p> 点击,click事件会触发四次。var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true); // true 表示在捕获阶段触发
p.addEventListener('click', callback, true); // true 表示在捕获阶段触发
div.addEventListener('click', callback, false);// fasle 表示在冒泡阶段触发
p.addEventListener('click', callback, false); // fasle 表示在冒泡阶段触发
function callback(event) {
var tag = event.currentTarget.tagName; // 当前目标对象的标签名
var phase = phases[event.eventPhase]; // 触发的阶段
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 点击以后的结果
// Tag: 'DIV'. EventPhase: 'capture' 捕获阶段
// Tag: 'P'. EventPhase: 'target' 目标阶段
// Tag: 'P'. EventPhase: 'target' 目标阶段
// Tag: 'DIV'. EventPhase: 'bubble' 冒泡阶段上面代码表示,click事件被触发了四次: <div> 节点的捕获阶段和冒泡阶段各1次, <p> 节点的目标阶段触发了2次。捕获阶段:事件从 <div> 向 <p> 传播时,触发 <div> 的click事件;目标阶段:事件从 <div> 到达 <p> 时,触发 <p> 的click事件;冒泡阶段:事件从 <p> 传回 <div> 时,再次触发 <div> 的click事件。其中, <p> 节点有两个监听函数(addEventListener方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为click事件触发一次。所以, <p> 会在target阶段有两次输出。注意,浏览器总是假定click事件的目标节点,就是点击位置嵌套最深的那个节点(本例是 <div> 节点里面的 <p> 节点)。所以, <p> 节点的捕获阶段和冒泡阶段,都会显示为target阶段。事件传播的最上层对象是window,接着依次是document,html(document.documentElement)和body(document.body)。也就是说,上例的事件传播顺序,在捕获阶段依次为window、document、html、body、div、p,在冒泡阶段依次为p、div、body、html、document、window。捕获阶段:
window(浏览器对象)--> document(文档对象) --> html --> body --> div --> p (点击目标,在目标阶段)
冒泡阶段:
p (点击目标,在目标阶段)--> div --> body --> html --> document(文档对象) --> window(浏览器对象)4、事件的代理由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') { // 浏览器假定click嵌套最深的元素是目标元素
// some code
}
});上面代码中,click事件的监听函数定义在 <ul> 节点,但是实际上,它处理的是子节点 <li> 的click事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个 <li> 节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。阻止事件冒泡 event.stopPropagation()如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation方法。// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
event.stopPropagation(); // 该方法在事件对象event上
}, true); // true表示在捕获阶段绑定事件监听函数
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false); // false表示在冒泡阶段(默认值)绑定事件监听函数上面代码中,stopPropagation方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。但是,stopPropagation方法只会阻止事件的传播,不会阻止该事件触发 <p> 节点的其他click事件的监听函数。也就是说,不是彻底取消click事件。p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 会触发
console.log(2);
});上面代码中,p元素绑定了两个click事件的监听函数。stopPropagation方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。如果想要彻底取消该事件,不再触发后面所有click的监听函数,可以使用stopImmediatePropagation()方法。p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不会被触发
console.log(2);
});上面代码中,stopImmediatePropagation方法可以彻底取消这个事件,使得后面绑定的所有click监听函数都不再触发。所以,只会输出1,不会输出2。三、Event 对象1. 概述事件发生以后,会产生一个事件对象,作为参数传给监听函数。浏览器原生提供一个Event对象,所有的事件都是这个对象的实例,或者说继承了Event.prototype对象。Event对象本身就是一个构造函数,可以用来生成新的实例。event = new Event(type, options);// 参数一,事件名称; 参数二,事件对象的配置对象Event构造函数接受两个参数。第一个参数type是字符串,表示事件的名称;第二个参数options是一个对象,表示事件对象的配置。该对象主要有下面两个属性。bubbles:布尔值,可选,默认为false,表示事件对象是否冒泡。cancelable:布尔值,可选,默认为false,表示事件是否可以被取消,即能否用Event.preventDefault()取消这个事件。一旦事件被取消,就好像从来没有发生过,不会触发浏览器对该事件的默认行为。var ev = new Event( // 创建一个新的事件实例
'look', // 事件名称
{ // 事件配置
'bubbles': true, // 事件是否冒泡
'cancelable': false // 事件是否可以被取消
}
);
document.dispatchEvent(ev); // 触发ev实例,该实例是look事件上面代码新建一个look事件实例,然后使用dispatchEvent方法触发该事件。注意,如果不是显式指定bubbles属性为true,生成的事件就只能在“捕获阶段”触发监听函数。// HTML 代码为
// <div><p>Hello</p></div>
var div = document.querySelector('div');
var p = document.querySelector('p');
function callback(event) {
var tag = event.currentTarget.tagName;
console.log('Tag: ' + tag); // 没有任何输出
}
div.addEventListener('click', callback, false); // 该事件是在冒泡阶段监听函数
var click = new Event('click');
p.dispatchEvent(click); // 给p发出一个click事件,该事件默认不会冒泡,因此不会触发父元素div的click事件上面代码中,p元素发出一个click事件,该事件默认不会冒泡。div.addEventListener方法指定在冒泡阶段监听,因此监听函数不会触发。如果写成div.addEventListener('click', callback, true),那么在“捕获阶段”可以监听到这个事件。另一方面,如果这个事件在div元素上触发。div.dispatchEvent(click);那么,不管div元素是在冒泡阶段监听,还是在捕获阶段监听,都会触发监听函数。因为这时div元素是事件的目标,不存在是否冒泡的问题,div元素总是会接收到事件,因此导致监听函数生效。2. 实例属性2.1 Event.bubbles 是否会冒泡,只读,Event.eventPhase 返回整数,表示事件所处阶段,只读Event.bubbles属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性,一般用来了解 Event 实例是否可以冒泡。前面说过,除非显式声明,Event构造函数生成的事件,默认是不冒泡的。Event.eventPhase属性返回一个整数常量,表示事件目前所处的阶段。该属性只读。var phase = event.eventPhase;Event.eventPhase的返回值有四种可能。0,事件目前没有发生。1,事件目前处于捕获阶段,即处于从祖先节点向目标节点的传播过程中。2,事件到达目标节点,即Event.target属性指向的那个节点。3,事件处于冒泡阶段,即处于从目标节点向祖先节点的反向传播过程中。2.2 Event.cancelable 是否可取消默认行为,Event.cancelBubble是否阻止冒泡,event.defaultPrevented 是否调用过取消默认行为方法Event.cancelable属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性,一般用来了解 Event 实例的特性。大多数浏览器的原生事件是可以取消的。比如,取消click事件,点击链接将无效。但是除非显式声明,Event构造函数生成的事件,默认是不可以取消的。var evt = new Event('foo');
evt.cancelable // false 默认不可取消当Event.cancelable属性为true时,调用Event.preventDefault()就可以取消这个事件,阻止浏览器对该事件的默认行为。如果事件不能取消,调用Event.preventDefault()会没有任何效果。所以使用这个方法之前,最好用Event.cancelable属性判断一下是否可以取消。function preventEvent(event) {
if (event.cancelable) {
event.preventDefault();
} else {
console.warn('This event couldn\'t be canceled.');
console.dir(event);
}
}Event.cancelBubble属性是一个布尔值,如果设为true,相当于执行Event.stopPropagation(),可以阻止事件的传播。Event.defaultPrevented属性返回一个布尔值,表示该事件是否调用过Event.preventDefault方法。该属性只读。if (event.defaultPrevented) {
console.log('该事件已经取消了');
}2.3 Event.currentTarget当前正在通过的节点,Event.target目标节点事件发生以后,会经过捕获和冒泡两个阶段,依次通过多个 DOM 节点。因此,任意时点都有两个与事件相关的节点,一个是事件的原始触发节点(Event.target),另一个是事件当前正在通过的节点(Event.currentTarget)。前者通常是后者的后代节点。Event.currentTarget属性返回事件当前所在的节点,即事件当前正在通过的节点,也就是当前正在执行的监听函数所在的那个节点。随着事件的传播,这个属性的值会变。Event.target属性返回原始触发事件的那个节点,即事件最初发生的节点。这个属性不会随着事件的传播而改变。事件传播过程中,不同节点的监听函数内部的Event.target与Event.currentTarget属性的值是不一样的。// HTML 代码为
// <p id="para">Hello <em>World</em></p>
function hide(e) {
// 不管点击 Hello 或 World,总是返回 true
console.log(this === e.currentTarget);
// 点击 Hello,返回 true
// 点击 World,返回 false
console.log(this === e.target);
}
document.getElementById('para').addEventListener('click', hide, false);上面代码中, <em> 是 <p> 的子节点,点击 <em> 或者点击 <p> ,都会导致监听函数执行。这时,e.target总是指向原始点击位置的那个节点,而e.currentTarget指向事件传播过程中正在经过的那个节点。由于监听函数只有事件经过时才会触发,所以e.currentTarget总是等同于监听函数内部的this。2.4 Event.type 事件类型(如:'click')Event.type属性返回一个字符串,表示事件类型。事件的类型是在生成事件的时候指定的。该属性只读。var evt = new Event('foo');
evt.type // "foo"2.5 Event.timeStamp 相对于打开网页后的毫秒时间戳Event.timeStamp属性返回一个毫秒时间戳,表示事件发生的时间。它是相对于网页加载成功开始计算的。var evt = new Event('foo');
evt.timeStamp // 3683.6999999995896它的返回值有可能是整数,也有可能是小数(高精度时间戳),取决于浏览器的设置。下面是一个计算鼠标移动速度的例子,显示每秒移动的像素数量。例子:计算鼠标移动速度var previousX;
var previousY;
var previousT;
window.addEventListener('mousemove', function(event) {
if (
previousX !== undefined &&
previousY !== undefined &&
previousT !== undefined
) {
var deltaX = event.screenX - previousX;
var deltaY = event.screenY - previousY;
var deltaD = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2));
var deltaT = event.timeStamp - previousT;
console.log(deltaD / deltaT * 1000);
}
previousX = event.screenX;
previousY = event.screenY;
previousT = event.timeStamp;
});2.6 Event.isTrusted 是否由真实用户行为产生的事件Event.isTrusted属性返回一个布尔值,表示该事件是否由真实的用户行为产生。比如,用户点击链接会产生一个click事件,该事件是用户产生的;Event构造函数生成的事件,则是脚本产生的。var evt = new Event('foo');
evt.isTrusted // false上面代码中,evt对象是脚本产生的,所以isTrusted属性返回false。2.7 Event.detail 事件的细节(单击or双击等)Event.detail属性只有浏览器的 UI (用户界面)事件才具有。该属性返回一个数值,表示事件的某种信息。具体含义与事件类型相关。比如,对于click和dblclick事件,Event.detail是鼠标按下的次数(1表示单击,2表示双击,3表示三击);对于鼠标滚轮事件,Event.detail是滚轮正向滚动的距离,负值就是负向滚动的距离,返回值总是3的倍数。// HTML 代码如下
// <p>Hello</p>
function giveDetails(e) {
console.log(e.detail); // 快速点击的次数
}
document.querySelector('p').onclick = giveDetails;3、实例方法3.1 Event.preventDefault() 取消浏览器对当前事件的默认行为Event.preventDefault方法取消浏览器对当前事件的默认行为。比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。注意,该方法只是取消事件对当前元素的默认影响,不会阻止事件的传播。如果要阻止传播,可以使用stopPropagation()或stopImmediatePropagation()方法。// HTML 代码为
// <input type="checkbox" id="my-checkbox" />
var cb = document.getElementById('my-checkbox');
cb.addEventListener(
'click',
function (e){ e.preventDefault(); },
false
);上面代码中,浏览器的默认行为是单击会选中单选框,取消这个行为,就导致无法选中单选框。利用这个方法,可以为文本输入框设置校验条件。如果用户的输入不符合条件,就无法将字符输入文本框。例子:只能输入字母的输入框// HTML 代码为
// <input type="text" id="my-input" />
var input = document.getElementById('my-input');
input.addEventListener('keypress', checkName, false);
function checkName(e) {
if (e.charCode < 97 || e.charCode > 122) {
e.preventDefault();
}
}上面代码为文本框的keypress事件设定监听函数后,将只能输入小写字母,否则输入事件的默认行为(写入文本框)将被取消,导致不能向文本框输入内容。3.2 Event.stopPropagation() 阻止事件传播stopPropagation方法阻止事件在 DOM 中继续传播,防止再触发定义在别的节点上的监听函数,但是不包括在当前节点上其他的事件监听函数。function stopEvent(e) {
e.stopPropagation(); // 阻止事件冒泡
}
el.addEventListener('click', stopEvent, false);上面代码中,click事件将不会进一步冒泡到el节点的父节点。3.3 Event.stopImmediatePropagation() 阻止同一个事件的其他监听函数被调用Event.stopImmediatePropagation方法阻止同一个事件的其他监听函数被调用,不管监听函数定义在当前节点还是其他节点。也就是说,该方法阻止事件的传播,比Event.stopPropagation()更彻底。如果同一个节点对于同一个事件指定了多个监听函数,这些函数会根据添加的顺序依次调用。只要其中有一个监听函数调用了Event.stopImmediatePropagation方法,其他的监听函数就不会再执行了。function l1(e){
e.stopImmediatePropagation();
}
function l2(e){ // 不会被调用
console.log('hello world');
}
el.addEventListener('click', l1, false);
el.addEventListener('click', l2, false);上面代码在el节点上,为click事件添加了两个监听函数l1和l2。由于l1调用了event.stopImmediatePropagation方法,所以l2不会被调用。3.4 Event.composedPath() 数组,目标和冒泡的节点Event.composedPath()返回一个数组,成员是事件的最底层节点和依次冒泡经过的所有上层节点。// HTML 代码如下
// <div>
// <p>Hello</p>
// </div>
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', function (e) {
console.log(e.composedPath());
}, false);
// [p, div, body, html, document, Window]上面代码中,click事件的最底层节点是p,向上依次是div、body、html、document、Window。四、鼠标事件1、鼠标事件的种类鼠标事件指与鼠标相关的事件,继承了MouseEvent接口。具体的事件主要有以下一些。click:按下鼠标(通常是按下主按钮)时触发。【单击】dblclick:在同一个元素上双击鼠标时触发。【双击】mousedown:按下鼠标键时触发。【按下】mouseup:释放按下的鼠标键时触发。【抬起】mousemove:当鼠标在一个节点内部移动时触发。当鼠标持续移动时,该事件会连续触发。为了避免性能问题,建议对该事件的监听函数做一些限定,比如限定一段时间内只能运行一次。【经过(多次)】mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件(详见后文)。【进入(单次)】mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件(详见后文)。【进入+子节点】mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件(详见后文)。【离开(单次)】mouseout:鼠标离开一个节点时触发,离开父节点会触发这个事件(详见后文)。【离开+子节点】contextmenu:按下鼠标右键时(上下文菜单出现前)触发,或者按下“上下文菜单键”时触发。【右键】wheel:滚动鼠标的滚轮时触发,该事件继承的是WheelEvent接口。【滚轮】click事件指的是,用户在同一个位置先完成mousedown动作,再完成mouseup动作。因此,触发顺序是,mousedown首先触发,mouseup接着触发,click最后触发。dblclick事件则会在mousedown、mouseup、click之后触发。mouseover事件和mouseenter事件,都是鼠标进入一个节点时触发。两者的区别是,mouseenter事件只触发一次,而只要鼠标在节点内部移动,mouseover事件会在子节点上触发多次。/* HTML 代码如下
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
*/
var ul = document.querySelector('ul');
// 进入 ul 节点以后,mouseenter 事件只会触发一次
// 以后只要鼠标在节点内移动,都不会再触发这个事件
// event.target 是 ul 节点
ul.addEventListener('mouseenter', function (event) {
event.target.style.color = 'purple';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);
// 进入 ul 节点以后,只要在子节点上移动,mouseover 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseover', function (event) {
event.target.style.color = 'orange';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);上面代码中,在父节点内部进入子节点,不会触发mouseenter事件,但是会触发mouseover事件。mouseout事件和mouseleave事件,都是鼠标离开一个节点时触发。两者的区别是,在父元素内部离开一个子元素时,mouseleave事件不会触发,而mouseout事件会触发。/* HTML 代码如下
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
*/
var ul = document.querySelector('ul');
// 先进入 ul 节点,然后在节点内部移动,不会触发 mouseleave 事件
// 只有离开 ul 节点时,触发一次 mouseleave
// event.target 是 ul 节点
ul.addEventListener('mouseleave', function (event) {
event.target.style.color = 'purple';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);
// 先进入 ul 节点,然后在节点内部移动,mouseout 事件会触发多次
// event.target 是 li 节点
ul.addEventListener('mouseout', function (event) {
event.target.style.color = 'orange';
setTimeout(function () {
event.target.style.color = '';
}, 500);
}, false);上面代码中,在父节点内部离开子节点,不会触发mouseleave事件,但是会触发mouseout事件。2、MouseEvent 接口概述MouseEvent接口代表所有鼠标事件所产生的对象都是MouseEvent实例。此外,滚轮事件和拖拉事件也是MouseEvent实例。MouseEvent接口继承了Event接口,所以拥有Event的所有属性和方法。它还有自己的属性和方法。浏览器原生提供一个MouseEvent构造函数,用于新建一个MouseEvent实例。var event = new MouseEvent(type, options);// 参数一,事件名称字符串;参数二,事件配置对象MouseEvent构造函数接受两个参数。第一个参数是字符串,表示事件名称;第二个参数是一个事件配置对象,该参数可选。除了Event接口的实例配置属性,该对象可以配置以下属性,所有属性都是可选的。screenX:数值,鼠标相对于屏幕的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。screenY:数值,鼠标相对于屏幕的垂直位置(单位像素),其他与screenX相同。clientX:数值,鼠标相对于程序窗口的水平位置(单位像素),默认值为0,设置该属性不会移动鼠标。clientY:数值,鼠标相对于程序窗口的垂直位置(单位像素),其他与clientX相同。ctrlKey:布尔值,是否同时按下了 Ctrl 键,默认值为false。shiftKey:布尔值,是否同时按下了 Shift 键,默认值为false。altKey:布尔值,是否同时按下 Alt 键,默认值为false。metaKey:布尔值,是否同时按下 Meta 键(win键),默认值为false。button:数值,表示按下了哪一个鼠标按键,默认值为0,表示按下主键(通常是鼠标的左键)或者当前事件没有定义这个属性;1表示按下辅助键(通常是鼠标的中间键),2表示按下次要键(通常是鼠标的右键)。buttons:数值,表示按下了鼠标的哪些键,是一个三个比特位的二进制值,默认为0(没有按下任何键)。1(二进制001)表示按下主键(通常是左键),2(二进制010)表示按下次要键(通常是右键),4(二进制100)表示按下辅助键(通常是中间键)。因此,如果返回3(二进制011)就表示同时按下了左键和右键。relatedTarget:节点对象,表示事件的相关节点,默认为null。mouseenter和mouseover事件时,表示鼠标刚刚离开的那个元素节点;mouseout和mouseleave事件时,表示鼠标正在进入的那个元素节点。下面是一个例子。var event = new MouseEvent('click2', {
'bubbles': true,
'cancelable': true
});
var cb = document.getElementById('checkbox');
cb.addEventListener('click2',function(){ // 绑定事件监听函数
console.log(22) // 被执行
})
cb.dispatchEvent(event);// 触发事件上面代码生成一个鼠标点击事件,并触发该事件。3、MouseEvent 接口的实例属性3.1 MouseEvent.altKey,MouseEvent.ctrlKey,MouseEvent.metaKey,MouseEvent.shiftKeyMouseEvent.altKey、MouseEvent.ctrlKey、MouseEvent.metaKey、MouseEvent.shiftKey这四个属性都返回一个布尔值,表示事件发生时,是否按下对应的键。它们都是只读属性。altKey属性:Alt 键ctrlKey属性:Ctrl 键metaKey属性:Meta 键(Mac 键盘是一个四瓣的小花,Windows 键盘是 Windows 键)shiftKey属性:Shift 键// HTML 代码如下
// <body onclick="showKey(event)">
function showKey(e) {
console.log('ALT key pressed: ' + e.altKey);
console.log('CTRL key pressed: ' + e.ctrlKey);
console.log('META key pressed: ' + e.metaKey);
console.log('SHIFT key pressed: ' + e.shiftKey);
}上面代码中,点击网页会输出是否同时按下对应的键。3.2 MouseEvent.button 鼠标的哪个键,MouseEvent.buttons同时按哪些键MouseEvent.button属性返回一个数值,表示事件发生时按下了鼠标的哪个键。该属性只读。0:按下主键(通常是左键),或者该事件没有初始化这个属性(比如mousemove事件)。1:按下辅助键(通常是中键或者滚轮键)。2:按下次键(通常是右键)。// HTML 代码为
// <button onmouseup="whichButton(event)">点击</button>
var whichButton = function (e) {
switch (e.button) {
case 0:
console.log('Left button clicked.');
break;
case 1:
console.log('Middle button clicked.');
break;
case 2:
console.log('Right button clicked.');
break;
default:
console.log('Unexpected code: ' + e.button);
}
}MouseEvent.buttons属性返回一个三个比特位的值,表示同时按下了哪些键。它用来处理同时按下多个鼠标键的情况。该属性只读。1:二进制为001(十进制的1),表示按下左键。2:二进制为010(十进制的2),表示按下右键。4:二进制为100(十进制的4),表示按下中键或滚轮键。同时按下多个键的时候,每个按下的键对应的比特位都会有值。比如,同时按下左键和右键,会返回3(二进制为011)。document.body.addEventListener('mousemove',function(e){ // 注意,用click时一直都是0
console.log(e.buttons)
})
// 未按下任何键时是 0
// 按下左键 1 (001)
// 按下右键 2 (010)
// 按下中键 4 (100)
// 按下左键和右键 3 (011)
// 按下左键和中键 5 (101)
// 按下右键和中键 6 (110)
// 按下左、中、右键 7 (111)3.3 MouseEvent.clientX 相对浏览器X坐标,MouseEvent.clientY 相对浏览器Y坐标MouseEvent.clientX属性返回鼠标位置相对于浏览器窗口左上角的水平坐标(单位像素),MouseEvent.clientY属性返回垂直坐标。这两个属性都是只读属性。// HTML 代码为
// <body onmousedown="showCoords(event)">
function showCoords(evt){
console.log(
'clientX value: ' + evt.clientX + '\n' +
'clientY value: ' + evt.clientY + '\n'
);
}这两个属性还分别有一个别名MouseEvent.x和MouseEvent.y。3.4 MouseEvent.movementX 上一个鼠标经过事件的X距离,MouseEvent.movementY 上一个鼠标经过事件的Y距离MouseEvent.movementX属性返回当前位置与上一个mousemove事件之间的水平距离(单位像素)。数值上,它等于下面的计算公式。currentEvent.movementX = currentEvent.screenX - previousEvent.screenXMouseEvent.movementY属性返回当前位置与上一个mousemove事件之间的垂直距离(单位像素)。数值上,它等于下面的计算公式。currentEvent.movementY = currentEvent.screenY - previousEvent.screenY。这两个属性都是只读属性。3.5 MouseEvent.screenX 相对屏幕X坐标,MouseEvent.screenY 相对屏幕Y坐标MouseEvent.screenX属性返回鼠标位置相对于屏幕左上角的水平坐标(单位像素),MouseEvent.screenY属性返回垂直坐标。这两个属性都是只读属性。// HTML 代码如下
// <body onmousedown="showCoords(event)">
function showCoords(evt) {
console.log(
'screenX value: ' + evt.screenX + '\n',
'screenY value: ' + evt.screenY + '\n'
);
}3.6 MouseEvent.offsetX 偏移量X,MouseEvent.offsetY 偏移量YMouseEvent.offsetX属性返回鼠标位置与目标节点左侧的padding边缘的水平距离(单位像素),MouseEvent.offsetY属性返回与目标节点上方的padding边缘的垂直距离。这两个属性都是只读属性。/* HTML 代码如下
<style>
p {
width: 100px;
height: 100px;
padding: 100px;
}
</style>
<p>Hello</p>
*/
var p = document.querySelector('p');
p.addEventListener(
'click',
function (e) {
console.log(e.offsetX); // 包含padding
console.log(e.offsetY);
},
false
);上面代码中,鼠标如果在p元素的中心位置点击,会返回150 150。因此中心位置距离左侧和上方的padding边缘,等于padding的宽度(100像素)加上元素内容区域一半的宽度(50像素)。3.7 MouseEvent.pageX 文档X坐标,MouseEvent.pageY 文档Y坐标MouseEvent.pageX属性返回鼠标位置与文档左侧边缘的距离(单位像素),MouseEvent.pageY属性返回与文档上侧边缘的距离(单位像素)。它们的返回值都包括文档不可见的部分。这两个属性都是只读。/* HTML 代码如下
<style>
body {
height: 2000px;
}
</style>
*/
document.body.addEventListener(
'click',
function (e) {
console.log(e.pageX);
console.log(e.pageY);
},
false
);上面代码中,页面高度为2000像素,会产生垂直滚动条。滚动到页面底部,点击鼠标输出的pageY值会接近2000。3.8 MouseEvent.relatedTarget 事件的相关节点MouseEvent.relatedTarget属性返回事件的相关节点。对于那些没有相关节点的事件,该属性返回null。该属性只读。下表列出不同事件的target属性值和relatedTarget属性值义。事件名称target 属性relatedTarget 属性focusin接受焦点的节点丧失焦点的节点focusout丧失焦点的节点接受焦点的节点mouseenter将要进入的节点将要离开的节点mouseleave将要离开的节点将要进入的节点mouseout将要离开的节点将要进入的节点mouseover将要进入的节点将要离开的节点dragenter将要进入的节点将要离开的节点dragexit将要离开的节点将要进入的节点下面是一个例子。/*
HTML 代码如下
<div id="outer" style="height:50px;width:50px;border-width:1px solid black;">
<div id="inner" style="height:25px;width:25px;border:1px solid black;"></div>
</div>
*/
var inner = document.getElementById('inner');
inner.addEventListener('mouseover', function (event) {
console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
}, false);
inner.addEventListener('mouseenter', function (event) {
console.log('进入' + event.target.id + ' 离开' + event.relatedTarget.id);
});
inner.addEventListener('mouseout', function () {
console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});
inner.addEventListener("mouseleave", function (){
console.log('离开' + event.target.id + ' 进入' + event.relatedTarget.id);
});
// 鼠标从 outer 进入inner,输出
// 进入inner 离开outer
// 进入inner 离开outer
// 鼠标从 inner进入 outer,输出
// 离开inner 进入outer
// 离开inner 进入outer4、MouseEvent 接口的实例方法4.1 MouseEvent.getModifierState() 是否按下指定功能键MouseEvent.getModifierState方法返回一个布尔值,表示有没有按下特定的功能键。它的参数是一个表示功能键的字符串。document.addEventListener('click', function (e) {
console.log(e.getModifierState('CapsLock'));
}, false);上面的代码可以了解用户是否按下了大写键。5、WheelEvent 接口 (滚轮)5.1 概述WheelEvent 接口继承了 MouseEvent 实例,代表鼠标滚轮事件的实例对象。目前,鼠标滚轮相关的事件只有一个wheel事件,用户滚动鼠标的滚轮,就生成这个事件的实例。浏览器原生提供WheelEvent()构造函数,用来生成WheelEvent实例。var wheelEvent = new WheelEvent(type, options);WheelEvent()构造函数可以接受两个参数,第一个是字符串,表示事件类型,对于滚轮事件来说,这个值目前只能是wheel。第二个参数是事件的配置对象。该对象的属性除了Event、UIEvent的配置属性以外,还可以接受以下几个属性,所有属性都是可选的。deltaX:数值,表示滚轮的水平滚动量,默认值是 0.0。deltaY:数值,表示滚轮的垂直滚动量,默认值是 0.0。deltaZ:数值,表示滚轮的 Z 轴滚动量,默认值是 0.0。deltaMode:数值,表示相关的滚动事件的单位,适用于上面三个属性。0表示滚动单位为像素,1表示单位为行,2表示单位为页,默认为0。5.2 实例属性WheelEvent事件实例除了具有Event和MouseEvent的实例属性和实例方法,还有一些自己的实例属性,但是没有自己的实例方法。下面的属性都是只读属性。WheelEvent.deltaX:数值,表示滚轮的水平滚动量。WheelEvent.deltaY:数值,表示滚轮的垂直滚动量。WheelEvent.deltaZ:数值,表示滚轮的 Z 轴滚动量。WheelEvent.deltaMode:数值,表示上面三个属性的单位,0是像素,1是行,2是页。五、键盘事件1、键盘事件的种类键盘事件由用户击打键盘触发,主要有keydown、keypress、keyup三个事件,它们都继承了KeyboardEvent接口。keydown:按下键盘时触发。【按下】keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个事件。【按下有值的键】keyup:松开键盘时触发该事件。【松开】如果用户一直按键不松开,就会连续触发键盘事件,触发的顺序如下。keydownkeypresskeydownkeypress...(重复以上过程)keyup2、KeyboardEvent 接口概述KeyboardEvent接口用来描述用户与键盘的互动。这个接口继承了Event接口,并且定义了自己的实例属性和实例方法。浏览器原生提供KeyboardEvent构造函数,用来新建键盘事件的实例。new KeyboardEvent(type, options) // 参数一,事件类型;参数二,事件配置对象KeyboardEvent构造函数接受两个参数。第一个参数是字符串,表示事件类型;第二个参数是一个事件配置对象,该参数可选。除了Event接口提供的属性,还可以配置以下字段,它们都是可选。key:字符串,当前按下的键,默认为空字符串。【键名】code:字符串,表示当前按下的键的字符串形式,默认为空字符串。【键码】location:整数,当前按下的键的位置,默认为0。ctrlKey:布尔值,是否按下 Ctrl 键,默认为false。shiftKey:布尔值,是否按下 Shift 键,默认为false。altKey:布尔值,是否按下 Alt 键,默认为false。metaKey:布尔值,是否按下 Meta 键,默认为false。repeat:布尔值,是否重复按键,默认为false。3、KeyboardEvent 的实例属性3.1 KeyboardEvent.altKey,KeyboardEvent.ctrlKey,KeyboardEvent.metaKey,KeyboardEvent.shiftKey 【是否按下对应键,布尔值】以下属性都是只读属性,返回一个布尔值,表示是否按下对应的键。KeyboardEvent.altKey:是否按下 Alt 键KeyboardEvent.ctrlKey:是否按下 Ctrl 键KeyboardEvent.metaKey:是否按下 meta 键(Mac 系统是一个四瓣的小花,Windows 系统是 windows 键)KeyboardEvent.shiftKey:是否按下 Shift 键下面是一个示例。function showChar(e) {
console.log('ALT: ' + e.altKey);
console.log('CTRL: ' + e.ctrlKey);
console.log('Meta: ' + e.metaKey);
console.log('Shift: ' + e.shiftKey);
}
document.body.addEventListener('keydown', showChar, false);3.2 KeyboardEvent.code 键码KeyboardEvent.code属性返回一个字符串,表示当前按下的键的字符串形式。该属性只读。下面是一些常用键的字符串形式,其他键请查文档。数字键0 - 9:返回digital0 - digital9字母键A - z:返回KeyA - KeyZ功能键F1 - F12:返回 F1 - F12方向键:返回ArrowDown、ArrowUp、ArrowLeft、ArrowRightAlt 键:返回AltLeft或AltRightShift 键:返回ShiftLeft或ShiftRightCtrl 键:返回ControlLeft或ControlRight3.3 KeyboardEvent.key 键名KeyboardEvent.key属性返回一个字符串,表示按下的键名。该属性只读。如果按下的键代表可打印字符,则返回这个字符,比如数字、字母。如果按下的键代表不可打印的特殊字符,则返回预定义的键值,比如 Backspace,Tab,Enter,Shift,Control,Alt,CapsLock,Esc,Spacebar,PageUp,PageDown,End,Home,Left,Right,Up,Down,PrintScreen,Insert,Del,Win,F1~F12,NumLock,Scroll 等。如果同时按下一个控制键和一个符号键,则返回符号键的键名。比如,按下 Ctrl + a,则返回a;按下 Shift + a,则返回大写的A。如果无法识别键名,返回字符串Unidentified。3.4 KeyboardEvent.location 键处于哪个位置,整数KeyboardEvent.location属性返回一个整数,表示按下的键处在键盘的哪一个区域。它可能取以下值。0:处在键盘的主区域,或者无法判断处于哪一个区域。1:处在键盘的左侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。2:处在键盘的右侧,只适用那些有两个位置的键(比如 Ctrl 和 Shift 键)。3:处在数字小键盘。3.5 KeyboardEvent.repeat 是否长按KeyboardEvent.repeat返回一个布尔值,代表该键是否被按着不放,以便判断是否重复这个键,即浏览器会持续触发keydown和keypress事件,直到用户松开手为止。4、KeyboardEvent 的实例方法4.1 KeyboardEvent.getModifierState() 是否按下指定功能键KeyboardEvent.getModifierState()方法返回一个布尔值,表示是否按下或激活指定的功能键。它的常用参数如下。Alt:Alt 键CapsLock:大写锁定键Control:Ctrl 键Meta:Meta 键NumLock:数字键盘开关键Shift:Shift 键if (
event.getModifierState('Control') +
event.getModifierState('Alt') +
event.getModifierState('Meta') > 1
) {
return;
}上面代码表示,只要Control、Alt、Meta里面,同时按下任意两个或两个以上的键就返回。六、进度事件1、进度事件的种类进度事件用来描述资源加载的进度,主要由 AJAX 请求、 <img> 、 <audio> 、 <video> 、 <style> 、 <link> 等外部资源的加载触发,继承了ProgressEvent接口。它主要包含以下几种事件。abort:外部资源中止加载时(比如用户取消)触发。如果发生错误导致中止,不会触发该事件。【中止加载】error:由于错误导致外部资源无法加载时触发。【加载错误】load:外部资源加载成功时触发。【加载成功】loadstart:外部资源开始加载时触发。【开始加载】loadend:外部资源停止加载时触发,发生顺序排在error、abort、load等事件的后面。【停止加载】progress:外部资源加载过程中不断触发。【加载中,不断触发】timeout:加载超时时触发。【加载超时】注意,除了资源下载,文件上传也存在这些事件。下面是一个例子。image.addEventListener('load', function (event) { // 加载成功
image.classList.add('finished');
});
image.addEventListener('error', function (event) { // 加载出错
image.style.display = 'none';
});上面代码在图片元素加载完成后,为图片元素添加一个finished的 Class。如果加载失败,就把图片元素的样式设置为不显示。有时候,图片加载会在脚本运行之前就完成,尤其是当脚本放置在网页底部的时候,因此有可能load和error事件的监听函数根本不会执行。所以,比较可靠的方式,是用complete属性先判断一下是否加载完成。function loaded() {
// ...
}
if (image.complete) { // 是否加载完成
loaded();
} else {
image.addEventListener('load', loaded); // 加载成功事件
}由于 DOM 的元素节点没有提供是否加载错误的属性,所以error事件的监听函数最好放在 <img> 元素的 HTML 代码中,这样才能保证发生加载错误时百分之百会执行。<img src="/wrong/url" onerror="this.style.display='none';" />loadend事件的监听函数,可以用来取代abort事件、load事件、error事件的监听函数,因为它总是在这些事件之后发生。req.addEventListener('loadend', loadEnd, false);
function loadEnd(e) {
console.log('传输结束,成功失败未知');
}loadend事件本身不提供关于进度结束的原因,但可以用它来做所有加载结束场景都需要做的一些操作。另外,error事件有一个特殊的性质,就是不会冒泡。所以,子元素的error事件,不会触发父元素的error事件监听函数。2、ProgressEvent 接口2.1 概述ProgressEvent接口主要用来描述外部资源加载的进度,比如 AJAX 加载、 <img> 、 <video> 、 <style> 、 <link> 等外部资源加载。进度相关的事件都继承了这个接口。这个接口继承了Event接口。浏览器原生提供了ProgressEvent()构造函数,用来生成事件实例。new ProgressEvent(type, options) // 参数一,事件类型;参数二,配置对象ProgressEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的类型,这个参数是必须的。第二个参数是一个配置对象,表示事件的属性,该参数可选。配置对象除了可以使用Event接口的配置属性,还可以使用下面的属性,所有这些属性都是可选的。lengthComputable:布尔值,表示加载的总量是否可以计算,默认是false。loaded:整数,表示已经加载的量,默认是0。total:整数,表示需要加载的总量,默认是0。2.2 ProgressEvent的实例属性。ProgressEvent.lengthComputable 总量是否可以计算ProgressEvent.loaded 已加载的量ProgressEvent.total 需要加载的总量如果ProgressEvent.lengthComputable为false,ProgressEvent.total实际上是没有意义的。下面是一个例子。var p = new ProgressEvent('load', {
lengthComputable: true,
loaded: 30,
total: 100,
});
document.body.addEventListener('load', function (e) {
console.log('已经加载:' + (e.loaded / e.total) * 100 + '%');
});
document.body.dispatchEvent(p);
// 已经加载:30%上面代码先构造一个load事件,抛出后被监听函数捕捉到。下面是一个实际的例子。var xhr = new XMLHttpRequest();
xhr.addEventListener('progress', updateProgress, false); // 加载中
xhr.addEventListener('load', transferComplete, false); // 加载成功
xhr.addEventListener('error', transferFailed, false); // 加载错误
xhr.addEventListener('abort', transferCanceled, false); // 中止加载
xhr.open();
function updateProgress(e) { // 加载中
if (e.lengthComputable) { // 是否可以计算总量
var percentComplete = e.loaded / e.total; // 加载进度计算
} else {
console.log('不能计算进度');
}
}
function transferComplete(e) { // 加载成功
console.log('传输结束');
}
function transferFailed(evt) { // 加载错误
console.log('传输过程中发生错误');
}
function transferCanceled(evt) { // 中止加载
console.log('用户取消了传输');
}上面是下载过程的进度事件,还存在上传过程的进度事件。这时所有监听函数都要放在XMLHttpRequest.upload对象上面。var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', updateProgress, false);
xhr.upload.addEventListener('load', transferComplete, false);
xhr.upload.addEventListener('error', transferFailed, false);
xhr.upload.addEventListener('abort', transferCanceled, false);
xhr.open();七、表单事件1、表单事件的种类1.1 input 事件 (值发生变化触发,会连续)input事件当 <input> 、 <select> 、 <textarea> 的值发生变化时触发。对于复选框( <input type=checkbox> )或单选框( <input type=radio> ),用户改变选项时,也会触发这个事件。另外,对于打开contenteditable属性的元素,只要值发生变化,也会触发input事件。input事件的一个特点,就是会连续触发,比如用户每按下一次按键,就会触发一次input事件。input事件对象继承了InputEvent接口。该事件跟change事件很像,不同之处在于input事件在元素的值发生变化后立即发生,而change在元素失去焦点时发生,而内容此时可能已经变化多次。也就是说,如果有连续变化,input事件会触发多次,而change事件只在失去焦点时触发一次。下面是 <select> 元素的例子。/* HTML 代码如下
<select id="mySelect">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
*/
function inputHandler(e) {
console.log(e.target.value)
}
var mySelect = document.querySelector('#mySelect');
mySelect.addEventListener('input', inputHandler);上面代码中,改变下拉框选项时,会触发input事件,从而执行回调函数inputHandler。1.2 select 事件 (选中文本时触发)select事件当在 <input> 、 <textarea> 里面选中文本时触发。// HTML 代码如下
// <input id="test" type="text" value="Select me!" />
var elem = document.getElementById('test');
elem.addEventListener('select', function (e) {
console.log(e.type); // "select" 事件类型
var _target = e.target;
console.log(_target.value); // 文本框的全部值
console.log(_target.selectionDirection); // 选择的方向:'forward'正向、'backward'反向
console.log(_target.selectionStart); // 开始选择的索引
console.log(_target.selectionEnd); // 结束选择的索引
// 注意:开始和结束索引是不分选择方向的,开始的索引一直是靠前的
console.log(_target.value.slice(_target.selectionStart, _target.selectionEnd)) // 选中的那部分字符串
}, false);选中的文本可以通过event.target元素的selectionDirection、selectionEnd、selectionStart和value属性拿到。1.3 change 事件 (值发生变化时触发,单次)change事件当 <input> 、 <select> 、 <textarea> 的值发生变化时触发。它与input事件的最大不同,就是不会连续触发,只有当全部修改完成时才会触发,另一方面input事件必然伴随change事件。具体来说,分成以下几种情况。激活单选框(radio)或复选框(checkbox)时触发。用户提交时触发。比如,从下列列表(select)完成选择,在日期或文件输入框完成选择。当文本框或 <textarea> 元素的值发生改变,并且丧失焦点时触发。下面是一个例子。// HTML 代码如下
// <select size="1" onchange="changeEventHandler(event);">
// <option>chocolate</option>
// <option>strawberry</option>
// <option>vanilla</option>
// </select>
function changeEventHandler(event) {
console.log(event.target.value);
}如果比较一下上面input事件的例子,你会发现对于 <select> 元素来说,input和change事件基本是等价的。1.4 invalid 事件 (表单提交不满足条件触发)用户提交表单时,如果表单元素的值不满足校验条件,就会触发invalid事件。<form>
<input type="text" required oninvalid="console.log('invalid input')" />
<button type="submit">提交</button>
</form>上面代码中,输入框是必填的。如果不填,用户点击按钮提交时,就会触发输入框的invalid事件,导致提交被取消。1.5 reset 事件(重置),submit 事件(提交)reset事件当表单重置(所有表单成员变回默认值)时触发。submit事件当表单数据向服务器提交时触发。注意,这两个事件发生在表单对象 <form> 上,而不是发生在表单的成员上,因为提交的是表单,而不是表单成员。<form onreset="console.log('触发了重置事件')" onsubmit="console.log('触发了提交事件')" name="input" action="html_form_action.php" method="get">
<input type="text" name="lname" value="Duck">
<button type="reset">重置</button>
<button type="submit">提交</button>
</form>2、InputEvent 接口(input事件的实例)InputEvent接口主要用来描述input事件的实例。该接口继承了Event接口,还定义了一些自己的实例属性和实例方法。浏览器原生提供InputEvent()构造函数,用来生成实例对象。new InputEvent(type, options) // 参数一,事件名称;参数二,配置对象InputEvent构造函数可以接受两个参数。第一个参数是字符串,表示事件名称,该参数是必需的。第二个参数是一个配置对象,用来设置事件实例的属性,该参数是可选的。配置对象的字段除了Event构造函数的配置属性,还可以设置下面的字段,这些字段都是可选的。inputType:字符串,表示发生变更的类型(详见下文)。data:字符串,表示插入的字符串。如果没有插入的字符串(比如删除操作),则返回null或空字符串。dataTransfer:返回一个 DataTransfer 对象实例,该属性通常只在输入框接受富文本输入时有效。InputEvent的实例属性主要就是上面三个属性,这三个实例属性都是只读的。(1)InputEvent.data 变动的那部分内容InputEvent.data属性返回一个字符串,表示变动的内容。// HTML 代码如下
// <input type="text" id="myInput">
var input = document.getElementById('myInput');
input.addEventListener('input', myFunction, false);
function myFunction(e) {
console.log(e.data);
}上面代码中,如果手动在输入框里面输入abc,控制台会先输出a,再在下一行输出b,再在下一行输出c。然后选中abc,一次性将它们删除,控制台会输出null或一个空字符串。(2)InputEvent.inputType 变更类型InputEvent.inputType属性返回一个字符串,表示字符串发生变更的类型。对于常见情况,Chrome 浏览器的返回值如下。完整列表可以参考文档。手动插入文本:insertText粘贴插入文本:insertFromPaste向后删除:deleteContentBackward向前删除:deleteContentForward(3)InputEvent.dataTransferInputEvent.dataTransfer属性返回一个 DataTransfer 实例。该属性只在文本框接受粘贴内容(insertFromPaste)或拖拽内容(insertFromDrop)时才有效。八、触摸事件1、触摸操作概述浏览器的触摸 API 由三个部分组成。Touch:一个触摸点的实例TouchList:多个触摸点集合的实例TouchEvent:触摸引发的事件实例Touch接口的实例对象用来表示触摸点(一根手指或者一根触摸笔),包括位置、大小、形状、压力、目标元素等属性。有时,触摸动作由多个触摸点(多根手指)组成,多个触摸点的集合由TouchList接口的实例对象表示。TouchEvent接口的实例对象代表由触摸引发的事件,只有触摸屏才会引发这一类事件。很多时候,触摸事件和鼠标事件同时触发,即使这个时候并没有用到鼠标。这是为了让那些只定义鼠标事件、没有定义触摸事件的代码,在触摸屏的情况下仍然能用。如果想避免这种情况,可以用event.preventDefault方法阻止发出鼠标事件。TouchEvent {isTrusted: true, touches: TouchList, targetTouches: TouchList, changedTouches: TouchList, altKey: false, …} // TouchEvent接口 ,继承Event接口属性和方法
altKey: false
bubbles: true
cancelBubble: false
cancelable: false
changedTouches: TouchList // TouchList 接口 (所有触摸点集合)
0: Touch // Touch 接口 (单个触摸点)
clientX: 232
clientY: 96
force: 1 // 触摸压力
identifier: 0 // 唯一ID
pageX: 232
pageY: 96
radiusX: 11.5
radiusY: 11.5
region: null
rotationAngle: 0
screenX: 476
screenY: 266
target: html // 触摸目标元素
__proto__: Touch
length: 1
__proto__: TouchList
composed: true
ctrlKey: false
currentTarget: null
defaultPrevented: false
detail: 0
eventPhase: 0
isTrusted: true
metaKey: false
path: (3) [html, document, Window]
returnValue: true
shiftKey: false
sourceCapabilities: InputDeviceCapabilities {firesTouchEvents: true}
srcElement: html
target: html
targetTouches: TouchList {0: Touch, length: 1} // TouchList 接口 (所有触摸点集合)
timeStamp: 994.1749999998137
touches: TouchList {0: Touch, length: 1} // TouchList 接口 (所有触摸点集合)
type: "touchmove" // 当前触摸事件类型
view: Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}
which: 0
2、Touch 接口2.1 Touch 接口概述 (单个触摸点)Touch 接口代表单个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。浏览器原生提供Touch构造函数,用来生成Touch实例。var touch = new Touch(touchOptions);Touch构造函数接受一个配置对象作为参数,它有以下属性。identifier:必需,类型为整数,表示触摸点的唯一 ID。target:必需,类型为元素节点,表示触摸点开始时所在的网页元素。clientX:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的水平距离,默认为0。clientY:可选,类型为数值,表示触摸点相对于浏览器窗口左上角的垂直距离,默认为0。screenX:可选,类型为数值,表示触摸点相对于屏幕左上角的水平距离,默认为0。screenY:可选,类型为数值,表示触摸点相对于屏幕左上角的垂直距离,默认为0。pageX:可选,类型为数值,表示触摸点相对于网页左上角的水平位置(即包括页面的滚动距离),默认为0。pageY:可选,类型为数值,表示触摸点相对于网页左上角的垂直位置(即包括页面的滚动距离),默认为0。radiusX:可选,类型为数值,表示触摸点周围受到影响的椭圆范围的 X 轴半径,默认为0。radiusY:可选:类型为数值,表示触摸点周围受到影响的椭圆范围的 Y 轴半径,默认为0。rotationAngle:可选,类型为数值,表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间,默认值为0。force:可选,类型为数值,范围在0到1之间,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力,默认为0。2.2 Touch 接口的实例属性(1)Touch.identifier 触摸点的IDTouch.identifier属性返回一个整数,表示触摸点的唯一 ID。这个值在整个触摸过程保持不变,直到触摸事件结束。someElement.addEventListener('touchmove', function (e) {
for (var i = 0; i < e.changedTouches.length; i++) {
console.log(e.changedTouches[i].identifier);
}
}, false);(2)Touch.screenX,Touch.screenY,Touch.clientX,Touch.clientY,pageX,pageY (相对屏幕、浏览器、文档的坐标)Touch.screenX属性和Touch.screenY属性,分别表示触摸点相对于屏幕左上角的横坐标和纵坐标,与页面是否滚动无关。Touch.clientX属性和Touch.clientY属性,分别表示触摸点相对于浏览器视口左上角的横坐标和纵坐标,与页面是否滚动无关。Touch.pageX属性和Touch.pageY属性,分别表示触摸点相对于当前页面左上角的横坐标和纵坐标,包含了页面滚动带来的位移。(3)Touch.radiusX,Touch.radiusY,Touch.rotationAngle (触摸椭圆区域半径、角度)Touch.radiusX属性和Touch.radiusY属性,分别返回触摸点周围受到影响的椭圆范围的 X 轴半径和 Y 轴半径,单位为像素。乘以 2 就可以得到触摸范围的宽度和高度。Touch.rotationAngle属性表示触摸区域的椭圆的旋转角度,单位为度数,在0到90度之间。上面这三个属性共同定义了用户与屏幕接触的区域,对于描述手指这一类非精确的触摸,很有帮助。指尖接触屏幕,触摸范围会形成一个椭圆,这三个属性就用来描述这个椭圆区域。下面是一个示例。div.addEventListener('touchstart', rotate);
div.addEventListener('touchmove', rotate);
div.addEventListener('touchend', rotate);
function rotate(e) {
var touch = e.changedTouches.item(0);
e.preventDefault();
src.style.width = touch.radiusX * 2 + 'px';
src.style.height = touch.radiusY * 2 + 'px';
src.style.transform = 'rotate(' + touch.rotationAngle + 'deg)';
};(4)Touch.force 触摸压力Touch.force属性返回一个0到1之间的数值,表示触摸压力。0代表没有压力,1代表硬件所能识别的最大压力。(5)Touch.target 开始触摸时的元素Touch.target属性返回一个元素节点,代表触摸发生时所在的那个元素节点。即使触摸点已经离开了这个节点,该属性依然不变。3、TouchList 接口TouchList接口表示一组触摸点的集合。它的实例是一个类似数组的对象,成员是Touch的实例对象,表示所有触摸点。用户用三根手指触摸,产生的TouchList实例就会包含三个成员,每根手指的触摸点对应一个Touch实例对象。它的实例主要通过触摸事件的TouchEvent.touches、TouchEvent.changedTouches、TouchEvent.targetTouches这几个属性获取。它的实例属性和实例方法只有两个。TouchList.length:数值,表示成员数量(即触摸点的数量)。TouchList.item():返回指定位置的成员,它的参数是该成员的位置编号(从零开始)。4、TouchEvent 接口4.1 概述TouchEvent 接口继承了 Event 接口,表示由触摸引发的事件实例,通常来自触摸屏或轨迹板。除了被继承的属性以外,它还有一些自己的属性。浏览器原生提供TouchEvent()构造函数,用来生成触摸事件的实例。new TouchEvent(type, options)TouchEvent()构造函数可以接受两个参数,第一个参数是字符串,表示事件类型;第二个参数是事件的配置对象,该参数是可选的,对象的所有属性也是可选的。除了Event接口的配置属性,该接口还有一些自己的配置属性。touches:TouchList实例,代表所有的当前处于活跃状态的触摸点,默认值是一个空数组[]。targetTouches:TouchList实例,代表所有处在触摸的目标元素节点内部、且仍然处于活动状态的触摸点,默认值是一个空数组[]。changedTouches:TouchList实例,代表本次触摸事件的相关触摸点,默认值是一个空数组[]。ctrlKey:布尔值,表示 Ctrl 键是否同时按下,默认值为false。shiftKey:布尔值,表示 Shift 键是否同时按下,默认值为false。altKey:布尔值,表示 Alt 键是否同时按下,默认值为false。metaKey:布尔值,表示 Meta 键(或 Windows 键)是否同时按下,默认值为false。4.2 实例属性TouchEvent 接口的实例具有Event实例的所有属性和方法,此外还有一些它自己的实例属性,这些属性全部都是只读。(1)TouchEvent.altKey,TouchEvent.ctrlKey,TouchEvent.shiftKey,TouchEvent.metaKey (是否同时按某些功能键)TouchEvent.altKey:布尔值,表示触摸时是否按下了 Alt 键。TouchEvent.ctrlKey:布尔值,表示触摸时是否按下了 Ctrl 键。TouchEvent.shiftKey:布尔值:表示触摸时是否按下了 Shift 键。TouchEvent.metaKey:布尔值,表示触摸时是否按下了 Meta 键(或 Windows 键)。下面是一个示例。someElement.addEventListener('touchstart', function (e) {
console.log('altKey = ' + e.altKey);
console.log('ctrlKey = ' + e.ctrlKey);
console.log('metaKey = ' + e.metaKey);
console.log('shiftKey = ' + e.shiftKey);
}, false);(2)TouchEvent.changedTouches (触摸点集合,不同触摸事件,含义不同)TouchEvent.changedTouches属性返回一个TouchList实例,成员是一组Touch实例对象,表示本次触摸事件的相关触摸点。对于不同的事件,该属性的含义有所不同。touchstart事件:被激活的触摸点touchmove事件:发生变化的触摸点touchend事件:消失的触摸点(即不再被触碰的点)下面是一个示例。someElement.addEventListener('touchmove', function (e) {
for (var i = 0; i < e.changedTouches.length; i++) {
console.log(e.changedTouches[i].identifier);
}
}, false);(3)TouchEvent.touches (仍然活动的触摸点集合)TouchEvent.touches属性返回一个TouchList实例,成员是所有仍然处于活动状态(即触摸中)的触摸点。一般来说,一个手指就是一个触摸点。下面是一个示例。someElement.addEventListener('touchstart', function (e) {
switch (e.touches.length) {
// 一根手指触摸
case 1: handle_one_touch(e); break;
// 两根手指触摸
case 2: handle_two_touches(e); break;
// 三根手指触摸
case 3: handle_three_touches(e); break;
// 其他情况
default: console.log('Not supported'); break;
}
}, false);(4)TouchEvent.targetTouches (目标元素内活动的触摸点集合)TouchEvent.targetTouches属性返回一个TouchList实例,成员是触摸事件的目标元素节点内部、所有仍然处于活动状态(即触摸中)的触摸点。function touches_in_target(ev) {
return (ev.touches.length === ev.targetTouches.length ? true : false);
}上面代码用来判断,是否所有触摸点都在目标元素内。5、触摸事件的种类触摸引发的事件,有以下几种。可以通过TouchEvent.type属性,查看到底发生的是哪一种事件。touchstart:用户开始触摸时触发,它的target属性返回发生触摸的元素节点。【开始触摸】touchend:用户不再接触触摸屏时(或者移出屏幕边缘时)触发,它的target属性与touchstart事件一致的,就是开始触摸时所在的元素节点。它的changedTouches属性返回一个TouchList实例,包含所有不再触摸的触摸点(即Touch实例对象)。【触摸结束】touchmove:用户移动触摸点时触发,它的target属性与touchstart事件一致。如果触摸的半径、角度、力度发生变化,也会触发该事件。【触摸移动中】touchcancel:触摸点取消时触发,比如在触摸区域跳出一个模态窗口(modal window)、触摸点离开了文档区域(进入浏览器菜单栏)、用户的触摸点太多,超过了支持的上限(自动取消早先的触摸点)。【触摸点被取消】下面是一个例子。var el = document.getElementsByTagName('canvas')[0];
el.addEventListener('touchstart', handleStart, false);
el.addEventListener('touchmove', handleMove, false);
function handleStart(evt) {
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
console.log(touches[i].pageX, touches[i].pageY);
}
}
function handleMove(evt) {
evt.preventDefault();
var touches = evt.changedTouches;
for (var i = 0; i < touches.length; i++) {
var touch = touches[i];
console.log(touch.pageX, touch.pageY);
}
}九、拖拉事件1、拖拉事件的种类拖拉(drag)指的是,用户在某个对象上按下鼠标键不放,拖动它到另一个位置,然后释放鼠标键,将该对象放在那里。拖拉的对象有好几种,包括元素节点、图片、链接、选中的文字等等。在网页中,除了元素节点默认不可以拖拉,其他(图片、链接、选中的文字)都是可以直接拖拉的。为了让元素节点可拖拉,可以将该节点的draggable属性设为true。<div draggable="true">
此区域可拖拉
</div>draggable属性可用于任何元素节点,但是图片( <img> )和链接( <a> )不加这个属性,就可以拖拉。对于它们,用到这个属性的时候,往往是将其设为false,防止拖拉这两种元素。注意,一旦某个元素节点的draggable属性设为true,就无法再用鼠标选中该节点内部的文字或子节点了。当元素节点或选中的文本被拖拉时,就会持续触发拖拉事件,包括以下一些事件。拖拉的节点上触发:drag:拖拉过程中,在被拖拉的节点上持续触发(相隔几百毫秒)。dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。通常应该在这个事件的监听函数中,指定拖拉的数据。dragend:拖拉结束时(释放鼠标键或按下 ESC 键)在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。它与dragstart事件,在同一个节点上触发。不管拖拉是否跨窗口,或者中途被取消,dragend事件总是会触发的。拖拉到别的节点上触发:dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。drop:被拖拉的节点或选中的文本,释放到目标节点时,在目标节点上触发。注意,如果当前节点不允许drop,即使在该节点上方松开鼠标键,也不会触发该事件。如果用户按下 ESC 键,取消这个操作,也不会触发该事件。该事件的监听函数负责取出拖拉数据,并进行相关处理。下面的例子展示,如何动态改变被拖动节点的背景色。div.addEventListener('dragstart', function (e) {
this.style.backgroundColor = 'red';
}, false);
div.addEventListener('dragend', function (e) {
this.style.backgroundColor = 'green';
}, false);上面代码中,div节点被拖动时,背景色会变为红色,拖动结束,又变回绿色。<div class="div1" draggable="true">
div1,此区域可拖拉
</div>
<div class="div2">
div2
</div>
<script>
var div1 = document.querySelector('.div1');
var div2 = document.querySelector('.div2');
div1.addEventListener('dragstart', function (e){
console.log('开始拖拉')
});
div1.addEventListener('drag', function (e) {
console.log('拖拉中') // 持续触发
});
div1.addEventListener('dragend', function (e){
console.log('结束拖拉')
});
div2.addEventListener('dragenter', function(){
console.log('拖到了div2')
})
div2.addEventListener('dragover', function(){
//console.log('正在div2上方') // 持续触发
})
div2.addEventListener('dragleave', function(){
console.log('离开div2')
})
div2.addEventListener('drop', function() {
console.log('在div2上释放')
})
</script>下面是一个例子,展示如何实现将一个节点从当前父节点,拖拉到另一个父节点中。/* HTML 代码如下
<div class="dropzone">
<div id="draggable" draggable="true">
该节点可拖拉
</div>
</div>
<div class="dropzone"></div>
<div class="dropzone"></div>
<div class="dropzone"></div>
*/
// 被拖拉节点
var dragged;
document.addEventListener('dragstart', function (event) {
// 保存被拖拉节点
dragged = event.target;
// 被拖拉节点的背景色变透明
event.target.style.opacity = 0.5;
}, false);
document.addEventListener('dragend', function (event) {
// 被拖拉节点的背景色恢复正常
event.target.style.opacity = '';
}, false);
document.addEventListener('dragover', function (event) {
// 防止拖拉效果被重置,允许被拖拉的节点放入目标节点
event.preventDefault();
}, false);
document.addEventListener('dragenter', function (event) {
// 目标节点的背景色变紫色
// 由于该事件会冒泡,所以要过滤节点
if (event.target.className === 'dropzone') {
event.target.style.background = 'purple';
}
}, false);
document.addEventListener('dragleave', function( event ) {
// 目标节点的背景色恢复原样
if (event.target.className === 'dropzone') {
event.target.style.background = '';
}
}, false);
document.addEventListener('drop', function( event ) {
// 防止事件默认行为(比如某些元素节点上可以打开链接),
event.preventDefault();
if (event.target.className === 'dropzone') {
// 恢复目标节点背景色
event.target.style.background = '';
// 将被拖拉节点插入目标节点
dragged.parentNode.removeChild(dragged);
event.target.appendChild( dragged );
}
}, false);关于拖拉事件,有以下几个注意点。拖拉过程只触发以上这些拖拉事件,尽管鼠标在移动,但是鼠标事件不会触发。将文件从操作系统拖拉进浏览器,不会触发dragstart和dragend事件。dragenter和dragover事件的监听函数,用来取出拖拉的数据(即允许放下被拖拉的元素)。由于网页的大部分区域不适合作为放下拖拉元素的目标节点,所以这两个事件的默认设置为当前节点不允许接受被拖拉的元素。如果想要在目标节点上放下的数据,首先必须阻止这两个事件的默认行为。<div ondragover="return false">
<div ondragover="event.preventDefault()">上面代码中,如果不取消拖拉事件或者阻止默认行为,就不能在div节点上放下被拖拉的节点。2、DragEvent 接口拖拉事件都继承了DragEvent接口,这个接口又继承了MouseEvent接口和Event接口。浏览器原生提供一个DragEvent()构造函数,用来生成拖拉事件的实例对象。new DragEvent(type, options)DragEvent()构造函数接受两个参数,第一个参数是字符串,表示事件的类型,该参数必须;第二个参数是事件的配置对象,用来设置事件的属性,该参数可选。配置对象除了接受MouseEvent接口和Event接口的配置属性,还可以设置dataTransfer属性要么是null,要么是一个DataTransfer接口的实例。DataTransfer的实例对象用来读写拖拉事件中传输的数据,详见下文《DataTransfer 接口》的部分。3、DataTransfer 接口概述所有拖拉事件的实例都有一个DragEvent.dataTransfer属性,用来读写需要传递的数据。这个属性的值是一个DataTransfer接口的实例。浏览器原生提供一个DataTransfer()构造函数,用来生成DataTransfer实例对象。var dataTrans = new DataTransfer();DataTransfer()构造函数不接受参数。拖拉的数据分成两方面:数据的种类(又称格式)和数据的值。数据的种类是一个 MIME 字符串(比如text/plain、image/jpeg),数据的值是一个字符串。一般来说,如果拖拉一段文本,则数据默认就是那段文本;如果拖拉一个链接,则数据默认就是链接的 URL。拖拉事件开始时,开发者可以提供数据类型和数据值。拖拉过程中,开发者通过dragenter和dragover事件的监听函数,检查数据类型,以确定是否允许放下(drop)被拖拉的对象。比如,在只允许放下链接的区域,检查拖拉的数据类型是否为text/uri-list。发生drop事件时,监听函数取出拖拉的数据,对其进行处理。4、DataTransfer 的实例属性4.1 DataTransfer.dropEffect 设置接受拖拉的区域的效果DataTransfer.dropEffect属性用来设置放下(drop)被拖拉节点时的效果,会影响到拖拉经过相关区域时鼠标的形状。它可能取下面的值。copy:复制被拖拉的节点move:移动被拖拉的节点link:创建指向被拖拉的节点的链接none:无法放下被拖拉的节点除了上面这些值,设置其他的值都是无效的。target.addEventListener('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
});上面代码中,被拖拉元素一旦drop,接受的区域会复制该节点。dropEffect属性一般在dragenter和dragover事件的监听函数中设置,对于dragstart、drag、dragleave这三个事件,该属性不起作用。因为该属性只对接受被拖拉的节点的区域有效,对被拖拉的节点本身是无效的。进入目标区域后,拖拉行为会初始化成设定的效果。4.2 DataTransfer.effectAllowed 设置被拖拉的节点允许的效果DataTransfer.effectAllowed属性设置本次拖拉中允许的效果。它可能取下面的值。copy:复制被拖拉的节点move:移动被拖拉的节点link:创建指向被拖拉节点的链接copyLink:允许copy或linkcopyMove:允许copy或movelinkMove:允许link或moveall:允许所有效果none:无法放下被拖拉的节点uninitialized:默认值,等同于all如果某种效果是不允许的,用户就无法在目标节点中达成这种效果。这个属性与dropEffect属性是同一件事的两个方面。前者设置被拖拉的节点允许的效果,后者设置接受拖拉的区域的效果,它们往往配合使用。dragstart事件的监听函数,可以用来设置这个属性。其他事件的监听函数里面设置这个属性是无效的。source.addEventListener('dragstart', function (e) { // 被拖拉节点上设置
e.dataTransfer.effectAllowed = 'move';
});
target.addEventListener('dragover', function (e) { // 接受区域节点上设置
e.dataTransfer.dropEffect = 'move';
});只要dropEffect属性和effectAllowed属性之中,有一个为none,就无法在目标节点上完成drop操作。4.3 DataTransfer.files 本地文件DataTransfer.files属性是一个 FileList 对象,包含一组本地文件,可以用来在拖拉操作中传送。如果本次拖拉不涉及文件,则该属性为空的 FileList 对象。例子:接收拖拉文件// HTML 代码如下
// <div id="output" style="min-height: 200px;border: 1px solid black;">
// 文件拖拉到这里
// </div>
var div = document.getElementById('output');
div.addEventListener("dragenter", function( event ) {
div.textContent = '';
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("dragover", function( event ) {
event.stopPropagation();
event.preventDefault();
}, false);
div.addEventListener("drop", function( event ) {
event.stopPropagation();
event.preventDefault();
var files = event.dataTransfer.files;
for (var i = 0; i < files.length; i++) {
div.textContent += files[i].name + ' ' + files[i].size + '字节\n';
}
}, false);上面代码中,通过dataTransfer.files属性读取被拖拉的文件的信息。如果想要读取文件内容,就要使用FileReader对象。div.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
var fileList = e.dataTransfer.files;
if (fileList.length > 0) {
var file = fileList[0];
var reader = new FileReader();
reader.readAsDataURL(file);// 解析为url
reader.onloadend = function(e) {
if (e.target.readyState === FileReader.DONE) {
var content = reader.result;
div.innerHTML = '<img src="'+ content +'"> File: ' + file.name + '\n\n' + content;
}
}
reader.readAsBinaryString(file);
}
});4.4 DataTransfer.types 数据格式DataTransfer.types属性是一个只读的数组,每个成员是一个字符串,里面是拖拉的数据格式(通常是 MIME 值)。比如,如果拖拉的是文字,对应的成员就是text/plain。下面是一个例子,通过检查dataTransfer属性的类型,决定是否允许在当前节点执行drop操作。function contains(list, value){
for (var i = 0; i < list.length; ++i) {
if(list[i] === value) return true;
}
return false;
}
function doDragOver(event) {
var isLink = contains(event.dataTransfer.types, 'text/uri-list');
if (isLink) event.preventDefault();
}上面代码中,只有当被拖拉的节点是一个链接时,才允许在当前节点放下。4.5 DataTransfer.itemsDataTransfer.items属性返回一个类似数组的只读对象(DataTransferItemList 实例),每个成员就是本次拖拉的一个对象(DataTransferItem 实例)。如果本次拖拉不包含对象,则返回一个空对象。DataTransferItemList 实例具有以下的属性和方法。length:返回成员的数量add(data, type):增加一个指定内容和类型(比如text/html和text/plain)的字符串作为成员add(file):add方法的另一种用法,增加一个文件作为成员remove(index):移除指定位置的成员clear():移除所有的成员DataTransferItem 实例具有以下的属性和方法。kind:返回成员的种类(string还是file)。type:返回成员的类型(通常是 MIME 值)。getAsFile():如果被拖拉是文件,返回该文件,否则返回null。getAsString(callback):如果被拖拉的是字符串,将该字符传入指定的回调函数处理。该方法是异步的,所以需要传入回调函数。下面是一个例子。div.addEventListener('drop', function (e) {
e.preventDefault();
if (e.dataTransfer.items != null) {
for (var i = 0; i < e.dataTransfer.items.length; i++) {
console.log(e.dataTransfer.items[i].kind + ': ' + e.dataTransfer.items[i].type);
}
}
});5、DataTransfer 的实例方法5.1 DataTransfer.setData() 设置拖拉事件所带有的数据DataTransfer.setData()方法用来设置拖拉事件所带有的数据。该方法没有返回值。event.dataTransfer.setData('text/plain', 'Text to drag');上面代码为当前的拖拉事件加入纯文本数据。该方法接受两个参数,都是字符串。第一个参数表示数据类型(比如text/plain),第二个参数是具体数据。如果指定类型的数据在dataTransfer属性不存在,那么这些数据将被加入,否则原有的数据将被新数据替换。如果是拖拉文本框或者拖拉选中的文本,会默认将对应的文本数据,添加到dataTransfer属性,不用手动指定。<div draggable="true">
aaa
</div>上面代码中,拖拉这个 <div> 元素会自动带上文本数据aaa。使用setData方法,可以替换到原有数据。<div
draggable="true"
ondragstart="event.dataTransfer.setData('text/plain', 'bbb')"
>
aaa
</div>上面代码中,拖拉数据实际上是bbb,而不是aaa。下面是添加其他类型的数据。由于text/plain是最普遍支持的格式,为了保证兼容性,建议最后总是保存一份纯文本格式的数据。var dt = event.dataTransfer;
// 添加链接
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');
// 添加 HTML 代码
dt.setData('text/html', 'Hello there, <strong>stranger</strong>');
dt.setData('text/plain', 'Hello there, <strong>stranger</strong>');
// 添加图像的 URL
dt.setData('text/uri-list', imageurl);
dt.setData('text/plain', imageurl);可以一次提供多种格式的数据。var dt = event.dataTransfer;
dt.setData('application/x-bookmark', bookmarkString);
dt.setData('text/uri-list', 'http://www.example.com');
dt.setData('text/plain', 'http://www.example.com');上面代码中,通过在同一个事件上面,存放三种类型的数据,使得拖拉事件可以在不同的对象上面,drop不同的值。注意,第一种格式是一个自定义格式,浏览器默认无法读取,这意味着,只有某个部署了特定代码的节点,才可能drop(读取到)这个数据。5.2 DataTransfer.getData() 返回指定类型的数据DataTransfer.getData()方法接受一个字符串(表示数据类型)作为参数,返回事件所带的指定类型的数据(通常是用setData方法添加的数据)。如果指定类型的数据不存在,则返回空字符串。通常只有drop事件触发后,才能取出数据。下面是一个drop事件的监听函数,用来取出指定类型的数据。function onDrop(event) {
var data = event.dataTransfer.getData('text/plain');
event.target.textContent = data;
event.preventDefault();
}上面代码取出拖拉事件的文本数据,将其替换成当前节点的文本内容。注意,这时还必须取消浏览器的默认行为,因为假如用户拖拉的是一个链接,浏览器默认会在当前窗口打开这个链接。getData方法返回的是一个字符串,如果其中包含多项数据,就必须手动解析。function doDrop(event) {
var lines = event.dataTransfer.getData('text/uri-list').split('\n');
for (let line of lines) {
let link = document.createElement('a');
link.href = line;
link.textContent = line;
event.target.appendChild(link);
}
event.preventDefault();
}上面代码中,getData方法返回的是一组链接,就必须自行解析。类型值指定为URL,可以取出第一个有效链接。var link = event.dataTransfer.getData('URL');下面的例子是从多种类型的数据里面取出数据。function doDrop(event) {
var types = event.dataTransfer.types;
var supportedTypes = ['text/uri-list', 'text/plain'];
types = supportedTypes.filter(function (value) { types.includes(value) });
if (types.length) {
var data = event.dataTransfer.getData(types[0]);
}
event.preventDefault();
}5.3 DataTransfer.clearData() 清除指定或全部数据DataTransfer.clearData()方法接受一个字符串(表示数据类型)作为参数,删除事件所带的指定类型的数据。如果没有指定类型,则删除所有数据。如果指定类型不存在,则调用该方法不会产生任何效果。event.dataTransfer.clearData('text/uri-list');上面代码清除事件所带的text/uri-list类型的数据。该方法不会移除拖拉的文件,因此调用该方法后,DataTransfer.types属性可能依然会返回Files类型(前提是存在文件拖拉)。注意,该方法只能在dragstart事件的监听函数之中使用,因为这是拖拉操作的数据唯一可写的时机。5.4 DataTransfer.setDragImage() 设置拖动过程中的图片拖动过程中(dragstart事件触发后),浏览器会显示一张图片跟随鼠标一起移动,表示被拖动的节点。这张图片是自动创造的,通常显示为被拖动节点的外观,不需要自己动手设置。DataTransfer.setDragImage()方法可以自定义这张图片。它接受三个参数。第一个是 <img> 节点或者 <canvas> 节点,如果省略或为null,则使用被拖动的节点的外观;第二个和第三个参数为鼠标相对于该图片左上角的横坐标和右坐标。下面是一个例子。/* HTML 代码如下
<div id="drag-with-image" class="dragdemo" draggable="true">
drag me
</div>
*/
var div = document.getElementById('drag-with-image');
div.addEventListener('dragstart', function (e) {
var img = document.createElement('img');
img.src = 'http://path/to/img';
e.dataTransfer.setDragImage(img, 0, 0);
}, false);十、其他常见事件1、资源事件1.1 beforeunload 事件 (关闭窗口前调用)beforeunload事件在窗口、文档、各种资源将要卸载前触发。它可以用来防止用户不小心卸载资源。如果该事件对象的returnValue属性是一个非空字符串,那么浏览器就会弹出一个对话框,询问用户是否要卸载该资源。但是,用户指定的字符串可能无法显示,浏览器会展示预定义的字符串。如果用户点击“取消”按钮,资源就不会卸载。window.addEventListener('beforeunload', function (event) {
event.returnValue = '你确定离开吗?';
});上面代码中,用户如果关闭窗口,浏览器会弹出一个窗口,要求用户确认。浏览器对这个事件的行为很不一致,有的浏览器调用event.preventDefault(),也会弹出对话框。IE 浏览器需要显式返回一个非空的字符串,才会弹出对话框。而且,大多数浏览器在对话框中不显示指定文本,只显示默认文本。因此,可以采用下面的写法,取得最大的兼容性。window.addEventListener('beforeunload', function (e) {
var confirmationMessage = '确认关闭窗口?';
e.returnValue = confirmationMessage;
return confirmationMessage;
});注意,许多手机浏览器默认忽略这个事件,桌面浏览器也有办法忽略这个事件。所以,它可能根本不会生效,不能依赖它来阻止用户关闭窗口。另外,一旦使用了beforeunload事件,浏览器就不会缓存当前网页,使用“回退”按钮将重新向服务器请求网页。这是因为监听这个事件的目的,一般是修改初始状态,这时缓存初始页面就没意义了。基本上,只有一种场合可以监听unload事件,其他情况都不应该监听:用户修改了表单,还没有保存就要离开。1.2 unload 事件 (即将关闭窗口时调用)unload事件在窗口关闭或者document对象将要卸载时触发。它的触发顺序排在beforeunload、pagehide事件后面。unload事件发生时,文档处于一个特殊状态。所有资源依然存在,但是对用户来说都不可见,UI 互动全部无效。这个事件是无法取消的,即使在监听函数里面抛出错误,也不能停止文档的卸载。window.addEventListener('unload', function(event) {
console.log('文档将要卸载');
});手机上,浏览器或系统可能会直接丢弃网页,这时该事件根本不会发生。而且跟beforeunload事件一样,一旦使用了unload事件,浏览器就不会缓存当前网页,理由同上。因此,任何情况下都不应该依赖这个事件,指定网页卸载时要执行的代码,可以考虑完全不使用这个事件。1.3 load 事件,error 事件 ,abort事件 (页面或某个资源加载成功/失败/取消时调用)load事件在页面或某个资源加载成功时触发。注意,页面或资源从浏览器缓存加载,并不会触发load事件。window.addEventListener('load', function(event) {
console.log('所有资源都加载完成');
});error事件是在页面或资源加载失败时触发。abort事件是在用户取消加载时触发。这三个事件实际上属于进度事件,不仅发生在document对象,还发生在各种外部资源上面。浏览网页就是一个加载各种资源的过程,图像(image)、样式表(style sheet)、脚本(script)、视频(video)、音频(audio)、Ajax请求(XMLHttpRequest)等等。这些资源和document对象、window对象、XMLHttpRequestUpload 对象,都会触发load事件和error事件。触发这三个事件的对象有:windowdocumentbodyimgstylescriptvideoaudioAjax等等2、session 历史事件2.1 pageshow 事件(页面显示,加载页面后执行),pagehide 事件(退出当前页面触发)默认情况下,浏览器会在当前会话(session)缓存页面,当用户点击“前进/后退”按钮时,浏览器就会从缓存中加载页面。pageshow 事件在页面加载时触发,包括第一次加载和从缓存加载两种情况。如果要指定页面每次加载(不管是不是从浏览器缓存)时都运行的代码,可以放在这个事件的监听函数。第一次加载时,它的触发顺序排在load事件后面。从缓存加载时,load事件不会触发,因为网页在缓存中的样子通常是load事件的监听函数运行后的样子,所以不必重复执行。同理,如果是从缓存中加载页面,网页内初始化的 JavaScript 脚本(比如 DOMContentLoaded 事件的监听函数)也不会执行。window.addEventListener('pageshow', function(event) {
console.log('pageshow: ', event);
});pageshow 事件有一个persisted属性,返回一个布尔值。页面第一次加载时,这个属性是false;当页面从缓存加载时,这个属性是true。window.addEventListener('pageshow', function(event){
if (event.persisted) { // 是否从缓存加载
// ...
}
});pagehide事件与pageshow事件类似,当用户通过“前进/后退”按钮,离开当前页面时触发。它与 unload 事件的区别在于,如果在 window 对象上定义unload事件的监听函数之后,页面不会保存在缓存中,而使用pagehide事件,页面会保存在缓存中。pagehide事件实例也有一个persisted属性,将这个属性设为true,就表示页面要保存在缓存中;设为false,表示网页不保存在缓存中,这时如果设置了unload 事件的监听函数,该函数将在 pagehide 事件后立即运行。如果页面包含 <frame> 或 <iframe> 元素,则 <frame> 页面的pageshow事件和pagehide事件,都会在主页面之前触发。注意,这两个事件只在浏览器的history(历史)对象发生变化时触发,跟网页是否可见没有关系。2.2 popstate 事件(在浏览器的history对象的当前记录发生显式切换时触发)popstate事件在浏览器的history对象的当前记录发生显式切换时触发。注意,调用history.pushState()或history.replaceState(),并不会触发popstate事件。该事件只在用户在history记录之间显式切换时触发,比如鼠标点击“后退/前进”按钮,或者在脚本中调用history.back()、history.forward()、history.go()时触发。该事件对象有一个state属性,保存history.pushState方法和history.replaceState方法为当前记录添加的state对象。window.onpopstate = function (event) {
console.log('state: ' + event.state);
};
history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');
history.back(); // state: {"page":1}
history.back(); // state: null
history.go(2); // state: {"page":3}上面代码中,pushState方法向history添加了两条记录,然后replaceState方法替换掉当前记录。因此,连续两次back方法,会让当前条目退回到原始网址,它没有附带state对象,所以事件的state属性为null,然后前进两条记录,又回到replaceState方法添加的记录。浏览器对于页面首次加载,是否触发popstate事件,处理不一样,Firefox 不触发该事件。2.3 hashchange 事件 (hash发生变化时触发)hashchange事件在 URL 的 hash 部分(即#号后面的部分,包括#号)发生变化时触发。该事件一般在window对象上监听。hashchange的事件实例具有两个特有属性:oldURL属性和newURL属性,分别表示变化前后的完整 URL。// URL 是 http://www.example.com/
window.addEventListener('hashchange', myFunction);
function myFunction(e) {
console.log(e.oldURL);
console.log(e.newURL);
}
location.hash = 'part2';
// http://www.example.com/
// http://www.example.com/#part23、网页状态事件3.1 DOMContentLoaded 事件 (DOM内容加载完成后触发)网页下载并解析完成以后,浏览器就会在document对象上触发 DOMContentLoaded 事件。这时,仅仅完成了网页的解析(整张页面的 DOM 生成了),所有外部资源(样式表、脚本、iframe 等等)可能还没有下载结束。也就是说,这个事件比load事件,发生时间早得多。document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM生成');
});注意,网页的 JavaScript 脚本是同步执行的,脚本一旦发生堵塞,将推迟触发DOMContentLoaded事件。document.addEventListener('DOMContentLoaded', function (event) {
console.log('DOM 生成');
});
// 这段代码会推迟触发 DOMContentLoaded 事件
for(var i = 0; i < 1000000000; i++) {
// ...
}3.2 readystatechange 事件(Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发)readystatechange事件当 Document 对象和 XMLHttpRequest 对象的readyState属性发生变化时触发。document.readyState有三个可能的值:loading(网页正在加载)、interactive(网页已经解析完成,但是外部资源仍然处在加载状态)和complete(网页和所有外部资源已经结束加载,load事件即将触发)。document.onreadystatechange = function () {
if (document.readyState === 'interactive') {
// ...
}
}这个事件可以看作DOMContentLoaded事件的另一种实现方法。4、窗口事件4.1 scroll 事件 (文档或文档元素滚动时触发)scroll事件在文档或文档元素滚动时触发,主要出现在用户拖动滚动条。window.addEventListener('scroll', callback);该事件会连续地大量触发,所以它的监听函数之中不应该有非常耗费计算的操作。推荐的做法是使用requestAnimationFrame或setTimeout控制该事件的触发频率,然后可以结合customEvent抛出一个新事件。(function () {
var throttle = function (type, name, obj) { // 节流函数 控制触发频率
var obj = obj || window;
var running = false;
var func = function () {
if (running) { return; }
running = true;
requestAnimationFrame(function() { // 结合此方法控制在每秒60次
obj.dispatchEvent(new CustomEvent(name));
running = false;
});
};
obj.addEventListener(type, func);
};
// 将 scroll 事件重定义为 optimizedScroll 事件
throttle('scroll', 'optimizedScroll');
})();
window.addEventListener('optimizedScroll', function() {
console.log('Resource conscious scroll callback!');
});上面代码中,throttle函数用于控制事件触发频率,requestAnimationFrame方法保证每次页面重绘(每秒60次),只会触发一次scroll事件的监听函数。也就是说,上面方法将scroll事件的触发频率,限制在每秒60次。具体来说,就是scroll事件只要频率低于每秒60次,就会触发optimizedScroll事件,从而执行optimizedScroll事件的监听函数。改用setTimeout方法,可以放置更大的时间间隔。(function() {
window.addEventListener('scroll', scrollThrottler, false);
var scrollTimeout;
function scrollThrottler() {
if (!scrollTimeout) {
scrollTimeout = setTimeout(function () {
scrollTimeout = null;
actualScrollHandler();
}, 66);
}
}
function actualScrollHandler() {
// ...
}
}());上面代码中,每次scroll事件都会执行scrollThrottler函数。该函数里面有一个定时器setTimeout,每66毫秒触发一次(每秒15次)真正执行的任务actualScrollHandler。下面是一个更一般的throttle函数的写法。function throttle(fn, wait) {
var time = Date.now();
return function() {
if ((time + wait - Date.now()) < 0) {
fn();
time = Date.now();
}
}
}
window.addEventListener('scroll', throttle(callback, 1000));上面的代码将scroll事件的触发频率,限制在一秒一次。lodash函数库提供了现成的throttle函数,可以直接使用。window.addEventListener('scroll', _.throttle(callback, 1000));本书前面介绍过debounce的概念,throttle与它区别在于,throttle是“节流”,确保一段时间内只执行一次,而debounce是“防抖”,要连续操作结束后再执行。以网页滚动为例,debounce要等到用户停止滚动后才执行,throttle则是如果用户一直在滚动网页,那么在滚动过程中还是会执行。4.2 resize 事件(窗口大小变化时触发)resize事件在改变浏览器窗口大小时触发,主要发生在window对象上面。var resizeMethod = function () {
if (document.body.clientWidth < 768) {
console.log('移动设备的视口');
}
};
window.addEventListener('resize', resizeMethod, true);该事件也会连续地大量触发,所以最好像上面的scroll事件一样,通过throttle函数控制事件触发频率。4.3 fullscreenchange 事件(元素进入/退出全屏时触发),fullscreenerror 事件(无法切换全屏时触发)fullscreenchange事件在元素进入或退出全屏状态时触发,该事件发生在document对象上面。注意,此事件非浏览器的进入/退出全屏时触发的事件。document.addEventListener('fullscreenchange', function (event) {
console.log(document.fullscreenElement); // 全屏的元素,如果为null时表示已退出全屏
});
document.addEventListener('click', function (event) {
document.querySelector('div').requestFullscreen(); // 设置元素为全屏展示
});fullscreenerror事件在浏览器无法切换到全屏状态时触发。5、剪贴板事件以下三个事件属于剪贴板操作的相关事件。cut:将选中的内容从文档中移除,加入剪贴板时触发。【剪切】copy:进行复制动作时触发。【拷贝】paste:剪贴板内容粘贴到文档后触发。【粘贴】这三个事件都是ClipboardEvent接口的实例。ClipboardEvent有一个实例属性clipboardData,是一个 DataTransfer 对象,存放剪贴的数据。具体的 API 接口和操作方法,请参见《拖拉事件》的 DataTransfer 对象部分。document.addEventListener('copy', function (e) {
e.clipboardData.setData('text/plain', 'Hello, world!');
e.clipboardData.setData('text/html', '<b>Hello, world!</b>');
e.preventDefault();
});上面的代码使得复制进入剪贴板的,都是开发者指定的数据,而不是用户想要拷贝的数据。6、焦点事件焦点事件发生在元素节点和document对象上面,与获得或失去焦点相关。它主要包括以下四个事件。focus:元素节点获得焦点后触发,该事件不会冒泡。blur:元素节点失去焦点后触发,该事件不会冒泡。focusin:元素节点将要获得焦点时触发,发生在focus事件之前。该事件会冒泡。focusout:元素节点将要失去焦点时触发,发生在blur事件之前。该事件会冒泡。这四个事件都继承了FocusEvent接口。FocusEvent实例具有以下属性。FocusEvent.target:事件的目标节点。FocusEvent.relatedTarget:对于focusin事件,返回失去焦点的节点;对于focusout事件,返回将要接受焦点的节点;对于focus和blur事件,返回null。由于focus和blur事件不会冒泡,只能在捕获阶段触发,所以addEventListener方法的第三个参数需要设为true。form.addEventListener('focus', function (event) {
event.target.style.background = 'pink';
}, true);
form.addEventListener('blur', function (event) {
event.target.style.background = '';
}, true);上面代码针对表单的文本输入框,接受焦点时设置背景色,失去焦点时去除背景色。7、CustomEvent 接口(自定义事件)CustomEvent 接口用于生成自定义的事件实例。那些浏览器预定义的事件,虽然可以手动生成,但是往往不能在事件上绑定数据。如果需要在触发事件的同时,传入指定的数据,就可以使用 CustomEvent 接口生成的自定义事件对象。浏览器原生提供CustomEvent()构造函数,用来生成 CustomEvent 事件实例。new CustomEvent(type, options)CustomEvent()构造函数接受两个参数。第一个参数是字符串,表示事件的名字,这是必须的。第二个参数是事件的配置对象,这个参数是可选的。CustomEvent的配置对象除了接受 Event 事件的配置属性,只有一个自己的属性。detail:表示事件的附带数据,默认为null。下面是一个例子。var event = new CustomEvent('build', { 'detail': 'hello' });
function eventHandler(e) {
console.log(e.detail);
}
document.body.addEventListener('build', function (e) {
console.log(e.detail);
});
document.body.dispatchEvent(event);上面代码中,我们手动定义了build事件。该事件触发后,会被监听到,从而输出该事件实例的detail属性(即字符串hello)。下面是另一个例子。var myEvent = new CustomEvent('myevent', {
detail: {
foo: 'bar'
},
bubbles: true,
cancelable: false
});
el.addEventListener('myevent', function (event) {
console.log('Hello ' + event.detail.foo);
});
el.dispatchEvent(myEvent);上面代码也说明,CustomEvent 的事件实例,除了具有 Event 接口的实例属性,还具有detail属性。十一、GlobalEventHandlers 接口 (全局事件处理接口)指定事件的回调函数,推荐使用的方法是元素的addEventListener方法。div.addEventListener('click', clickHandler, false);除了之外,还有一种方法可以直接指定事件的回调函数。div.onclick = clickHandler;这个接口是由GlobalEventHandlers接口提供的。它的优点是使用比较方便,缺点是只能为每个事件指定一个回调函数,并且无法指定事件触发的阶段(捕获阶段还是冒泡阶段)。HTMLElement、Document和Window都继承了这个接口,也就是说,各种 HTML 元素、document对象、window对象上面都可以使用GlobalEventHandlers接口提供的属性。下面就列出这个接口提供的主要的事件属性。1、GlobalEventHandlers.onabort (中断事件)某个对象的abort事件(停止加载)发生时,就会调用onabort属性指定的回调函数。各种元素的停止加载事件,到底如何触发,目前并没有统一的规定。因此实际上,这个属性现在一般只用在<img>元素上面。// HTML 代码如下
// <img src="example.jpg" id="img">
var img = document.getElementById('img');
img.onabort = function () {
console.log('图片加载中断');
}2、GlobalEventHandlers.onerror (错误事件)error事件发生时,就会调用onerror属性指定的回调函数。error事件分成两种。一种是 JavaScript 的运行时错误,这会传到window对象,导致window.onerror()。window.onerror = function (message, source, lineno, colno, error) {
// ...
}window.onerror的处理函数共接受五个参数,含义如下。message:错误信息字符串source:报错脚本的 URLlineno:报错的行号,是一个整数colno:报错的列号,是一个整数error: 错误对象另一种是资源加载错误,比如 <img> 或 <script> 加载的资源出现加载错误。这时,Error 对象会传到对应的元素,导致该元素的onerror属性开始执行。element.onerror = function (event) {
// ...
}注意,一般来说,资源的加载错误不会触发window.onerror。3、GlobalEventHandlers.onload(加载完成事件)、GlobalEventHandlers.onloadstart(开始加载事件)元素完成加载时,会触发load事件,执行onload()。它的典型使用场景是window对象和 <img> 元素。对于window对象来说,只有页面的所有资源加载完成(包括图片、脚本、样式表、字体等所有外部资源),才会触发load事件。对于 <img> 和 <video> 等元素,加载开始时还会触发loadstart事件,导致执行onloadstart。4、GlobalEventHandlers.onfocus(获取焦点事件),GlobalEventHandlers.onblur(失去焦点事件)当前元素获得焦点时,会触发element.onfocus;失去焦点时,会触发element.onblur。element.onfocus = function () {
console.log("onfocus event detected!");
};
element.onblur = function () {
console.log("onblur event detected!");
};注意,如果不是可以接受用户输入的元素,要触发onfocus,该元素必须有tabindex属性。5、GlobalEventHandlers.onscroll(滚动事件)页面或元素滚动时,会触发scroll事件,导致执行onscroll()。6、GlobalEventHandlers.oncontextmenu(右键菜单事件),GlobalEventHandlers.onshow(显示右键菜单时触发)用户在页面上按下鼠标的右键,会触发contextmenu事件,导致执行oncontextmenu()。如果该属性执行后返回false,就等于禁止了右键菜单。document.oncontextmenu与window.oncontextmenu效果一样。document.oncontextmenu = function () {// 禁用右键菜单
return false;
};上面代码中,oncontextmenu属性执行后返回false,右键菜单就不会出现。元素的右键菜单显示时,会触发该元素的onshow监听函数。7、其他的事件属性鼠标的事件属性。onclickondblclickonmousedownonmouseenteronmouseleaveonmousemoveonmouseoutonmouseoveronmouseuponwheel键盘的事件属性。onkeydownonkeypressonkeyup焦点的事件属性。onbluronfocus表单的事件属性。oninputonchangeonsubmitonresetoninvalidonselect触摸的事件属性。ontouchcancelontouchendontouchmoveontouchstart拖动的事件属性分成两类:一类与被拖动元素相关,另一类与接收被拖动元素的容器元素相关。被拖动元素的事件属性。ondragstart:拖动开始ondrag:拖动过程中,每隔几百毫秒触发一次ondragend:拖动结束接收被拖动元素的容器元素的事件属性。ondragenter:被拖动元素进入容器元素。ondragleave:被拖动元素离开容器元素。ondragover:被拖动元素在容器元素上方,每隔几百毫秒触发一次。ondrop:松开鼠标后,被拖动元素放入容器元素。 <dialog> 对话框元素的事件属性。oncancelonclose文档学习文档:https://wangdoc.com/javascript/
前端面试题目搜集
一、理论知识
1.1、前端 MV*框架的意义
早期前端都是比较简单,基本以页面为工作单元,内容以浏览型为主,也偶尔有简单的表单操作,基本不太需要框架。
随着 AJAX 的出现,Web2.0的兴起,人们可以在页面上可以做比较复杂的事情了,然后前端框架才真正出现了。
如果是页面型产品,多数确实不太需要它,因为页面中的 JavaScript代码,处理交互的绝对远远超过处理模型的,但是如果是应用软件类产品,这就太需要了。
长期做某个行业软件的公司,一般都会沉淀下来一些业务组件,主要体现在数据模型、业务规则和业务流程,这些组件基本都存在于后端,在前端很少有相应的组织。
从协作关系上讲,很多前端开发团队每个成员的职责不是很清晰,有了前端的 MV框架,这个状况会大有改观。
之所以感受不到 MV*框架的重要性,是因为Model部分代码较少,View的相对多一些。如果主要在操作View和Controller,那当然 jQuery 这类库比较好用了。
参考《前端 MV*框架的意义》
1.2、请简述盒模型
IE6盒子模型与W3C盒子模型。
文档中的每个元素被描绘为矩形盒子。盒子有四个边界:外边距边界margin, 边框边界border, 内边距边界padding与内容边界content。
CSS3中有个box-sizing属性可以控制盒子的计算方式,
content-box:padding和border不被包含在定义的width和height之内。对象的实际宽度等于设置的width值和border、padding之和。(W3C盒子模型)
border-box:padding和border被包含在定义的width和height之内。对象的实际宽度就等于设置的width值。(IE6盒子模型)
参考《盒模型》
1.3、请你谈谈Cookie的弊端
a. 每个特定的域名下最多生成的cookie个数有限制
b. IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie
c. cookie的最大大约为4096字节,为了兼容性,一般不能超过4095字节
d. 安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。
1.4、浏览器本地存储
在HTML5中提供了sessionStorage和localStorage。
sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁,是会话级别的存储。
localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
1.5、web storage和cookie的区别
a. Cookie的大小是受限的
b. 每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽
c. cookie还需要指定作用域,不可以跨域调用
d. Web Storage拥有setItem,getItem等方法,cookie需要前端开发者自己封装setCookie,getCookie
e. Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生
f. IE7、IE6中的UserData通过简单的代码封装可以统一到所有的浏览器都支持web storage
1.6、对BFC规范的理解
BFC全称是Block Formatting Context,即块格式化上下文。它是CSS2.1规范定义的,关于CSS渲染定位的一个概念。
BFC是页面CSS 视觉渲染的一部分,用于决定块盒子的布局及浮动相互影响范围的一个区域。
BFC的一个最重要的效果是,让处于BFC内部的元素与外部的元素相互隔离,使内外元素的定位不会相互影响。
利用BFC可以闭合浮动,防止与浮动元素重叠。
参考《Learning BFC》
1.7、线程与进程的区别
a. 一个程序至少有一个进程,一个进程至少有一个线程
b. 线程的划分尺度小于进程,使得多线程程序的并发性高
c. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
d. 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
e. 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配
1.8、你都使用哪些工具来测试代码的性能?
JSPerf, Dromaeo
1.9、你遇到过比较难的技术问题是?你是如何解决的?
1.10、常使用的库有哪些?常用的前端开发工具?开发过什么应用或组件?
1.11、列举IE与其他浏览器不一样的特性?
a. IE的排版引擎是Trident (又称为MSHTML)
b. Trident内核曾经几乎与W3C标准脱节(2005年)
c. Trident内核的大量 Bug等安全性问题没有得到及时解决
d. JS方面,有很多独立的方法,例如绑定事件的attachEvent、创建事件的createEventObject等
e. CSS方面,也有自己独有的处理方式,例如设置透明,低版本IE中使用滤镜的方式
参考《Trident(排版引擎)》
1.12、什么叫优雅降级和渐进增强?
渐进增强 progressive enhancement:
针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
优雅降级 graceful degradation:
一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
区别:
a. 优雅降级是从复杂的现状开始,并试图减少用户体验的供给
b. 渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要
c. 降级(功能衰减)意味着往回看;而渐进增强则意味着朝前看,同时保证其根基处于安全地带
参考《优雅降级和渐进增强的区别》
1.13、WEB应用从服务器主动推送Data到客户端有那些方式?
a. html5 websoket
b. WebSocket 通过 Flash
c. XHR长时间连接
d. XHR Multipart Streaming
e. 不可见的Iframe
f. <script>标签的长时间连接(可跨域)
1.14、对前端界面工程师这个职位是怎么样理解的?
a. 前端是最贴近用户的程序员,前端的能力就是能让产品从 90分进化到 100 分,甚至更好
b. 参与项目,快速高质量完成实现效果图,精确到1px;
c. 与团队成员,UI设计,产品经理的沟通;
d. 做好的页面结构,页面重构和用户体验;
e. 处理hack,兼容、写出优美的代码格式;
f. 针对服务器的优化、拥抱最新前端技术。
1.15、你在现在的团队处于什么样的角色,起到了什么明显的作用?
1.16、你的优点是什么?缺点是什么?
1.17、如何管理前端团队?
1.18、最近在学什么?能谈谈你未来3,5年给自己的规划吗?
1.19、平时如何管理你的项目?
a. 先期团队必须确定好全局样式(globe.css),编码模式(utf-8) 等;
b. 编写习惯必须一致(例如都是采用继承式的写法,单样式都写成一行);
c. 标注样式编写人,各模块都及时标注(标注关键样式调用的地方);
d. 页面进行标注(例如 页面 模块 开始和结束);
e. CSS跟HTML 分文件夹并行存放,命名都得统一(例如style.css);
f. JS 分文件夹存放 命名以该JS功能为准的英文翻译。
g. 图片采用整合的 images.png png8 格式文件使用 尽量整合在一起使用方便将来的管理
1.20、说说最近最流行的一些东西吧?常去哪些网站?
CSDN、SegmentFault、php.net、MDN、css参考手册、iconfont、
underscore、github、Bootstrap、W3Shool、W3Cplus、caniuse
1.21、Flash、Ajax各自的优缺点,在使用中如何取舍?
Flash:
a. Flash适合处理多媒体、矢量图形、访问机器
b. 对CSS、处理文本上不足,不容易被搜索
Ajax:
a. Ajax对CSS、文本支持很好,支持搜索
b. 多媒体、矢量图形、机器访问不足
共同点:
a. 与服务器的无刷新传递消息
b. 可以检测用户离线和在线状态
c. 操作DOM
1.22、请解释一下 JavaScript 的同源策略
同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议。
指一段脚本只能读取来自同一来源的窗口和文档的属性。
1.23、AMD和CMD 规范的区别?
AMD 提前执行依赖 - 尽早执行,requireJS 是它的实现
CMD 按需执行依赖 - 懒执行,seaJS 是它的实现
参考《SeaJS与RequireJS最大的区别》、《与 RequireJS 的异同》
1.24、网站重构的理解
重构:在不改变外部行为的前提下,简化结构、添加可读性,而在网站前端保持一致的行为。
a. 使网站前端兼容于现代浏览器(针对于不合规范的CSS、如对IE6有效的)
b. 对于移动平台的优化,针对于SEO进行优化
c. 减少代码间的耦合,让代码保持弹性
d. 压缩或合并JS、CSS、image等前端资源
1.25、浏览器的内核分别是什么?
IE浏览器的内核Trident、Mozilla的Gecko、Chrome的Blink(WebKit的分支)、Opera内核原为Presto,现为Blink;
1.26、前端页面有哪三层构成,分别是什么?作用是什么?
a. 结构层:由 HTML 或 XHTML 之类的标记语言负责创建,仅负责语义的表达。解决了页面“内容是什么”的问题。
b. 表示层:由CSS负责创建,解决了页面“如何显示内容”的问题。
c. 行为层:由脚本负责。解决了页面上“内容应该如何对事件作出反应”的问题。
1.27、知道的网页制作会用到的图片格式有哪些?
png-8,png-24,jpeg,gif,svg
Webp:谷歌(google)开发的一种旨在加快图片加载速度的图片格式。图片压缩体积大约只有JPEG的2/3,并能节省大量的服务器带宽资源和数据空间。Facebook Ebay等知名网站已经开始测试并使用WebP格式。
Apng:全称是“Animated Portable Network Graphics”, 是PNG的位图动画扩展,可以实现png格式的动态图片效果。04年诞生,但一直得不到各大浏览器厂商的支持,直到日前得到 iOS safari 8的支持,有望代替GIF成为下一代动态图标准。
二、HTML
2.1、<img>标签上title属性与alt属性的区别是什么?
alt属性是为了给那些不能看到你文档中图像的浏览者提供文字说明的。且长度必须少于100个英文字符或者用户必须保证替换文字尽可能的短。
这包括那些使用本来就不支持图像显示或者图像显示被关闭的浏览器的用户,视觉障碍的用户和使用屏幕阅读器的用户等。
title属性为设置该属性的元素提供建议性的信息。使用title属性提供非本质的额外信息。参考《alt和title属性的区别及应用》
2.2、分别写出以下几个HTML标签:文字加粗、下标、居中、字体
加粗:<b>、<strong>
下标:<sub>
居中:<center>
字体:<font>、<basefont>、参考《HTML标签列表》
2.3、请写出至少5个html5新增的标签,并说明其语义和应用场景
section:定义文档中的一个章节
nav:定义只包含导航链接的章节
header:定义页面或章节的头部。它经常包含 logo、页面标题和导航性的目录。
footer:定义页面或章节的尾部。它经常包含版权信息、法律信息链接和反馈建议用的地址。
aside:定义和页面内容关联度较低的内容——如果被删除,剩下的内容仍然很合理。
参考《HTML5 标签列表》
2.4、请说说你对标签语义化的理解?
a. 去掉或者丢失样式的时候能够让页面呈现出清晰的结构
b. 有利于SEO:和搜索引擎建立良好沟通,有助于爬虫抓取更多的有效信息:爬虫依赖于标签来确定上下文和各个关键字的权重;
c. 方便其他设备解析(如屏幕阅读器、盲人阅读器、移动设备)以意义的方式来渲染网页;
d. 便于团队开发和维护,语义化更具可读性,遵循W3C标准的团队都遵循这个标准,可以减少差异化。
2.5、Doctype作用? 严格模式与混杂模式如何区分?它们有何意义?
<!DOCTYPE> 声明位于文档中的最前面,处于 <html> 标签之前。告知浏览器以何种模式来渲染文档。
严格模式的排版和 JS 运作模式是,以该浏览器支持的最高标准运行。
在混杂模式中,页面以宽松的向后兼容的方式显示。模拟老式浏览器的行为以防止站点无法工作。
DOCTYPE不存在或格式不正确会导致文档以混杂模式呈现。
2.6、你知道多少种Doctype文档类型?
标签可声明三种 DTD 类型,分别表示严格版本、过渡版本以及基于框架的 HTML 文档。
HTML 4.01 规定了三种文档类型:Strict、Transitional 以及 Frameset。
XHTML 1.0 规定了三种 XML 文档类型:Strict、Transitional 以及 Frameset。
Standards (标准)模式(也就是严格呈现模式)用于呈现遵循最新标准的网页,
Quirks(包容)模式(也就是松散呈现模式或者兼容模式)用于呈现为传统浏览器而设计的网页。
2.7、HTML与XHTML——二者有什么区别
a. XHTML 元素必须被正确地嵌套。
b. XHTML 元素必须被关闭。
c. 标签名必须用小写字母。
d. XHTML 文档必须拥有根元素。
参考《XHTML 与 HTML 之间的差异》
2.8、html5有哪些新特性、移除了那些元素?
a. HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加。
b. 拖拽释放(Drag and drop) API
c. 语义化更好的内容标签(header,nav,footer,aside,article,section)
d. 音频、视频API(audio,video)
e. 画布(Canvas) API
f. 地理(Geolocation) API
g. 本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失
h. sessionStorage 的数据在页面会话结束时会被清除
i. 表单控件,calendar、date、time、email、url、search
j. 新的技术webworker, websocket等
移除的元素:
a. 纯表现的元素:basefont,big,center, s,strike,tt,u;
b. 对可用性产生负面影响的元素:frame,frameset,noframes;
2.9、iframe的优缺点?
优点:
a. 解决加载缓慢的第三方内容如图标和广告等的加载问题
b. iframe无刷新文件上传
c. iframe跨域通信
缺点:
a. iframe会阻塞主页面的Onload事件
b. 无法被一些搜索引擎索引到
c. 影响浏览器中的并行资源下载,iframe和父页面不能共享下载
d. script脚本标签写在了iframe之前,iframe中的资源会被阻塞
参考《iframe的一些记录》《实践一下前端性能分析》
2.10、Quirks模式是什么?它和Standards模式有什么区别?
在写程序时我们也会经常遇到这样的问题,如何保证原来的接口不变,又提供更强大的功能,尤其是新功能不兼容旧功能时。IE6以前的页面大家都不会去写DTD,所以IE6就假定 如果写了DTD,就意味着这个页面将采用对CSS支持更好的布局,而如果没有,则采用兼容之前的布局方式。这就是Quirks模式(怪癖模式,诡异模式,怪异模式)。
区别:总体会有布局、样式解析和脚本执行三个方面的区别。
a. 盒模型:在W3C标准中,如果设置一个元素的宽度和高度,指的是元素内容的宽度和高度,而在Quirks 模式下,IE的宽度和高度还包含了padding和border。
b. 设置行内元素的高宽:在Standards模式下,给<span>等行内元素设置wdith和height都不会生效,而在quirks模式下,则会生效。
c. 设置百分比的高度:在standards模式下,一个元素的高度是由其包含的内容来决定的,如果父元素没有设置百分比的高度,子元素设置一个百分比的高度是无效的用
d. 设置水平居中:使用margin:0 auto在standards模式下可以使元素水平居中,但在quirks模式下却会失效。
2.11、请阐述table的缺点
a. 太深的嵌套,比如table>tr>td>h3,会导致搜索引擎读取困难,而且,最直接的损失就是大大增加了冗余代码量。
b. 灵活性差,比如要将tr设置border等属性,是不行的,得通过td
c. 代码臃肿,当在table中套用table的时候,阅读代码会显得异常混乱
d. 混乱的colspan与rowspan,用来布局时,频繁使用他们会造成整个文档顺序混乱。
e. table需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。
f. 不够语义
参考《为什么说table表格布局不好?》
2.12、简述一下src与href的区别
src用于替换当前元素;href用于在当前文档和引用资源之间确立联系。
src是source的缩写,指向外部资源的位置,指向的内容将会嵌入到文档中当前标签所在位置
href是Hypertext Reference的缩写,指向网络资源所在位置,建立和当前元素(锚点)或当前文档(链接)之间的链接
三、CSS
3.1、谈谈你对CSS布局的理解
3.2、请列举几种可以清除浮动的方法(至少两种)
浮动会漂浮于普通流之上,像浮云一样,但是只能左右浮动。正是这种特性,导致框内部由于不存在其他普通流元素了,表现出高度为0(高度塌陷)。
a. 添加额外标签,例如<div style="clear:both"></div>
b. 使用br标签和其自身的html属性,例如<br clear="all" />
c. 父元素设置 overflow:hidden;在IE6中还需要触发hasLayout,例如zoom:1;
d. 父元素设置 overflow:auto 属性;同样IE6需要触发hasLayout
e. 父元素也设置浮动
f. 父元素设置display:table
g. 使用:after 伪元素;由于IE6-7不支持:after,使用 zoom:1触发 hasLayout。
在CSS2.1里面有一个很重要的概念,那就是 Block formatting contexts (块级格式化上下文),简称 BFC。
创建了BFC的元素就是一个独立的盒子,里面的子元素不会在布局上影响外面的元素,同时BFC仍然属于文档中的普通流。
IE6-7的显示引擎使用的是一个称为布局(layout)的内部概念。
参考《那些年我们一起清除过的浮动》
3.3、请列举几种隐藏元素的方法
a. visibility: hidden;这个属性只是简单的隐藏某个元素,但是元素占用的空间任然存在。
b. opacity: 0;一个CSS3属性,设置0可以使一个元素完全透明,制作出和visibility一样的效果。与visibility相比,它可以被transition和animate
c. position: absolute;使元素脱离文档流,处于普通文档之上,给它设置一个很大的left负值定位,使元素定位在可见区域之外。
d. display: none;元素会变得不可见,并且不会再占用文档的空间。
e. transform: scale(0);将一个元素设置为无限小,这个元素将不可见。这个元素原来所在的位置将被保留。
f. HTML5 hidden attribute;hidden属性的效果和display:none;相同,这个属性用于记录一个元素的状态
g. height: 0; overflow: hidden;将元素在垂直方向上收缩为0,使元素消失。只要元素没有可见的边框,该技术就可以正常工作。
h. filter: blur(0);将一个元素的模糊度设置为0,从而使这个元素“消失”在页面中。
参考《使用CSS隐藏HTML元素的4种常用方法》《通过HTML和CSS隐藏和显示元素的4种方法》
3.4、如何让一段文本中的所有英文单词的首字母大写
text-transform:
none| capitalize(将每个单词的第一个字母转换成大写) | uppercase(将每个单词转换成大写 ) | lowercase(将每个单词转换成小写 )
3.5、请简述CSS样式表继承
CSS样式表继承指的是,特定的CSS属性向下传递到子孙元素。会被继承下去的属性如下:参考《CSS样式表继承详解》
文本相关:font-family,font-size, font-style,font-variant,font-weight, font,letter-spacing,line-height,color
列表相关:list-style-image,list-style-position,list-style-type, list-style
3.6、请简述CSS的选择器
元素选择器:* 、E、 E#id、 E.class
关系选择器:E、F、E>F、E+F、E~F
属性选择器:E[att]、E[att="val"]、E[att~="val"]、E[att^="val"]、E[att$="val"]、E[att*="val"]、E[att|="val"]
伪类选择器:E:link、E:visited、E:hover、E:active、E:focus、E:lang(fr)、E:not(s)、E:root、E:first-child、E:last-chil等
伪对象选择器:E:first-letter/E::first-letter、E:first-line/E::first-line、E:before/E::before、E:after/E::after、E::selection
参考《选择符列表》
3.7、CSS伪类与CSS伪对象的区别
CSS 引入伪类和伪元素的概念是为了描述一些现有CSS无法描述的东西
根本区别在于:它们是否创造了新的元素(抽象)
伪类:一开始用来表示一些元素的动态状态,随后CSS2标准扩展了其概念范围,使其成为了所有逻辑上存在但在文档树中却无须标识的“幽灵”分类
伪对象:代表了某个元素的子元素,这个子元素虽然在逻辑上存在,但却并不实际存在于文档树中
参考《CSS伪类与CSS伪元素的区别及由来》
3.8、请简述CSS的权重规则
一个行内样式+1000,一个id+100,一个属性选择器/class类/伪类选择器+10,一个元素名/伪对象选择器+1。
关系选择器将拆分为两个选择器再计算。参考《CSS权重》
3.9、请写出多种等高布局
a. 假等高列:使用背景图片,在列的父元素上使用这个背景图进行Y轴的铺放,从而实现一种等高列的假像
b. 给容器div使用单独的背景色(固定布局)(流体布局):用<div>元素中的最大高度撑大其他的<div>容器高度
c. 创建带边框的两列等高布局:用border-left来做,只能使用两列。
d. 使用正padding和负margin对冲实现多列布局方法:在所有列中使用正的上、下padding和负的上、下margin,并在所有列外面加上一个容器,设置overflow:hiden把溢出背景切掉
e. 使用边框和定位模拟列等高:但不能使用在多列
f. 模仿表格布局等高列效果:兼容性不好,在ie6-7无法正常运行
3.10、在CSS样式中常使用px、em,各有什么优劣,在表现上有什么区别?
px是相对长度单位,相对于显示器屏幕分辨率而言的。
em是相对长度单位,相对于当前对象内文本的字体尺寸。
px定义的字体,无法用浏览器字体放大功能。
em的值并不是固定的,会继承父级元素的字体大小,1 ÷ 父元素的font-size × 需要转换的像素值 = em值。
3.11、CSS中 link 和@import 的区别是什么?
a. link属于HTML标签,而@import是CSS提供的,且只能加载 CSS
b. 页面被加载时,link会同时被加载,而@import引用的CSS会等到页面被加载完再加载
c. import只在IE5以上才能识别,而link是HTML标签,无兼容问题
d. link方式的样式的权重 高于@import的权重
e. 当使用 Javascript 控制 DOM 去改变样式的时候,只能使用 link 方式,因为 @import 眼里只有 CSS ,不是 DOM 可以控制
3.12、position的absolute与fixed共同点与不同点
相同:
a. 改变行内元素的呈现方式,display被置为block
b. 让元素脱离普通流,不占据空间
c. 默认会覆盖到非定位元素上
区别:
absolute的”根元素“是可以设置的,而fixed的”根元素“固定为浏览器窗口。
当你滚动网页,fixed元素与浏览器窗口之间的距离是不变的。
3.13、position的值, relative和absolute分别是相对于谁进行定位的?
absolute:生成绝对定位的元素,相对于 static 定位以外的第一个祖先元素进行定位
fixed:生成绝对定位的元素,相对于浏览器窗口进行定位。 (IE6不支持)
relative:生成相对定位的元素,相对于其在普通流中的位置进行定位
static:默认值。没有定位,元素出现在正常的流中
3.14、CSS3有哪些新特性?
CSS3实现圆角(border-radius),阴影(box-shadow),对文字加特效(text-shadow),线性渐变(gradient),变形(transform)
增加了更多的CSS选择器 多背景 rgba,在CSS3中唯一引入的伪元素是::selection,媒体查询,多栏布局
参考《CSS3中的动画效果记录》、《CSS3中border-radius、box-shadow与gradient那点事儿》
3.15、为什么要初始化CSS样式?
因为浏览器的兼容问题,不同浏览器对有些标签的默认值是不同的,如果没对CSS初始化往往会出现浏览器之间的页面显示差异。
当然,初始化样式会对SEO有一定的影响,但鱼和熊掌不可兼得,但力求影响最小的情况下初始化。
3.16、解释下 CSS sprites原理,优缺点
CSS Sprites其实就是把网页中一些背景图片整合到一张图片文件中,
再利用CSS的“background-image”,“background- repeat”,“background-position”的组合进行背景定位,
background-position可以用数字精确的定位出背景图片的位置。
优点:
a. 减少网页的http请求
b. 减少图片的字节
c. 解决了网页设计师在图片命名上的困扰,只需对一张集合的图片上命名就可以了,不需要对每一个小元素进行命名
d. 更换风格方便,只需要在一张或少张图片上修改图片的颜色或样式,整个网页的风格就可以改变。
缺点:
a. 在宽屏,高分辨率的屏幕下的自适应页面,你的图片如果不够宽,很容易出现背景断裂
b. CSS Sprites在开发的时候,要通过photoshop或其他工具测量计算每一个背景单元的精确位置
c. 在维护的时候比较麻烦,如果页面背景有少许改动,一般就要改这张合并的图片
3.17、解释下浮动和它的工作原理?
a. 浮动元素脱离文档流,不占据空间(引起“高度塌陷”现象)
b. 浮动元素碰到包含它的边框或者浮动元素的边框停留。
3.18、浮动元素引起的问题
a. 父元素的高度无法被撑开,影响与父元素同级的元素
b. 与浮动元素同级的非浮动元素会跟随其后
c. 若非第一个元素浮动,则该元素之前的元素也需要浮动,否则会影响页面显示的结构
3.19、什么是 FOUC(无样式内容闪烁)?你如何来避免 FOUC?
如果使用import方法对CSS进行导入,会导致某些页面在Windows下的IE出现一些奇怪的现象:
以无样式显示页面内容的瞬间闪烁,这种现象称之为文档样式短暂失效(Flash of Unstyled Content),简称为FOUC。
原理:当样式表晚于结构性html加载,当加载到此样式表时,页面将停止之前的渲染。此样式表被下载和解析后,将重新渲染页面,也就出现了短暂的花屏现象。
解决方法:使用LINK标签将样式表放在文档HEAD中。
3.20、line-height三种赋值方式有何区别?(带单位、纯数字、百分比)
带单位:px不用计算,em则会使元素以其父元素font-size值为参考来计算自己的行高
纯数字:把比例传递给后代,例如父级行高为1.5,子元素字体为18px,则子元素行高为1.5*18=27px
百分比:将计算后的值传递给后代
参考《line-height的理解》、《浅析line-height和vertical》,查看在线源码。
3.21、:link、:visited、:hover、:active的执行顺序是怎么样的?
L-V-H-A,l(link)ov(visited)e h(hover)a(active)te,即用喜欢和讨厌两个词来概括
3.22、经常遇到的浏览器兼容性有哪些?如何解决?
a. 浏览器默认的margin和padding不同
b. IE6双边距bug
c. 在ie6,ie7中元素高度超出自己设置高度。原因是IE8以前的浏览器中会给元素设置默认的行高的高度导致的
d. min-height在IE6下不起作用
e. 透明性IE用filter:Alpha(Opacity=60),而其他主流浏览器用 opacity:0.6
f. input边框问题,去掉input边框一般用border:none;就可以,但由于IE6在解析input样式时的BUG(优先级问题),在IE6下无效
3.23、有哪项方式可以对一个DOM设置它的CSS样式?
a. 外部样式表:通过<link>标签引入一个外部css文件
b. 内部样式表:将css代码放在 <style> 标签内部
c. 内联样式:将css样式直接定义在 HTML 元素内部
3.24、什么是外边距重叠?重叠的结果是什么?
外边距重叠就是margin-collapse。
在CSS当中,相邻的两个盒子(可能是兄弟关系也可能是祖先关系)的外边距可以结合成一个单独的外边距。这种合并外边距的方式被称为折叠,并且因而所结合成的外边距称为折叠外边距。
折叠结果遵循下列计算规则:
a. 两个相邻的外边距都是正数时,折叠结果是它们两者之间较大的值。
b. 两个相邻的外边距都是负数时,折叠结果是两者绝对值的较大值。
c. 两个外边距一正一负时,折叠结果是两者的相加的和。
3.25、rgba()和opacity的透明效果有什么不同?
a. opacity作用于元素,以及元素内的所有内容的透明度,rgba()只作用于元素的颜色或其背景色。
b. 设置rgba透明的元素的子元素不会继承透明效果!
3.26、css属性content有什么作用?有什么应用?
css的content属性专门应用在 before/after 伪元素上,用于来插入生成内容。
可以配合自定义字体显示特殊符号。
四、JavaScript
4.1、请解释一下什么是闭包
当函数可以记住并访问所在的作用域时,就产生了闭包,即使函数是在当前作用域之外执行。闭包有如下特性:
a. JavaScript允许你使用在当前函数以外定义的变量
b. 即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量
c. 闭包可以更新外部变量的值
d. 用闭包模拟私有方法
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
参考《作用域、提升与闭包》《作用域、提升与闭包》
4.2、call 和 apply 的区别是什么?
call 和 apply 就是为了改变函数体内部 this 的指向。
区别是从第二个参数起,call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。
当参数明确时用call与apply都行, 当参数不明确时可用apply给合arguments
4.3、如何使用原生 Javascript 代码深度克隆一个对象(注意区分对象类型)
在网上找了个函数,用递归的方式做复制。传入的参数必须得是Array或Object。
并且用到了JSON.stringify和JSON.parse。查看在线代码。参考《JavaScript中的对象克隆》
4.4、 jQuery中 $(′.class′)和$('div.class') 哪个效率更高?
jQuery内部使用Sizzle引擎,处理各种选择器。Sizzle引擎的选择顺序是从右到左,所以这条语句是先选.class,
第二个会直接过滤出div标签,而第一个就不会过滤了,将所有相关标签都列出。参考《jQuery最佳实践》
4.5、实现输出document对象中所有成员的名称和类型
用一个for in方式循环document,然后在将内容console出来,
就是看到篇文章还会判断document.hasOwnProperty,然后再做打印,我测试了下这样的话打印不出来。
查看在线代码。参考《JavaScript要点归档:DOM》
4.6、获得一个DOM元素的绝对位置
offsetTop:返回当前元素相对于其 offsetParent 元素的顶部的距离
offsetLeft:返回当前元素相对于其 offsetParent 元素的左边的距离
getBoundingClientRect():返回值是一个DOMRect对象,它包含了一组用于描述边框的只读属性——left、top、right和bottom,属性单位为像素
参考《JavaScript中尺寸、坐标》,查看在线代码。
4.7、如何利用JS生成一个table?
首先是用createElement创建一个table,再用setAttribute设置table的属性,
然后用for循环设置tr和td的内容,用appendChild拼接内容,设置td的时候还用到innerHTML和style.padding。
查看在线代码。参考《JavaScript要点归档:DOM表格》《JavaScript要点归档:DOM》
4.8、实现预加载一张图片,加载完成后显示在网页中并设定其高度为50px,宽度为50px
先new Image()获取一个图片对象,然后在图片对象的onload中设置宽度和高度。查看在线代码。
4.9、假设有一个4行tr的table,将table里面tr顺序颠倒
先是通过table.tBodies[0].rows获取到当前tbody中的行,接下来是两种方法处理。获取到的行没有reverse这个方法。
第一种是将这些行push到另外一个数组中
第二种是用Array.prototype.slice.call()将那些行变成数组,
接着用reverse倒叙,table再appendChild。查看在线代码。
这里我有个疑问,就是在appendChild的时候,并不是在最后把列加上,而是做了替换操作?
4.10、模拟一个HashTable类,一个类上注册四个方法:包含有add、remove、contains、length方法
先是在构造函数中定义一个数组,然后用push模拟add,splice模拟remove。
四个方法都放在了prototype上面。查看在线代码。
4.11、Ajax读取一个XML文档并进行解析的实例
a. 初始化一个HTTP请求,IE以ActiveX对象引入。 后来标准浏览器提供了XMLHttpRequest类,它支持ActiveX对象所提供的方法和属性
b. 发送请求,可以调用HTTP请求类的open()和send()方法
c. 处理服务器的响应,通过http_request.onreadystatechange = nameOfTheFunction。来指定函数
参考《AJAX》《开始AJAX》,查看在线代码。
4.12、JS如何实现面向对象和继承机制?
创建对象方法:
a. 利用json创建对象
b. 使用JavaScript中的Object类型
c. 通过创建函数来生成对象
继承机制:
a. 构造函数绑定,使用call或apply方法,将父对象的构造函数绑定在子对象上
b. prototype模式,继承new函数的模式
c. 直接继承函数的prototype属性,对b的一种改进
d. 利用空对象作为中介
e. 在ECMAScript5中定义了一个新方法Object.create(),用于创建一个新方法
f. 拷贝继承,把父对象的所有属性和方法,拷贝进子对象,实现继承。参考《JavaScript中的对象克隆》
参考《Javascript继承机制的设计思想》《构造函数的继承》,查看在线代码。
4.13、JS模块的封装方法,比如怎样实现私有变量,不能直接赋值,只能通过公有方法
a. 通过json生成对象的原始模式,多写几个就会非常麻烦,也不能反映出它们是同一个原型对象的实例
b. 原始模式的改进,可以写一个函数,解决代码重复的问题。同样不能反映出它们是同一个原型对象的实例
c. 构造函数模式,就是一个普通函数,不过内部使用了this变量,但是存在一个浪费内存的问题。
d. Prototype模式,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承,可以把那些不变的属性和方法,直接定义在prototype对象上。Prototype模式的验证方法:isPrototypeOf()、hasOwnProperty()和in运算符。
参考《封装》,查看在线代码。
4.14、对this指针的理解,可以列举几种使用情况?
this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
this指的是:调用函数的那个对象。
a. 纯粹的函数调用,属于全局性调用,因此this就代表全局对象Global。
b. 作为对象方法的调用,这时this就指这个上级对象。
c. 作为构造函数调用,就是通过这个函数new一个新对象(object)。这时,this就指这个新对象。
d. apply与call的调用,它们的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。
参考《Javascript的this用法》《this》,查看在线代码。
4.15、在JavaScript中,常用的绑定事件的方法有哪些?
a. 在DOM元素中直接绑定,DOM元素,可以理解为HTML标签,onXXX="JavaScript Code",查看事件列表。
b. 在JavaScript代码中绑定,elementObject.onXXX=function(){},通称为DOM0事件系统。
c. 绑定事件监听函数,标准浏览器使用 addEventListener() ,IE11以下版本attachEvent() 来绑定事件监听函数,通称为DOM2事件系统。
参考《JavaScript绑定事件的方法》《JavaScript中事件处理》
4.16、解释下javascript的冒泡和捕获
<div id="click1">
<div id="click2">
<div id="click3">事件</div>
</div>
</div>
a. Netscape主张元素1的事件首先发生,这种事件发生顺序被称为捕获型
b. 微软则保持元素3具有优先权,这种事件顺序被称为冒泡型
c. W3C选择了一个择中的方案。任何发生在w3c事件模型中的事件,首是进入捕获阶段,直到达到目标元素,再进入冒泡阶段
事件监听函数addEventListener()的第三个参数就是控制方法是捕获还是冒泡
参考《事件》、《javascript的冒泡和捕获》,查看在线代码。
4.17、jQuery的特点
a. 一款轻量级的js库
b. 丰富快速的DOM选择器
c. 链式表达式
d. 事件、样式、动画等特效支持
e. Ajax操作封装,支持跨域
f. 跨浏览器兼容
g. 插件扩展开发
参考《JQuery特点、优缺点及其常用操作》
4.18、Ajax有哪些好处和弊端?
优点:
a. 无刷新更新数据
b. 异步与服务器通信
c. 前端和后端负载平衡
d. 基于标准被广泛支持
e. 界面与应用分离
缺点:
a. AJAX干掉了Back和History功能,即对浏览器机制的破坏
b. AJAX的安全问题
c. 对搜索引擎支持较弱
d. 违背URL和资源定位的初衷
参考《AJAX工作原理及其优缺点》
4.19、null和undefined的区别?
null:
a. null是一个表示"无"的对象,转为数值时为0
b. null表示"没有对象",即该处不应该有值。
undefined:
a. undefined是一个表示"无"的原始值,转为数值时为NaN。
b. undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。
参考《undefined与null的区别》
4.20、new操作符具体干了什么呢?
a. 一个新对象被创建。它继承自函数原型
b. 构造函数被执行。执行的时候,相应的传参会被传入
c. 上下文(this)会被指定为这个新实例
d. 如果构造函数返回了一个“对象”,那么这个对象会取代整个new出来的结果
参考《new运算符》
4.21、js延迟加载的方式有哪些?
a. 将script节点放置在最后</body>之前
b. 使用script标签的defer和async属性,defer属性为延迟加载,是在页面渲染完成之后再进行加载的,而async属性则是和文档并行加载
c. 通过监听onload事件,动态添加script节点
d. 通过ajax下载js脚本,动态添加script节点
参考《javascript延迟加载方式》
4.22、如何解决跨域问题?
a. JSONP(JSON with Padding),填充式JSON
b. iframe跨域
c. HTML5的window.postMessage方法跨域
d. 通过设置img的src属性,进行跨域请求
e. 跨域资源共享(CORS),服务器设置Access-Control-Allow-OriginHTTP响应头之后,浏览器将会允许跨域请求
4.23、documen.write和 innerHTML的区别
write:
a. 改变 HTML 输出流
b. 当在文档加载之后使用 document.write(),这会覆盖该文档。例如onload事件中
c. 输入css的style标签能改变样式,例如document.write("<style>b{color:red;font-weight:bold;}</style>");
innerHTML:
a. 改变 HTML 内容
b. 输入css的style标签不能改变样式。也是能改变样式的
参考《JavaScript HTML DOM - 改变 HTML》
4.24、哪些操作会造成内存泄漏?
a. 当页面中元素被移除或替换时,若元素绑定的事件仍没被移除,在IE中不会作出恰当处理,此时要先手工移除事件,不然会存在内存泄露。
b. 在IE中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会处理。
c. 闭包可以维持函数内局部变量,使其得不到释放。
d. 在销毁对象的时候,要遍历属性中属性,依次删除,否则会泄漏。
参考《js内存泄漏的几种情况》、《JavaScript内存分析》
4.25、JavaScript中的变量声明提升?
函数声明和变量声明总是被JavaScript解释器隐式地提升到包含他们的作用域的最顶端。
function优先声明于var。
函数表达式中只会提升名称,函数体只有在执行到赋值语句时才会被赋值。
function foo() {
bar();
var x = 1;
}
function foo() {//等同于
var x;
bar();
x = 1;
}
function test() {
foo(); // TypeError "foo is not a function"
bar(); // "this will run!"
var foo = function () { }// 函数表达式被赋值给变量'foo'
function bar() { }// 名为'bar'的函数声明
}
4.26、如何判断当前脚本运行在浏览器还是node环境中?
通过判断Global对象是否为window,如果是window,当前脚本运行在浏览器中
4.27、什么是 "use strict"
ECMAscript 5添加了第二种运行模式:"严格模式"(strict mode)
设立"严格模式"的目的,主要有以下几个:
a. 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
b. 消除代码运行的一些不安全之处,保证代码运行的安全;
c. 提高编译器效率,增加运行速度;
d. 为未来新版本的Javascript做好铺垫。
注:经过测试IE6,7,8,9均不支持严格模式
参考《Javascript 严格模式详解》
4.28、eval是做什么的?
eval()函数可计算某个字符串,并执行其中的的 JavaScript 代码。
eval()是一个顶级函数并且跟任何对象无关。
如果字符串表示了一个表达式,eval()会对表达式求值。如果参数表示了一个或多个JavaScript声明, 那么eval()会执行声明。
4.29、JavaScript原型,原型链 ?
原型:
a. 原型是一个对象,其他对象可以通过它实现属性继承。
b. 一个对象的真正原型是被对象内部的[[Prototype]]属性(property)所持有。浏览器支持非标准的访问器__proto__。
c. 在javascript中,一个对象就是任何无序键值对的集合,如果它不是一个主数据类型(undefined,null,boolean,number,string),那它就是一个对象。
原型链:
a. 因为每个对象和原型都有一个原型(注:原型也是一个对象),对象的原型指向对象的父,而父的原型又指向父的父,我们把这种通过原型层层连接起来的关系称为原型链。
b. 这条链的末端一般总是默认的对象原型。
a.__proto__ = b;
b.__proto__ = c;
c.__proto__ = {}; //default object
{}.__proto__.__proto__; //null
参考《理解JavaScript原型》《原型》
4.30、画出此对象的内存图
查看在线代码。
4.31、JQuery与jQuery UI 有啥区别?
jQuery是一个js库,主要提供的功能是选择器,属性修改和事件绑定等等。
jQuery UI则是在jQuery的基础上,利用jQuery的扩展性,设计的插件。提供了一些常用的界面元素,诸如对话框、拖动行为、改变大小行为等等
4.32、jQuery的源码看过吗?能不能简单说一下它的实现原理?
jQuery给我们带来了一个简洁方便的编码模型(1>创建jQuery对象;2>直接使用jQuery对象的属性/方法/事件),
一个强悍的dom元素查找器($),插件式编程接口(jQuery.fn),以及插件初始化的”配置”对象思想
参考《jQuery工作原理解析以及源代码示例》《jQuery系列分析》
4.33、jQuery 中如何将数组转化为json字符串
在jQuery1.8.3中有个方法“parseJSON”,在这个方法中会做从string转换为json。
如果当前浏览器支持window.JSON,那就直接调用这个对象中的方法。
如果没有就使用( new Function( "return " + data ) )();执行代码返回。
eval和new Function是有区别的。
4.34、请写出console.log中的内容
1 var msg = 'hello';//顶级作用域window下有个变量msg
2 function great(name, attr) {
3 var name = 'david';
4 var greating = msg + name + '!';
5 var msg = '你好';
6 for (var i = 0; i < 10; i++) {
7 var next = msg + '你的id是' + i * 2 + i;
8 }
9 console.log(arguments[0]);
10 console.log(arguments[1]);
11 console.log(greating);
12 console.log(next);
13 }
14 great('Tom')
查看在线代码。
a. arguments[0]被覆盖了
b. msg出现了声明提升,可以查看4.25的例子
c. next中出现了隐式的类型转换
4.35、请说明下下面代码的执行过程
1 var t=true;
2 window.setTimeout(function(){
3 t=false;
4 },1000);
5 while(t){
6 console.log(1);
7 }
8 alert('end');
查看在线代码。
a. JavaScript引擎是单线程运行的,浏览器无论在什么时候都只且只有一个线程在运行JavaScript程序
b. setTimeout是异步线程,需要等待js引擎处理完同步代码(while语句)之后才会执行,while语句直接是个死循环,js引擎没有空闲,不会执行下面的alert,也不会插入setTimeout。我在chrome中执行在线代码,最后浏览器是终止死循环执行alert。
c. JavaScript的工作机制是:当线程中没有执行任何同步代码的前提下才会执行异步代码,setTimeout是异步代码,所以setTimeout只能等js空闲才会执行,但死循环是永远不会空闲的,所以setTimeout也永远不会执行。
4.36、输出今天的日期,以YYYY-MM-DD的方式,比如今天是2014年9月26日,则输出2014-09-26
参考《JavaScript Date 对象》《日月如梭,玩转JavaScript日期》
4.37、Javascript中callee和caller的作用?
arguments.callee属性包含当前正在执行的函数。
Function.caller返回一个对函数的引用,该函数调用了当前函数。
4.38、函数柯里化(Currying)如何理解?
柯里化:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
柯里化其实本身是固定一个可以预期的参数,并返回一个特定的函数,处理批特定的需求。这增加了函数的适用性,但同时也降低了函数的适用范围。
参考《前端开发者进阶之函数柯里化Currying》
4.39、JS异步编程方式有几种?
a. 回调函数
b. 事件监听
c. 发布订阅
d. promise
参考《异步编程的4种方法》
4.40、请说说在JavaScript引用类型和值类型的理解
值类型:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。即Undefined、Null、Boolean、Number 和 String。
引用类型:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存处。即对象、数组
参考《ECMAScript 原始值和引用值》,查看在线代码。下面有道题目可以研究下,具体流程参考《理解js引用类型指针的工作方式》
var a = {n:1};
var b = a;
a.x = a = {n:2};
console.log(a.x);// --> undefined
console.log(b.x);// --> [object Object]
4.41、请解释一下JavaScript中的作用域和作用域链
变量的作用域(scope):程序源代码中定义这个变量的区域。
作用域链:是一个对象列表或链表,这组对象定义了这段代码“作用域中”的变量。查找变量会从第一个对象开始查找,有则用,无则查找链上的下一个对象。
参考《JavaScript权威指南》
五、网络与优化
5.1、讲讲输入完网址按下回车,到看到网页这个过程中发生了什么
a. 域名解析
b. 发起TCP的3次握手
c. 建立TCP连接后发起http请求
d. 服务器端响应http请求,浏览器得到html代码
e. 浏览器解析html代码,并请求html代码中的资源
f. 浏览器对页面进行渲染呈现给用户
参考《输入URL到展现页面的全过程》
5.2、谈谈你对前端性能优化的理解
a. 请求优化:
合并JS和CSS,减少DNS查找次数,避免重定向,使用GET完成AJAX请求,减小请求中的Cookie,缓存资源,使用CDN,开启GZip,压缩HTML页面,开启长连接,避免行内脚本阻塞并行下载,少用iframe(阻塞onload事件,影响并行下载)。
b. CSS优化:
样式表置于页面顶部,避免使用CSS表达式,使用外部JS和CSS,压缩JS和CSS,避免滤镜。
c. JavaScript优化:
脚本置于页面底部,减少DOM访问,减少重绘和重排,尽量使用局部变量,使用定时器分割大型任务,用合适的正则操作字符串,惰性模式减少分支,事件委托,第三方代码异步加载,节流与去抖动,使用localStorage替代cookie。
d. 图片优化:
内联图使用Data:URL,压缩图片或使用WebP格式,固定图片尺寸,图片预加载,图片延迟加载,使用字体矢量图标,Sprites图片。
参考《前端网络、JavaScript优化以及开发小技巧》《实践一下前端性能分析》《JavaScript性能优化 DOM编程》
5.3、请说出三种减少页面加载时间的方法
a. 尽量减少页面中重复的HTTP请求数量
b. 服务器开启gzip压缩
c. css样式的定义放置在文件头部
d. Javascript脚本放在文件末尾
e. 压缩合并Javascript、CSS代码
f. 使用多域名负载网页内的多个文件、图片
参考《减低页面加载时间的方法》
5.4、请介绍下cache-control
每个资源都可以通过 Cache-Control HTTP 头来定义自己的缓存策略
Cache-Control 指令控制谁在什么条件下可以缓存响应以及可以缓存多久
Cache-Control 头在 HTTP/1.1 规范中定义,取代了之前用来定义响应缓存策略的头(例如 Expires)。
5.5、一次js请求一般情况下有哪些地方会有缓存处理?
a. 浏览器端存储
b. 浏览器端文件缓存
c. HTTP缓存304
d. 服务器端文件类型缓存
e. 表现层&DOM缓存
参考《一次HTTP请求中有哪些地方可以缓存》
5.6、一个页面上有大量的图片(大型电商网站),加载很慢,你有哪些方法优化这些图片的加载,给用户更好的体验。
a. 图片懒加载,滚动到相应位置才加载图片。
b. 图片预加载,如果为幻灯片、相册等,将当前展示图片的前一张和后一张优先下载。
c. 使用CSSsprite,SVGsprite,Iconfont、Base64等技术,如果图片为css图片的话。
d. 如果图片过大,可以使用特殊编码的图片,加载时会先加载一张压缩的特别厉害的缩略图,以提高用户体验。
5.7、谈谈以前端角度出发做好SEO需要考虑什么?
a. 了解搜索引擎如何抓取网页和如何索引网页
b. meta标签优化
c. 关键词分析
d. 付费给搜索引擎
e. 链接交换和链接广泛度(Link Popularity)
f. 合理的标签使用
5.8、HTTP2.0有哪些提升?
a. HTTP 2.0中的二进制分帧层突破了限制:客户端和服务器可以把HTTP消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。
b. 把HTTP消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,进一步提升性能。
c. HTTP 2.0通过让所有数据流共用同一个连接,可以更有效地使用TCP连接。
d. 服务器除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。
e. HTTP 2.0会压缩首部元数据,针对之前的数据只编码发送差异数据。
5.9、TCP与UDP的区别
a. UDP 协议的头长度不到TCP头的一半,所以同样大小的包里UDP携带的净数据比TCP包多。
b. TCP会发响应,UDP不会。并且UDP没有Seq和Ack等概念,省去了建立连接的开销,DNS解析就使用UDP协议。TCP有3次握手4次挥手。
c. UDP不能分割报文段(MSS),超过MTU的时候,发送方的网络层负责分片,接收方收到分片后再组装起来,这个过程会消耗资源,降低性能。
d. UDP没有重传机制,丢包的时候就不能按需发送。TCP有超时重传、快速重传和SACK。
5.10、URI和URL
URI(Uniform Resource Identifier):统一资源标识符。
URL(Uniform Resoure Locator):统一资源定位符,通俗的说法是网址。
URI表示某一互联网资源,而URL表示资源地点,所以URL是URI的子集。
参考资料:
【答阿里寒冬面试题】呵呵,大神的面试题就是好!
做几道前端面试题休息休息吧
来看一点CSS相关的吧
前端面试题第二弹袭来,接招!
阿里巴巴校招笔试题整理(HTML+CSS篇)
最全前端面试问题及答案总结
2014年最新前端开发面试题
2014PPTV-题解
一些前端开发的笔试题及答案【编程题】
常见前端面试题之HTML/CSS部分
BAT及各大互联网公司2014前端笔试面试题--Html,Css篇
BAT及各大互联网公司2014前端笔试面试题--JavaScript篇【编程题】
10道javascript笔试题【编程题】
本文转自 咖啡机(K.F.J) 博客园博客,原文链接:http://www.cnblogs.com/strick/p/4968200.html,如需转载请自行联系原作者