笔记来源 尚硅谷最新版JavaScript基础全套教程完整版(140集实战教学,JS从入门到精通)_哔哩哔哩_bilibili
事件对象
1、事件对象
<前情提要>
事件对象
- 当事件的响应函数被触发时,浏览器每次都会将一个事件对象作为实参传递进响应函数
- 在事件对象中封装了当前事件相关的一切信息,比如:鼠标的坐标、键盘哪个按键被按下、鼠标滚轮滚动的方向。。。
事件属性
鼠标/键盘属性
<练习1:当鼠标在areaDiv中移动时,在showMsg中来显示鼠标的坐标>
HTML 代码
<div id="areaDiv"></div>
<div id="showMsg"></div>
CSS 代码
#areaDiv {
border: 1px solid black;
width: 300px;
height: 50px;
margin-bottom: 10px;
}
#showMsg {
border: 1px solid black;
width: 300px;
height: 20px;
}
JS 代码
var areaDiv = document.getElementById("areaDiv");
var showMsg = document.getElementById("showMsg");
// 绑定鼠标移动事件
areaDiv.onmousemove = function(event){
console.log(event); // IE8:undefined
// clientX可以获取鼠标指针的水平坐标
// cilentY可以获取鼠标指针的垂直坐标
var x = event.clientX;
var y = event.clientY;
showMsg.innerHTML = "x = " + x + ", y = " + y;
}
效果
内置浏览器
Chrome
Edge
IE11
IE8
在IE8中,响应函数被触发时,浏览器不会传递事件对象
在IE8及以下的浏览器中,是将事件对象作为window
对象的属性保存的
那么按照之前学习到的思路,我们可以对其进行兼容性改造
var x;
var y;
if (event) {
x = event.clientX;
y = event.clientY;
}
else{
x = window.event.clientX;
y = window.event.clientY;
}
showMsg.innerHTML = "x = " + x + ", y = " + y;
IE8测试
感觉上述代码不优雅,对上述代码进行二次改造
//if (!event) {
// event = window.event;
//}
event = event || window.event;
var x = event.clientX;
var y = event.clientY;
<练习2:div跟随鼠标移动>
// 兼容性写法
event = event || window.event;
var left = event.clientX;
var top = event.clientY;
// div随鼠标移动,注意style属性是有单位的
box1.style.left = (left - box1.clientWidth / 2) + "px";
box1.style.top = (top - box1.clientHeight / 2) + "px";
但是,当我们给body设置一个较大height
属性值时,会发现一个问题,就是鼠标指针与 div 之间存在一定距离
这是为什么呢?
clientX
和clientY
用于获取鼠标在当前的可见窗口的坐标 div 的偏移量,是相对于整个页面的pageX
和pageY
可以获取鼠标相对于当前页面的坐标,但是这个两个属性在IE8中不支持,所以如果需要兼容IE8,则不要使用
var left = event.pageX;
var top = event.pageY;
再试下效果
貌似好了哈,那直接测试下 IE8?
这要怎么办?
我们现在给 body 设置了一个height
,红色框表示可见区域大小,蓝色框表示 body 的实际区域大小
既然我们没办法使用pageX
和pageY
兼容IE8,那暂时只能使用clientX
和clientY
了,而clientX
和clientY
是按照可见区域大小计算的,那让 div 的水平和垂直偏移量也按照可见区域大小计算不就行了吗?但是我们又暂时没办法让 div 总是参考可见区域大小的原点作为定位的原点,难道就没有办法了吗?
我们之前学习过,scrollTop
表示滚动条的垂直滚动距离,而div位置原点 与鼠标指针原点的差距应该刚好是滚动条垂直滚动的距离,那么是不是可以利用这两个属性来“弥补” 这两者之间的距离差呢?
box1.style.top = (document.body.scrollTop + top - box1.clientHeight / 2) + "px";
发现还是不行,要知道我们是给 body 设置的height
属性,之所以出现滚动条是因为 body 的父元素容不下 body 了,所以应该获取谁的scrollTop
属性?body 的父元素,即 html
box1.style.top = (document.documentElement.scrollTop + top - box1.clientHeight / 2) + "px";
Chrome
IE8
在视频中,测试的结果是Chrome和火狐等浏览器获取scrollTop
的对象不一致,需要做兼容
chrome认为浏览器的滚动条是body的,可以通过body.scrollTop来获取火狐等浏览器认为浏览器的滚动条是html的,
var st = document.body.scrollTop || document.documentElement.scrollTop;
但是不知道什么原因(浏览器对scrollTop
和scrollLeft
都统一兼容了?毕竟视频是几年前的了),我这里并没有这个问题,所以上述问题存疑,待考究,后面以我实际代码测试结果为准
同理,当水平方向有滚动条时,也要消除水平方向上的距离差,所以综合代码如下
box1.style.left = (document.documentElement.scrollLeft + left - box1.clientWidth / 2) + "px";
box1.style.top = (document.documentElement.scrollTop + top - box1.clientHeight / 2) + "px";
我这里通过documentElement
获取的scrollLeft
和scrollTop
在 Chrome、Edge、IE11、IE8 中均正常
2、事件的冒泡(Bubble)
HTML 代码
<div id="box1">
我是div
<span id="s1">
我是span
</span>
</div>
CSS 代码
#box1{
width:200px;
height: 200px;
background-color: #99FF99;
}
#s1{
background-color: yellowgreen;
}
JS 代码
document.getElementById("s1").onclick = function(){
alert("我是span"); // 我是span 我是div 我是body 我是HTML
};
document.getElementById("box1").onclick = function(){
alert("我是div"); // 我是div 我是body 我是HTML
};
document.body.onclick = function(){
alert("我是body"); // 我是body 我是HTML
};
document.documentElement.onclick = function(){
alert("我是HTML"); // 我是HTML
};
所谓的冒泡指的就是事件的向上传导,当后代元素上的事件被触发时,其祖先元素的相同事件也会被触发
在开发中大部分情况冒泡都是有用的,如果不希望发生事件冒泡可以通过事件对象来取消冒泡
可以将事件对象的cancelBubble
设置为true
,即可取消冒泡
document.getElementById("s1").onclick = function(event){
// 兼容event
event = event || window.event;
alert("我是span"); // 我是span
event.cancelBubble = true;
};
3、事件的委派(Delegate)
HTML 代码
<button type="button" id="btn">Add</button>
<ul id="ulDiv">
<li><a href="javascript:;">超链接1</a></li>
<li><a href="javascript:;">超链接2</a></li>
<li><a href="javascript:;">超链接3</a></li>
</ul>
JS 代码
function clickFun(){
alert("超链接");
}
window.onload = function(){
// 为每一个超链接都绑定一个单击响应函数
var aList = document.getElementsByTagName("a");
for(var i=0;i<aList.length;i++){
aList[i].onclick = clickFun;
}
var btn = document.getElementById("btn");
var ulDiv = document.getElementById("ulDiv");
btn.onclick = function(){
var li = document.createElement("li");
li.innerHTML = "<a href=\"javascript:;\">add超链接</a>";
ulDiv.appendChild(li);
};
};
这里我们为每一个超链接都绑定了一个单击响应函数,这种操作比较麻烦
而且这些操作只能为已有的超链接设置事件,而新添加的超链接必须重新绑定
我们希望,只绑定一次事件,即可应用到多个的元素上,即使元素是后添加的
我们可以尝试将其绑定给元素的共同的祖先元素
ulDiv.onclick = function(){
alert("事件委派超链接");
};
事件委派是指将事件统一绑定给元素的共同的祖先元素
这样当后代元素上的事件触发时,会一直冒泡到祖先元素,从而通过祖先元素的响应函数来处理事件
事件委派是利用了冒泡,通过委派可以减少事件绑定的次数,提高程序的性能
但是也有个问题,我们是给整个 ul 绑定的单击响应事件,ul 是块元素,在超链接所在行点击任何位置都会触发事件
那怎么办呢?我们就需要再加一层判断: 如果触发事件的对象是我们期望的元素,则执行否则不执行
那怎么知道触发事件的对象是什么呢?
ulDiv.onclick = function(event){
event = event || window.event;
// 如果触发事件的对象是我们期望的元素,则执行否则不执行
// alert(event.target); // 点击超链接外:[object HTMLLIElement]; 点击超链接:javascript:;
if(event.target.className == "link"){
alert("事件委派超链接");
}
};
但是这种写法有点问题,当其class
属性有多个时,就不对了
<li><a href="javascript:;" class="link hello">超链接1</a></li> <!-- 失效 -->
<li><a href="javascript:;" class="link">超链接2</a></li>
<li><a href="javascript:;" class="link">超链接3</a></li>
我这里将tagName
代替className
作为判断条件进行判断
ulDiv.onclick = function(event){
event = event || window.event;
if(event.target.tagName == "A" || event.target.tagName == "a"){
alert("事件委派超链接");
}
};
4、事件的绑定(Bind)
on事件名
使用对象.事件 = 函数
的形式绑定响应函数,它只能同时为一个元素的一个事件绑定一个响应函数
不能绑定多个,如果绑定了多个,则后边会覆盖掉前边的
var btn = document.getElementById("btn");
// 为btn绑定一个单击响应函数
btn.onclick = function() {
alert(1);
};
// 为btn绑定第二个响应函数
btn.onclick = function() {
alert(2); // 2
};
addEventListener()
addEventListener()
通过这个方法也可以为元素绑定响应函数,参数:
- 事件的字符串,不要
on
- 回调函数,当事件触发时该函数会被调用
- 是否在捕获阶段触发事件,需要一个布尔值,一般都传
false
使用addEventListener()
可以同时为一个元素的相同事件同时绑定多个响应函数
这样当事件被触发时,响应函数将会按昭函数的绑定顺序执行
btn.addEventListener("click", function(){
alert(1); // 1
}, false);
btn.addEventListener("click", function(){
alert(2); // 2
}, false);
btn.addEventListener("click", function(){
alert(3); // 3
}, false);
我们直接在 IE8 中进行测试,这个方法不支持IE8及以下的浏览器
那说了半天,IE8 需要用什么方法替代呢?
attachEvent()
attachEvent()
在 IE8 中可以用来绑定事件,参数:
- 事件的字符串,要
on
- 回调函数
btn.attachEvent("onclick", function(){
alert(1); // 1
});
btn.attachEvent("onclick", function(){
alert(2); // 2
});
btn.attachEvent("onclick", function(){
alert(3); // 3
});
继续测试,在 IE8 中没有报错,但是执行顺序却是相反的,而且其他浏览器中直接就不行了
总结: 这个方法也可以同时为一个事件绑定多个处理函数,不同的是它是后绑定先执行,执行顺序和addEventListener()
相反
看起来,我们还是要自己封装一个方法来兼容不同的浏览器
// 定义一个函数,用来为指定元素绑定响应函数
// 参数:
// - obj 要绑定事件的对象
// - eventStr 事件的字符串
// - callback 回调函数
function bind(obj, eventStr, callback) {
if (obj.addEventListener) {
obj.addEventListener(eventStr, callback, false);
} else {
obj.attachEvent("on" + eventStr, callback);
}
}
我们调用下只能自定义的bind
函数
bind(btn, "click", function() {
alert(1);
});
测试下效果,发现在 IE8 和其他浏览器中均支持
好,我们接着再看个问题
bind(btn, "click", function() {
alert(this); // IE8: [object window];非IE8:[object HTMLButtonElement]
});
测试发现,在 Chrome 中打印的是[object HTMLButtonElement]
而在 IE8 中打印的却是[object window]
addEventListener()
中的this
是绑定事件的对象,attachEvent()
中的this
是window
,需要统一两个方法this
我们之前讲过call
和apply
方法,this
是指定的那个对象,是不是可以利用call
或者apply
方法对bind
函数进行优化呢?
function bind(obj, eventStr, callback) {
if (obj.addEventListener) {
obj.addEventListener(eventStr, callback, false);
} else {
// this是谁由调用方式决定
// callback.call(obj)
obj.attachEvent("on" + eventStr, function(){
// 在匿名函数中调用回调函数
callback.call(obj);
});
}
}
5、事件的传播
关于事件的传播网景公司和微软公司有不同的理解
- 微软公司认为事件应该是由内向外传播,也就是当事件触发时,应该先触发当前元素上的事件,然后再向当前元素的祖先元素上传播,也就说件应该在 冒泡阶段 执行
- 网景公司认为事件应该是由外向内传播的,也就是当前事件触发时,应该先触发当前元素的最外层的祖先元素的事件,然后在向内传播给后代元素
- W3C综合了两个公司的方案,将事件传播分成了三个阶段
- 捕获阶段:在捕获阶段时从最外层的祖先元素,向目标元素进行事件的捕获,但是默认此时不会触发事件
- 目标阶段:事件捕获到目标元素,捕获结束开始在目标元素上触发事件
- 冒泡阶段:事件从目标元素向他的祖先元素传递,依次触发祖先元素上的事件
如果希望在捕获阶段就触发事件,可以将addEventListener()
的第三个参数设置为true
一般情况下我们不会希望在捕获阶段触发事件,所以这个参数一般都是false
IE8 及以下的浏览器中没有捕获阶段
6、拖拽
拖拽的流程
- 当鼠标在被拖拽元素上按下时,开始拖拽
onmousedown
- 当鼠标移动时被拖拽元素跟随鼠标移动
onmousemove
- 当鼠标松开时,被拖拽元素固定在当前位置
onmouseup
HTML 代码
<div id="box1"></div>
<div id="box2"></div>
CSS 代码
#box1 {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
}
#box2 {
width: 100px;
height: 100px;
background-color: yellow;
position: absolute;
left: 300px;
top: 300px;
}
JS 代码
var box1 = document.getElementById("box1");
// 1. 当鼠标在被拖拽元素上按下时,开始拖拽 `onmousedown`
box1.onmousedown = function(event) {
event = event || window.event;
var boxLeft = event.clientX - box1.offsetLeft; // 鼠标水平坐标 - 元素水平偏移量 = 鼠标原点和元素原点水平距离
var boxTop = event.clientY - box1.offsetTop; // 鼠标垂直坐标 - 元素垂直偏移量 = 鼠标原点和元素原点垂直距离
// 2. 当鼠标移动时被拖拽元素跟随鼠标移动 `onmousemove`
document.onmousemove = function(event) {
event = event || window.event;
box1.style.left = event.clientX - boxLeft + "px";
box1.style.top = event.clientY - boxTop + "px";
};
// 3. 当鼠标松开时,被拖拽元素固定在当前位置 `onmouseup`
document.onmouseup = function(event) {
// 取消document的onmousemove事件
document.onmousemove = null;
// 取消document的onmouseup事件
document.onmouseup = null;
};
};
效果
当我们拖拽一个网页中的内容时,浏览器会默认去搜索引擎中搜索内容,此时会导致拖拽功能的异常,这个是浏览器提供的默认行为
如果不希望发生这个行为,则可以通过return false
来取消默认行为
但是这招对 IE8 不起作用
那有什么方法可以兼容 IE8 呢?我们先接着往下看
setCapture()
var btn1 = document.getElementById("btn1");
var btn2 = document.getElementById("btn2");
btn1.onclick = function() {
alert(1);
}
btn2.onclick = function() {
alert(2);
}
// 设置btn1对鼠标按下相关的事件进行捕获
// 当调用一个元素的setCapture()方法以后,这个元素将会把下一次所有的鼠标按下相关的事件捕获到自身上
btn1.setCapture();
我们点击 btn2 按钮,发现只有刷新后的第一次点击的提示为1,再次点击就变成了2
我们可以利用setCapture()
方法对 IE8 浏览器的默认行为进行限制吗?当拖拽元素时捕获事件,取消拖拽时释放对事件的捕获
var box1 = document.getElementById("box1");
// 1. 当鼠标在被拖拽元素上按下时,开始拖拽 `onmousedown`
box1.onmousedown = function(event) {
// 设置box1捕获所有鼠标按下的事件
// 只有IE支持,但是在火狐中调用时不会报错,而如果使用Chrome调用,会报错
box1.setCapture && box1.setCapture();
event = event || window.event;
var boxLeft = event.clientX - box1.offsetLeft; // 鼠标水平坐标 - 元素水平偏移量 = 鼠标原点和元素原点水平距离
var boxTop = event.clientY - box1.offsetTop; // 鼠标垂直坐标 - 元素垂直偏移量 = 鼠标原点和元素原点垂直距离
// 2. 当鼠标移动时被拖拽元素跟随鼠标移动 `onmousemove`
document.onmousemove = function(event) {
event = event || window.event;
box1.style.left = event.clientX - boxLeft + "px";
box1.style.top = event.clientY - boxTop + "px";
};
// 3. 当鼠标松开时,被拖拽元素固定在当前位置 `onmouseup`
document.onmouseup = function(event) {
// 取消document的onmousemove事件
document.onmousemove = null;
// 取消document的onmouseup事件
document.onmouseup = null;
// 当鼠标松开时,取消对事件的捕获
box1.releaseCapture && box1.releaseCapture();
};
// 取消默认行为
return false;
};
测试在 IE8 中的效果
如果我想拖动 div2呢?这个时候我们需要封装一个函数,方便我们直接传参调用
// 拖拽方法封装成一个函数
function draw(obj){
obj.onmousedown = function(event) {
obj.setCapture && obj.setCapture();
event = event || window.event;
var boxLeft = event.clientX - obj.offsetLeft;
var boxTop = event.clientY - obj.offsetTop;
document.onmousemove = function(event) {
event = event || window.event;
obj.style.left = event.clientX - boxLeft + "px";
obj.style.top = event.clientY - boxTop + "px";
};
document.onmouseup = function(event) {
document.onmousemove = null;
document.onmouseup = null;
obj.releaseCapture && obj.releaseCapture();
};
return false;
};
}
HTML 代码
<div id="box1"></div>
<div id="box2"></div>
<img src="img/an.jpg" id="img" style="width: 320px;height: 320px;position: absolute;left:200px;top:400px;"/>
JS 代码调用函数
var box1 = document.getElementById("box1");
var box2 = document.getElementById("box2");
var img = document.getElementById("img");
draw(box1);
draw(box2);
draw(img);
效果
Chrome
IE8