JS基础(十)DOM0和DOM2级事件

JS中的DOM0和DOM2事件

JS中的事件基础及核心原理以及项目实战

事件的定义

事件:元素天生具备的行为方式(和写不写JS代码没有关系)【onclick不是事件,click才是事件,浏览器会把一些常用事件挂载到元素对象的私有属性上,让我们可以实现DOM0事件绑定】,当我们去操作元素的时候会触发元素的很多事件
事件绑定:给当前元素的某个事件绑定方法,目的是让当前元素某个事件被触发时,做出一些反应

事件绑定的两种方法

  • DOM0级事件绑定
    • curEle.onclick=function(){};
  • DOM2级事件绑定
    • 标准浏览器:curEle.addEventListener(‘click’,function(){},false)
    • IE6-8:curEle.attachEvent(‘onclick’,function(){})

DOM0事件

常用事件汇总

[PC端]

  • 表单元素常用的事件
  • blur:失去焦点
  • focus:获取焦点
  • change:内容改变
  • select:被选中事件
  • 键盘常用事件行为
  • keydown:键盘按下
  • keyup:键盘抬起
  • keypress:键盘按下(中文输入法不能触发,英文状态下可以触发,因为文本被输入到文本框中)
  • 鼠标常用事件行为
  • click:点击(不是单击)
  • dblclick:双击(300ms之内连续触发两次点击事件)
  • mouseover:鼠标滑过
  • mouseout:鼠标滑出
  • mouseenter: 鼠标进入
  • mouseleave: 鼠标离开
  • mousedown:鼠标按下(左键)
  • mouseup:鼠标左键抬起
  • mousewheel:鼠标滚轮滚动
  • 其他常用事件行为
  • load:加载成功
  • error:加载失败
  • scroll:
  • resize:大小改变事件:window.onresize:浏览器窗口改变大小触发该事件

[移动端

移动端键盘一般都是虚拟键盘,虽然存在keydown,keyup,但兼容不好,使用input事件代替
移动端没有鼠标,所以鼠标类事件在移动端兼容性非常差,mouse类事件基本无法使用
移动端大部分事件是靠手指完成的,所以它独有手指事件
-单手指事件:touchstart touchmove touchend touchcancel
-多手指事件:gesturestart gestuerchange gestuerend…..
移动端还有很多事件是靠硬件完成的:手机传感器,陀螺仪,重力感应器等
移动端兼容click事件,它把click当做单击使用(会有300MS的延迟事件)

事件对象

对以下事件绑定的严谨版描述:基于DOM0级事件的绑定方式,给box的click事件绑定了一个方法。当手动触发click事件时,会把绑定的方法执行
box.onclick=function(e){
arguments[0]===e;//=>e就是事件对象
e=e||window.event;(兼容版获取事件对象的写法)
};
当元素的某个事件行为被触发,不仅会把之前绑定的方法执行,还会给绑定的方法传递一个值(浏览器默认传递的),我们把传递的这个值称为事件对象:(标准浏览器)

  • 这个值是个对象类型的值,里面存储了很多的属性和方法,
  • 这些存储的值都是当前本次操作的基本信息,例如:鼠标位置,触发行为类型。。。。【只和本次操作有关,一个页面中只有一个事件对象】

IE6-8下关于事件对象的机制

  • 方法被触发执行时,浏览器并没有把事件对象当做值传递给函数;(e是undefined)
  • IE6-8下的事件对象需要我们通过window.event来获取

鼠标事件对象(mouseevent)

  • clientX/clientY:当前鼠标触发点距离当前窗口左上角的X/Y坐标
  • pageX/pageY:当前鼠标触发点距离BODY左上角的X/Y坐标(IE6-8中没有这两个属性)
    兼容处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
box.onckick=function(e){
if(typeof e==='undefined'){
e=window.event; //->处理e
//->pageX/pageY
e.pageX=e.clientX+(document.documentElement.scrollLeft||document.body.scrollLeft);
e.pageY=e.clientY+(document.documentElement.scrollTop||document.body.scrollTop);
//->target
e.target=e.srcElement;
//->preventDafault
e.preventDefault=function(){
e.returnValue=false;
};
//=>stopPropagation
e.stopPropagation=function(){
e.cancelBubble=true
}
}
}//该兼容比较完整,当处理一个简单兼容时可简单处理
  • type:当前触发事件的类型
  • target:事件源(当前鼠标操作的是那个元素)(IE6-8没有该属性,用srcElement这个属性代表事件源)
  • preventDefault:作用:阻止事件的默认行为;(IE6-8没有该方法,使用 returnValue=false处理)
  • stopPropagation:阻止事件的冒泡传播 [IE6-8不兼容,使用cancelBubble=true来处理]

键盘事件对象(keyboardEvent)

  • code:当前按得键盘按键是哪一个 【IE6-8没有这个属性】key属性和code一样,储存形式不一样
  • keyCode:存储的是当前按键的码值(大部分按键都有自己的码值)
  • which:和keyCode一样,也是当前按键的码值(which不兼容IE6-8)
  • 这几个码值需要记住(删除:8 、 回车:13、 空格:32、 tab :9、)

手指事件对象(TouchEvent)

touches&&changedTouches&&targetTouches:存储的是当前屏幕上每一个手指操作的位置信息

  • touches:只有手指在屏幕上我们才能获取相关信息,手指离开后,这样touchend事件中我们就无法通过touches获取手指信息
  • changedTouches:手指在屏幕上时,和touches一样可以获得相关信息,手指离开后也可以记录手指离开屏幕一瞬间所在的位置信息(最常用的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
touchesEvent
type:'touchStart',
target:事件源,
touches:
0:{
clientX:xxx,
clientY:xxx,
pageX:xxx,
pageY:xxx,
.....
}
...
lentth:1
chan

详细处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
let oBox = document.querySelector('#box');
oBox.ontouchstart = function (e) {
let point = e.changedTouches[0];
//=>记录当前手指的起始坐标位置(记录在当前元素的自定义属性上)
this.strX = point.pageX;
this.strY = point.pageY;
this.isMove = false;
};
oBox.ontouchmove = function (e) {
let point = e.changedTouches[0];
//=>一般我们手指操作,都会或多或少的发生一些偏移(习惯性偏移),此时不应该算作滑动,只有滑动的距离超出一定范围,我们按照滑动处理即可(一般都是把10px作为偏差值)
let changeX = point.pageX - this.strX,
changeY = point.pageY - this.strY;
this.changeX = changeX;
this.changeY = changeY;
if (Math.abs(changeX) > 10 || Math.abs(changeY) > 10) {
this.isMove = true;
}
};
oBox.ontouchend = function (e) {
let point = e.changedTouches[0];
//=>手指离开的时候:验证是否发生滑动
if (!this.isMove) {
//=>点击操作
console.log('我是点击操作~~');
return;
}
//=>滑动操作
let dir = null;
if (Math.abs(this.changeX) > Math.abs(this.changeY)) {
//=>左右滑动
dir = this.changeX < 0 ? 'LEFT' : 'RIGHT';
} else {
//=>上下滑动
dir = this.changeY < 0 ? 'UP' : 'DOWN';
}
console.log(`当前手指滑动的方向为:${dir}`);
};

手机移动端的点击、长按、滑动等都是基于内置的原生的touchstart/touchmove/touchend事件模拟出来的一些效果,没有现成的事件
市场上有许多成熟的类库,我们直接使用即可
1、fastclick.js:目的就是解决了移动端click事件300ms的延迟问题
2、百度云touch手指事件库
3、hammer.js
4、zepto.js:提供移动端事件操作的板块,也是目前市场上使用率最高的

阻止事件的默认行为

A标签的默认行为及阻止

1.超链接:点击A标签可以实现页面的跳转
2、锚点定位:通过HASH值定位到当前页面的指定ID盒子位置
- 首先URL地址栏末尾追加了一个HASH;
- 如果地址栏包含hash值,浏览器在渲染页面后,会默认定位到hash值的位置

真实项目中我们想用A标签做一个普通按钮(优势:它的:hover样式是兼容所有浏览器的)此时,要把之前提到的两个默认行为阻止掉才可以

阻止A标签的默认行为

在HTML中阻止默认行为(常用)

在JS中阻止

  • 单点击A标签时 首先会触发click事件
  • 其次按照href中的地址页面进行跳转
    link.onclick=function(){ return false;}//阻止页面行为
    link.onclick=function(e){
    ​ e=e||window.event;
    ​ e.preventDefault?e.preventDefault():e.returnValue=false;
    ​ }

事件的传播机制

事件传播有三个阶段

Event.prototype:

1
2
3
4
- 0  NONE:默认值,不代表任何意思
- 1 `CAPTURING_PHASE 捕获阶段`
- 2 ` AT_TARGET 目标阶段(当前事件源)`
- 3 `BUBBLING _PHASE :冒泡阶段`

当前元素的某个事件行为被触发,它的所有的祖先元素(一直到document)的相关事件行为也会被触发执行(由里向外),我们把这种传播机制叫做冒泡传播

mouseover和mouseenter事件的区别

mouseover:鼠标滑到元素上:存在事件的冒泡传播机制 mouseenter:鼠标进入元素里;浏览器阻止了它的冒泡传播机制; 不同点

区别 mouseover mouseenter
区别 1 存在冒泡传播机制 冒泡传播机制被浏览器阻止
区别 2 当鼠标从父元素进入到子元素时,首先会触发父元素的mouseout事件,再触发子元素的mouseover事件,由于冒泡传播机制,导致父元素的mouseover事件也被触发 当鼠标从父元素进入到子元素时,并不会触发父元素的mouseout事件,但是触发了子元素的mouseenter事件,由于浏览器阻止了它的冒泡传播,所以父元素的该事件不会被触发

事件委托(very important)

原理:利用事件的冒泡传播机制完成(mouseenter不存在冒泡传播)

当一个容器内的很多元素都要为同一事件绑定方法,那么我们只需要给外层容器的该事件绑定方法,当里层元素的事件被出发时,会通过冒泡传播机制传到最外层容器那里,触发外层容器绑定的方法执行,在方法执行时,我们只需要根据判断事件源的不同而做不同的事情。(利用事件委托可提高50%左右的性能)

拖拽当中鼠标焦点丢失问题

在拖拽登陆窗口的案例中(11.25) 当鼠标移动速度过快时,鼠标离开了h3,因为盒子跟不上奔跑的速度,所以导致以下几个问题 - 鼠标在h3之外飞,不会触发H3的mousemove,盒子就不动了; - 鼠标在h3之外抬起,也不会触发h3的mouseup,那么原有绑定的dragmove无法被移除,鼠标重新进入h3,此时不管鼠标是否按下,都会触发H3的dragmove; 原因:鼠标飞出h3,再怎么操作就和h3没有关系 解决方案: 1.把mouseup和mousemove事件绑定给document,原因:不管鼠标怎么飞,都飞不出document;(使用箭头函数)只要你鼠标还在文档中,那么mousemove和mouseup永远生效 2.setCapture(不兼容谷歌,支持Ie)

DOM2事件绑定

1
2
3
4
5
6
 //=>标准浏览器
oBox.addEventLister('click',function(e){
//this:obx
},false)
//false=>让事件在冒泡传播时执行
//true=>让事件在捕获阶段执行(非常少见)
1
2
3
4
5
//=>IE6-8浏览器
oBox.attachEvent('onclick',function(e){
//e:事件对象,不同于DOM0级事件,浏览器会默认将事件对象传递进来,与window.event的值相同,因此对于:pageX/pageY/target...等依旧存在兼容;
})
//=>绑定的方法都是在冒泡传播阶段执行

DOM1?

在DOM第一代升级迭代的时候,事件绑定依然沿用DOM0绑定的方式..so…

DOM0于DOM2事件绑定的区别

DOM0事件绑定的原理
  • 给当前元素的某一私有属性(onXXX)赋值的过程;(之前属性默认值是null,如果我们赋值了一个函数,就相当于绑定了一个方法)
  • 当我们赋值成功(赋值一个函数),此时浏览器会把DOM元素和赋值的的函数建立关联,以及建立DOM元素的行为监听,当某一行为被用户触发,浏览器会把赋值的函数执行;
DOM0事件绑定的特点
  • 只有DOM元素天生拥有这个私有属性(onxxx事件私有属性),我们赋值的方法才叫事件绑定,否则属于设置自定义属性
  • 移除事件绑定的时候,我们只需要赋值为null;
  • 在DOM0事件绑定中,只能给当前元素的某一个事件行为绑定一个方法,绑定多个方法,最后一次的绑定的会替换前面绑定的

DOM2事件绑定的原理

  • DOM2事件绑定使用的addEventListener/attachEvent方法都是在eventTarget这个内置类的原型上定义的,我们调用的时候,首先要通过原型链找到这个方法,然后执行完成事件绑定的效果
  • 浏览器会给当前元素的某个事件行为开辟一个事件池(事件队列)【浏览器有一个统一的事件池,每个元素绑定的行为都放在这里,通过相关标志区分】,当我们通过 addEventListener/attachEvent进行事件绑定的时候,会把绑定的方法放在事件池中
  • 当元素的某一行为被触发,浏览器回到对应事件池中,把当前放在事件池的所有方法按序依次执行

特点

  • 所有DOM0支持的行为,DOM2都可以用,DOM2还支持DOM0没有的事件行为(这样说比较笼统)
    (核心)【浏览器会把一些常用事件挂载到元素对象的私有属性上,让我们可以实现DOM0事件绑定,DOM2:凡是浏览器给元素天生设置的事件在DOM2中都可以使用】
    例如:onDOMContentLoaded(所有的DOM0和IE6-8的DOM2都不支持)
    ​onDOMContentLoaded//当前浏览器中的DOM结构加载完成,就会触发这个事件

  • DOM2中可以给当前元素的某一事件行为绑定多个不同方法(因为绑定的所有方法都放在事件池中);

  • 事件的移除:事件类型、绑定的方法、传播阶段三个完全一致,才可以完成移除(因此在绑定方法时,尽量不要用匿名函数,否则不好移除)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//=>ON:给当前元素的某个事件绑定某个方法
var on = function (curEle, type, fn) {
if (document.addEventListener) {
//=>标准浏览器
curEle.addEventListener(type, fn, false);
return;
}
//=>IE6~8
curEle.attachEvent('on' + type, fn);
};
//=>OFF:移除当前元素某个事件绑定的某个方法
13.var off = function (curEle, type, fn) {
14. if (document.removeEventListener) {
15. curEle.removeEventListener(type, fn, false);
16. return;
17. }
18. //=>IE6~8
19. curEle.detachEvent('on' + type, fn);
20.};

xxx.removeEventLister('click',function(){},false)

==DOM0和DOM2绑定的方法是毫无联系的(因为是两套完全不同的机制),即使绑定的方法相同,也是执行两次,谁先绑定,就先执行谁==

window.onload&&$(document).ready()的区别

window.onload:当浏览器中的所有资源(DOM结构、文本内容、图片)都加载完成,触发load事件;

  • 它是基于DOM0的事件绑定机制完成的,所以在同一页面中只能为他绑定一个方法,绑定多个,以最后一个为主;
  • 如果想在一个页面中使用多次,应该是基于DOM2绑定的;
1
2
3
4
5
6
function fn1(){
}
function fn1(){
}
window.addEventListener('load',fn1,false);
window.addEventListener('load',fn2,false);

$(function(){})或者$(document).ready(function(){});
当文档中的DOM结构加载完成,就会触发执行,在一个页面中可以使用多次

  • JQ中提供的方法,JQ是基于onDOMContentLoaded这个事件完成操作的
  • JQ中的事件绑定都是基于DOM2事件绑定的
  • onDOMContentLoaded在IE6-8下attachEvent也是不支持的,JQ在IE6-8下使用readystatechange来完成

DOM2事件的兼容处理

  • 语法上的兼容
    标准: curEle.addEventListener(’type’,fn,false);
    IE6-8: curEle.attachEvent(’on’+type,fn);

  • 顺序问题
    标准:按照绑定的顺序依次执行
    IE6-8:当事件行为被触发,IE6-8执行是乱序

  • 重复问题
    标准:可以自动去重,已经存在的方法不允许再次添加
    IE6-8:在向事件池中增加方法时没有去重机制

  • this问题
    标准:当事件行为被触发,方法中的this指向当前元素本身
    IE6-8:当事件行为被触发,方法中的this指向window

IE6-8事件池机制vs标准浏览器事件池机制(比较规范的回答)

1、向事件池中添加方法时,标准浏览器是使用addEventListener,IE6-8使用的是attachEvent;而且标准浏览器有自动去重的机制,已经添加的方法不允许再次添加。IE6-8没有去重机制;
2、浏览器执行事件池中的方法时,不仅把方法执行,还把事件对象当作实参传递给给对应的方法,但是也是有区别的,IE6-8传递的事件对象的值和window.event是相同 的,因此存在兼容问题;
3.当事件行为被触发,标准浏览器是依次执行,方法中的this指向当前元素;IE6-8下,是乱序执行,且方法中的this指向window;

不兼容的本质:IE6-8低版本浏览器对于他的内置事件池处理机制的不完善导致的。
DOM2事件绑定兼容处理机制原理:自己创建一个类似于标准浏览器的自定义事件池(针对IE6-8)

  • on :手动创建一个自定义事件池,把需要绑定的方法全部存在自定义事件池中
  • off: 把不需要绑定的方法从自定义事件池中移除
  • run:把run放在内置事件池中,当行为触发,需要浏览器把run执行,在run中把自定义事件池的方法执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
//向事件池追加方法
var on = function (curEle, type, fn) {
//标准浏览器无需处理
if (document.addEventListener) {
curEle.addEventListener(type, fn, false);
return;
}
//创建自定义事件池:创建到当前元素的自定义属性上,不受作用域限制,以后在其他方法中需要使用,可直接获取(不放在全局是防止全局污染)

// 每一个事件都有一个独立的事件池
if (typeof curEle[type + 'pond'] === 'undefined') {
curEle[type + 'pond'] = [];
//只要执行on,就说明当前方法已被绑定
//把run放到内置事件池中;
// curEle.attachEvent('on'+type,run);//需要保证run中的 this是当前元素
curEle.attachEvent('on' + type, function () {
run.call(curEle, e);//将run放在一个匿名函数中,强制改变this
});
}

var aryPond = curEle[type + 'pond'];
// 去重操作,当前事件池中是否有当前事件
for (var i = 0; i < ary.length; i++) {
if (aryPond[i] === fn) {
return;
}
}
aryPond.push(fn);
};

//移除事件池的某个方法
var off = function (curEle, type, fn) {
if (document.removeEventListener) {
curEle.removeEventListener(type, fn, false);
return;
}

var aryPond = curEle[type + 'pond'];
if (!aryPond) return;
for (var i = 0; i < ary.length; i++) {
if (aryPond[i] === fn) {
// aryPond.splice(i,1);此方法会导致原始数组索引改变,此时正在执行的run循环,索引和想要的索引不一致,数组塌陷
//我们不能让当前数组的索引改变;
aryPond[i] = null;//置空,索引没变

}

}

};


//把自定义事件池中的方法依次执行
var run = function (e) {
// this=>curEle
//e:window.event,不兼容,需要处理
if (typeof e.target === 'undefined') {
e.target = e.srcElement;
e.which = e.keyCode;
e.pageX = e.clientX + document.documentElement.scrollLeft || document.body.scrollLeft;
e.stopPropagation = e.cancelBubble = true;
e.preventDefault = e.returnValue = false;
}


var aryPond = curEle[type + 'pond'];
if (!aryPond) return;
for (var i = 0; i < aryPond.length; i++) {
var itemFn = aryPond[i];
// itemFn();=>这样执行itemFn里的this时window,须将他替换成函数里的this
if (itemFn === null) {
//当前这一项在执行的时候,可以将off中置空的那一项删除
aryPond.slice(i, 1);
i--;
continue;
}
itemFn.call(this, e);
}

};

Bind方法的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//ES5封装  BIND
Function.prototype.myBind = function myBind(context) {
//this=>当前需要处理的函数
//context=>我们需要预先改变的this值(如果不传,默认window);
//arguments=>存储包含context在内的所有实参;
context = context || window;
var outerArg = Array.prototype.slice.call(arguments, 1);
var _this = this;
if('bind' in this){//如果不是IE6-8,直接返回
return _this.bind.apply(_this,arguments) ;
}

return function () {
var innerArg = Array.prototype.slice.call(arguments);
outerArg = outerArg.contact(innerArg);
_this.apply(context, outerArg);
}
}
1
2
3
4
5
6
7
8
9
//ES6方法
Function.prototype.myBind = function myBind(context = window, ...outer) { //使用ES6中的剩余运算符
if ('bind' in this) {
return this.bind(arguments);
}
return function (...inner) {
this.apply(context, outer.concat(inner));
}
};
坚持原创技术分享,您的支持将鼓励我继续创作!