dom-event

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

1、事件基础:全新认识事件

1、什么是事件?
事件是元素天生具备的行为方式 (和写不写js代码没关系),当我们去操作元素的时候会触发元素很多事件

2、事件绑定
给当前元素的某一个事件绑定方法,目的是为了让当前元素某个事件被触发的时候,可以做一些事情

给一个事件绑定方法,目前有两种方式:
1、DOM0级事件绑定
oBox.onclick=function(){}

2、DOM2级事件绑定
oBox.addEventListener('click', function(){}, false)标准浏览器
oBox.attachEvent('onclick', function()}) IE6-8

3、常用的事件汇总

【PC端】
表单元素常用事件行为
blur: 失去焦点
focus: 获取焦点
change: 内容改变
select: 被选中

键盘常用事件
keydown: 键盘按下
keyup: 键盘抬起
keypress: 键盘按下后有keypress(中文输入法状态下,会触发keydown,但是由于内容没有改变,keypress会一直触发)

鼠标常用事件
click: 点击(不是单机)
dblclick: 双击(300ms内凉席触发量词点击事件,这样为双击事件)
mouseover: 鼠标划过
mouseout: 鼠标划开
mouseenter: 鼠标进入
mouseleave: 鼠标离开
mousemove: 鼠标移动
mousedown: 鼠标左键按下
mouseup: 鼠标左键抬起
mousewheel: 鼠标滚轮滚动

其他事件行为
load: 加载成功
error: 加载失败
scroll: 滚动条滚动时间
resize: 大小改变事件 window.onresize当浏览器的窗口大小发生改变触发这个事件

【移动端】
移动端键盘一般都是虚拟键盘,虽然部分手机存在keydown/keyup但是兼容不好,所以我们想用键盘事件的时候,使用input事件代替inputBox.oninput = function(){}

移动端没有鼠标,所以鼠标类的事件在移动端兼容性都特别差

移动端的大部分操作是靠手指完成,移动端独有手指事件
单手指事件模型: touchstart、touchmove、touchend、touchcancel(由于意外事件导致触摸事件结束)

多手指事件模型: gesturestart、gesturechange、gestureend…

移动端还有很多操作是基于手机硬件完成的,例如手机传感器,手机陀螺仪,手机重力感应器等

在移动端兼容click事件,PC端的click是点击,但是移动端把click事件当做单击: 移动端使用click事件处理点击操作存在300ms

事件对象

1
2
3
4
//=> 事件绑定, 给oBox的click事件,基于DOM0级事件绑定的方式,绑定了一个方法;以后当我们手动触发oBox的click的时候,会把绑定的方法执行;
oBox.onclick = function(e){
//=> arguments[0] === e; 当方法执行的时候,浏览器默认传递方法的参数值(事件对象)
}

当元素的某一个事件被触发,不仅会把之前绑定的方法执行,而且还会给当前绑定的方法传递一个值(浏览器默认传递),我们把传递的这个值成为事件对象

1、因为这个值是个对象类型的值,里面存储了很多的属性和方法
2、这个对象中存储的值都是当前操作的一些基本信息,例如:鼠标的位置、触发的行为类型、触发的事件源等

以上所说的都是针对标准浏览器,IE6-8下不是这样的机制

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

1
2
//=> 兼容写法
e = e || window.event;

事件对象是为了记录当前操作基本信息的,所以只和本次操作有关:本次操作,页面中不管通过什么方式获取的e或者window.event(也不关在哪获取),他们存储的基本信息是相同的

鼠标事件对象 MouseEvent

clientX / clientY:当前鼠标触发点距离当前窗口左上角的X/Y轴的坐标
pageX / pageY:当前鼠标触发点距离BODY左上角的X/Y轴坐标(页面第一屏左上角),但是IE6-8中没有这两个属性

type:当前触发的事件类型

target:事件源(当前鼠标操作的是哪一个元素,那么事件源就是谁),IE6-8下没有tarrget属性,它有的是srcElement这个属性代表事件源

preventDefault:此方法是为了阻止事件的默认行为,IE6-8下没有这个方法,需要使用e.returnValue = false处理

stopPropagation:此方法是为了阻止事件的冒泡传播,IE6-8不兼容使用cancelBubble处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//=> pageX/pageY 兼容处理
oBox.onclick = function(e){
if(typeof e === 'undefined'){ //=> IE6-8
e = window.event;

//=> 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;

//=> preventDefault
e.preventDefault = function(){
e.returnValue = false;
}

//=> stopPropagation
e.stopPropagation = function(){
e.cancelBubble = true;
}
//=> 下面再使用属性或者方法的时候,完全按照标准浏览器的语法来实现即可(IE6-8下不兼容的属性和方法我们已经重写为兼容的了)
}
}

上面的兼容处理方式属于比较完整的,但是如果项目中我们指向用到一个不兼容的属性,哦们没有必要写这么多,简单处理一下兼容就可以了

1
2
3
4
5
oBox.onclick = function(e){
e = e || window.event;
e.target = e.target || e.scrElement;
e.preventDefault?e.preventDefault:e.returnValue = false;
}

键盘事件对象 KeyboardEvent

code:当前键盘的按键,例如:按删除键,存储的是Backspace(IE6-8下没有这个属性),还有一个叫key的属性和code一样,存储的也是按键的名称

keyCode:存储的是当前键盘对应的码值(大部分按键都有自己的码值)

which:和keyCode一样对应的也是键盘码的值(不兼容IE6-8)

1
2
3
4
inputBox.onkeyup = function(e){
e = e || window.event;
var code = e.which || e.keyCode;
}

移动端手指事件对象 TouchEvent

touches / changeTouches / targetTouches: 存储的是当前屏幕上每一个手指操作的位置信息(手指离开屏幕没有相关信息,这样touchend事件中我们无法通过touches获取手指信息)

changedTouches:手指再屏幕上的时候,和touches获取的信息一样,但是它可以记录手指离开屏幕一瞬间所在的位置信息(最常用)

1
2
3
4
5
6
7
8
9
10
11
12
13
TouchEvent
type: 'touchstart',
target: 事件源
touches:
0: {
clientX: xxx,
clientY: xxx,
pageX: xxx,
pageY: xxx,
...
}
length: 1
changeTouches以及targetTouches存储的结构和touches相同

我们知道移动端的click是点击事件(不是pc端的点击效果),存在300ms的延迟,项目中我们需要解决这个延迟:
使用touchstart/touchmove/touchend来处理

简单处理

1
2
3
4
5
oBox.ontouchend = function(){
//=> 不管你是怎么操作的,我只需要知道,手指离开就算点击即可,存在一些问题:
//=> 手指按住屏幕不松开,事件超过750ms应该算作长按,不是点击,手机离开不应该按照点击处理
//=> 手指在屏幕上滑动,此时应该算作滑动不是点击,手指离开屏幕不应该算作点击处理
}

详细处理,解决移动端click300ms延迟

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
var box = document.querySelector('#box');
box.ontouchstart = function(e){
let point = e.changedTouches[0];
//=> 记录当前手指的起始坐标位置(记录在当前元素的自定义属性上,在其他方法中如果我们想要获取直接通过自定义属性获取即可)
this.strX = point.pageX;
this.strY = point.pageY;
this.isMove = false;

};
box.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;
}
};
box.ontouchend = function(e){
let point = e.changedTouches[0];
//=> 手指离开的时候,验证是否发生滑动
if(!this.isMove){
//=> 点击操作
console.log('这是点击操作');
}

//=> 滑动操作
let dir = null;
if(Math.abs(this.changeX) > Math.abs(this.changeY)){
//=> 左右滑动
dir = this.changeX < 0 ? 'Left' : 'Right';
}else{
//=> 上下滑动
dir = this.changeY < 0 ? 'Top' : 'Down';
}
console.log(`当前手指滑动的方向为:${dir}`);
}

在移动端开发中,我们需要的一些操作(例如:点击,单机,双击,长按,滑动[四个方向]…)都是基于内置原生的touchstart/touchmove/touchend事件一点点模拟出来的效果,没有现成的事件
而多手指操作(例如:旋转、缩放…)都是基于gesture事件模型模拟出来的效果

目前市场上有很多成熟的类库或插件,专门为大家把常用的操作进行了封装,我们直接调取使用即可

1、fastclick.js 目的就是为了解决移动端click事件300ms延迟问题(如果我们的移动端使用了click我们只需要把这个js导入配置一下即可)
2、百度云touch手势事件库
3、hammer.js
4、zepto.js 提供了移动端事件操作的板块,也是目前市场上使用率最高的(小型JQ)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$('.box').tap(function(){
//=> 点击
});
$('.box').singleTap(function(){
//=> 单击
});
$('.box').doubleTap(function(){
//=> 双击
});
$('.box').longTap(function(){
//=> 长按
});
$('.box').swipe(function(){
//=> 滑动 swipeLeft/swipeRight/swipeUp/swipeDown
});
//=> .pinchIn(function(){}) 缩小
//=> .pinchOut(function(){}) 放大

A标签的默认行为以及阻止

A标签都有那些默认行为
1、超链接:点击A标签可以实现页面的跳转
2、锚点定位:通过hash(哈希)值定位到当前页面指定ID元素的位置

真实项目中我们项用A标签做一个普通的按钮(优势:它的hover样式是兼容所有浏览器的)

锚点定位(哈希定位):定位到当前页面中和hash值(#xxx)相同的元素位置
1、首先在URL地址栏末尾追加一个hash值
2、如果地址栏当中有hash值,浏览器除了页面渲染外,在渲染完成后会默认定位到指定hash值元素位置

阻止A标签的默认行为

1
2
3
//=> 在html中阻止默认行为(最常用)
<a href="javascript:;"></a>
<a href="javascript:void 0;"></a>
1
2
3
4
5
6
7
8
9
10
11
12
//=> 结构当中有a标签 <a href="www.baidu.com" id="link"></a>
//=> 1、当点击会触发click事件
//=> 2、其次按照href中的地址进行页面跳转
link.onclick = function(){
return false; //=> 在函数中返回一个false(只能是false)也可以阻止默认行为
}

//=> 通过事件对象阻止默认行为
link.onclick = function(e){
e = e || window.event;
e.preventDefault?e.preventDefault():e.returnValue=false;
}

事件的传播机制

事件传播有三个阶段
Event.prototype

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

点击某个元素,触发click事件
浏览器在执行元素上绑定的click事件之前
1、从整个页面的document开始向内查找,把元素的祖先元素遍历一遍(为冒泡阶段的传播途径做准备)
2、接下来找到目标元素(找到事件源)把事件源上绑定的方法执行(没有绑定方法就不执行)
3、不仅触发了当前事件源的点击行为,而且浏览器会按照第一阶段规划的传播路线,从内向外把祖先元素的click行为依次触发

当前元素的某个事件行为被触发,它所有的祖先元素(一直到document)的相关事件行为也会被触发(顺序是从内向外),如果组件元素的这个行为绑定了方法,绑定的方法也会被触发,我们把事件的这种传播机制叫做**冒泡传播**

1
2
3
xxx.addEventListener('click', function(){}, true); //=> 当前绑定的方法会在捕获阶段触发

xxx.addEventListener('click', function(){}, false); //=> 当前绑定的方法是在目标阶段或者冒泡阶段才会被触发执行(等价于dom0事件绑定)

mouseover和mouseenter的区别

mouseover:鼠标滑到元素上
mouseenter:鼠标进入元素里面

【1】
mouseover存在事件的冒泡传播机制,而mouseenter浏览器把它的事件传播机制阻止了

【2】
鼠标从父元素进入到子元素
mouseover:先触发父元素的mouseout(因为鼠标已经不再父元素上了,mouseover本意是鼠标在元素上菜算触发),在触发子元素的mouseover(由于冒泡传播机制,导致父元素的mouseover也被重新触发了)

mouseenter:进入,从大盒子进入到小盒子,没有触发大盒子的mouseleave事件,但是也出发了小盒子的mouseenter,浏览器阻止了它的冒泡传播,所大盒子的mouseenter不会被触发

事件委托

利用事件的冒泡传播机制完成(mouseenteru存在委托机制,因为它不存在冒泡传播)

一个容器中的很多元素的同一个事件都要绑定方法,此时我们没有必要在获取所有元素,一个个的绑定方法,我们只需要给最外层元素的这个事件绑定一个方法,这样不管里面哪一个元素的这个事件行为被触发都会利用冒泡传播机制,把最外层容器绑定的那个方法执行,在方法执行的时候,我们可以根据事件源判断出操作的是那个元素,从而做不同的事件(使用事件委托这样完成的操作比一个个的单独事件绑定性能提高50%左右)

DOM2级别事件绑定原理以及兼容处理

初步了解JS中事件绑定的方式

DOM0事件绑定

1
2
3
4
5
6
oBox.onclick = function(e){
//=> this:oBox
e = e || window.event;
}
oBox.onmouseenter = function(){}
...

DOM2事件绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
//=> 标准浏览器中
oBox.addEventListener('click', function(e){
//=> this:oBox

}, false);
//=> false 让当前绑定的方法在冒泡阶段执行(一般都用false)
//=> true 让当前绑定的方法在捕获阶段执行(一般不用)

//=> IE6-8
oBox.attachEvent('onclick', function(e){
//=> 此时绑定的方法都是在冒泡传播阶段执行
//=> e:事件对象,不同于DOM0事件绑定,使用attachEvent绑定方法,当事件触发方法执行的时候,浏览器也会把事件对象做实参传递给函数(传递的值和window.event是相同的),所以IE6-8下获取的事件对象对于:pageX/pageY/target...依然没有,还是存在兼容性(事件对象兼容处理在DOM2中依然存在)
});

有DOM0和DOM2事件绑定,那么DOM1事件绑定呢?
因为在DOM第一代升级迭代的时候,DOM元素的事件绑定方式沿用的是DOM0代绑定的方式,在此版本DOM中,事件绑定没有升级

DOM0事件绑定和DOM2事件绑定的区别

DOM0事件绑定的原理
1、给当前元素对象的某一个私有属性(onxxx)赋值的过程(之前属性默认值是null,如果我们给赋值一个函数,相当于绑定了一个方法)

2、当我们赋值成功(赋值一个函数),此时浏览器会把DOM元素和赋值的函数建立关联,以及建立DOM元素行为操作的监听,当某一个行为被用户触发,浏览器会把相关行为赋值的函数执行

特点:

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
//=> 1、只有DOM元素拥有这个私有属性(onxxx事件私有属性),我们赋值的方法才叫事件绑定,否则属于给当前元素设置一个自定义属性而已

document.body.onclick=function(){}; //=> 事件绑定

/*
* 手动点击页面中的body触发方法执行
* document.body.onclick();
*/

document.body.onsmile=function(){}; //=> 自定义属性

/*
* 只能document.body.onsmile() 这样执行
*/

//=> 2、移除事件绑定的时候,我们只需要赋值为null即可
document.body.onclick=null;

//=> 3、在DOM0事件绑定中,只能给当前元素的某一个事件行为(某一个时间私有属性)绑定一个方法,绑定多个方法,最后一次绑定的会把之前绑定的都替换掉
document.body.onclick=function(){
console.log(1);
}

document.body.onclick=function(){
console.log(2);
}

//=> 点击BODY只能输出2,因为第二次赋值的函数把第一个赋值的函数给替换了

DOM2事件绑定的原理
1、我们DOM2事件绑定使用的addEventListener/attachEvent都是在EventTarget这个内置类的原型上定义的,我们调取使用的时候,首先通过原型链找到这个方法,然后执行完成事件绑定的效果

2、浏览器首先会给当前元素的某一个时间行为开辟一个事件池(时间队列)[其实是浏览器有一个统一的事件池,我们每个元素的某个行为绑定的方法都放在这个事件池中,只是通过相关的标识来区分的]

3、当元素的某一个行为触发,浏览器会到事件池中,把当前存放在事件池中的所有方法,依次按照存放的先后顺序执行

特点:

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
//=> 1、所有DOM0支持的事件行为,DOM2都可以使用,不仅如此,DOM2还支持一些DOM0没有的事件行为
DOMContentLoaded...
document.body.DOMContentLoaded === undefined; //=> DOM0中没有这个属性

window.addEventListener('DOMContentLoaded', function(){
//=> 标准浏览器中兼容这个事件;当浏览器中的dom结构加载完成,就会触发这个事件(也会把绑定的方法执行)
}, false);

window.attachEvent('onDOMContentLoaded', function(){
//=> IE6-8中的DOM2也不支持这个事件
});

//=> 2、DOM2中可以个当前元素的某一个事件行为绑定多个不同的方法(因为绑定的所有方法都存放在事件池中)

function fn1(){
console.log(1);
}
function fn2(){
console.log(2);
}
function fn3(){
console.log(3);
}
document.body.addEventListener('click', fn1, false);
document.body.addEventListener('click', fn3, false);
document.body.addEventListener('click', fn2, false);
document.body.addEventListener('click', fn3, false); //=> 本次向事件池中存储的时候,发现fn3已经在事件池中存在了,不在存储了

//=> 3、DOM2事件绑定的移除比较麻烦一些,需要和绑定的时候:事件类型、绑定方法、以及传播阶段,三个完全一致才可以移除掉
document.body.removeEventListener('click', fn2, false);

document.body.addEventListener('click', function(){
console.log(1);
}, false);

document.body.removeEventListener('click', function(){
console.log(1);
}, false);

//=> DOM2事件绑定需要我们养成“未雨绸缪”的习惯,绑定方法的时候尽量不用你明函数,为后期可能会把方法在事件池中移除掉做准备
1
2
3
4
5
document.body.onclick = function fn(){
console.log(fn); //=> 当前匿名函数的函数本身(给匿名函数设置名字只能在函数本身使用)
console.log(1);
}
fn(); //=> Uncaught ReferenceError: fn is not defined
1
2
3
4
5
6
7
8
document.addEventListener('click', function fn(){
console.log(1);
}, false);
document.addEventListener('click', function fn(){
console.log(2);
}, false);

//=> 点击输出结果是1和2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fn(){
console.log(1);
}
document.addEventListener('click', fn, false);
document.addEventListener('click', fn, false); //=> 第二次这个存储不到事件池中,因为事件池中已经存在fn了

document.onclick = fn; //=> 本次绑定是没有的,被下面的DOM0绑定给替换掉了
document.onclick = function(){
console.log(2);
};
document.addEventListener('click', function(){
console.log(3);
}, false);

//=> 点击输出结果 1 2 3
//=> 1、DOM0和DOM2绑定的方法是毫无关系的(因为是两套不同的处理机制),即使是绑定的方法相同,也是执行两次
//=> 2、谁先绑定的就先执行谁

事件是元素天生自带的行为 click mouseover DOMContentLoaded…
DOM0:浏览器会把一些常用的事件挂载到元素对象的私有属性上,让我们可以实现DOM0事件绑定

DOM2:凡事浏览器给元素天生设置的事件,在DOM2中都可以使用

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

window.onload: 当浏览器中所有的资源内容(DOM结构、文本内容、图片…)都加载完成,触发load事件
1、基于DOM0事件绑定完成的,所以在同一个页面中只能给它绑定一个方法(绑定多个以最后一次绑定的为主)

2、想在一个页面中使用多次,我们应该是基于DOM2事件绑定的

1
2
3
4
5
6
7
8
function fn1(){
//=> 第一个事件
}
function fn2(){
//=> 第二个事件
}
window.addEventListener('load', fn1, false);
window.addEventListener('load', fn2, false);

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

1、JQ中提供的方法,JQ是基于DOMContentLoaded这个事件完成这个操作的

2、JQ中的事件绑定都是基于DOM2事件绑定完成的

3、但是DOMContentLoaded在IE6-8下使用attachEvent也是不支持的,JQ在IE6-8下使用的是onreadystatechange这个事件处理的

DOM2事件绑定的兼容处理

语法上的兼容
[标准]
curEle.addEventListener(‘type’, fn, false);
[IE6-8]
curEle.attachEvent(‘ontype’, fn);

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
/*
* on: 基于DOM2实现事件绑定(兼容所有浏览器)
* @parameter
* curEle: 当前需要操作的元素
* type: 需要绑定方法的事件类型
* fn: 需要绑定的方法
* @return
* 不需要返回值
*/

//=> ON: 给当前元素某个事件绑定方法
let on = function (curEle, type, fn){
if(document.addEventListener) {
//=> 标准浏览器
curEle.addEventListener(type, fn, false);
return;
}

//=> IE6-8
curEle.attachEvent(`on${type}`, fn);
};

//=> OFF: 移除当前元素某个事件绑定的方法
let off = function (curEle, type, fn){
if(document.addEventListener) {
//=> 标准浏览器
curEle.removeEventListener(type, fn, false);
return;
}

//=> IE6-8
curEle.detachEvent(`on${type}`, fn);
}

除了语法上的区别,在执行机制上有一些区别
在IE6-8中使用attachEvent做事件绑定(把方法存放在当前元素指定事件类型的事件池中)

1、顺序问题:当事件行为触发,执行对应事件池中存放的方法的时候,IE低版本浏览器执行方法的顺序是乱序(标准浏览器是按照绑定的先后顺序一次执行的)

2、重复问题:IE低版本浏览器在向事件池中增加方法的时候,没有去重机制,哪怕当前方法已经存放过了,还会重复的添加进去(标准浏览器事件的事件池机制很完善,可以自动去重,已经存放过的方法不允许再添加进来)

3、this问题:IE低版本浏览器中,当事件行为触发,把事件池中方法执行,此时方法中的this指向window,而不是像标准浏览器一样,指向当前元素本身

究其根本:其实都是IE低版本浏览器对其它内置事件池机制的不完善导致的

DOM2事件绑定兼容处理的原理:告别LOW的IE6-8的内置事件池,而是自己创建一个类似标砖浏览器的“自定义事件池”标砖浏览器不需要处理兼容,只有IE6-8才需要处理兼容

自定义兼容事件池

所谓事件池其实就是一个容器,不管是内置的还是自己创建的
IE6-8兼容处理:

1、 on:
没有自定义的事件池,我们需要手动创建一个自定义的事件池把需要绑定的方法,全部存放在自定义的事件池中
在存储的时候我们自己做重复验证,有的话就不在存储了(重复问题就解决了)

2、 off:
把不需要绑定的方法从自定义的事件池中移除掉

3、 run: 我们需要把run放在内置事件池中,当行为触发,需要浏览器把run方法执行,在run中把自定义事件池中存放的所有方法执行
this问题 -> 顺序问题 都解决了

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/*
* on: 基于DOM2实现事件绑定(兼容所有浏览器)
* @parameter
* curEle: 当前需要操作的元素
* type: 需要绑定方法的事件类型
* fn: 需要绑定的方法
* @return
* 不需要返回值
*/

//=> ON: 给当前元素某个事件绑定方法
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先放在内置事件池中(当行为触发,先执行run在run方法中,再把我们自定义方法执行)
//curEle.attachEvent.('on' + type, //function(){
//run.call(curEle, e);
//}); //=> 把run只能向内置事件池中存放一次

curEle.attachEvent('on' + type, run.bind(curEle))
}
var ary = curEle[type + 'Pond'];
//=> 增加之前首先看一下,当前自定义事件池中是否有这个方法,我们不增加了
for (var i = 0; i < ary.length; i++){
if(ary[i] === fn){
return;
}
}

//=> 向自定义的事件池中增加方法
ary.push(fn);

};

//=> OFF: 移除当前元素某个事件绑定的方法
var off = function (curEle, type, fn){
if(document.removeEventListener) {
//=> 标准浏览器
curEle.removeEventListener(type, fn, false);
return;
}
//=> 从自定义事件池中把某个方法移除
var ary = curEle[type + 'Pond'];
if (ary) {
for (var i = 0; i < ary.length; i++){
if(ary[i] === fn){
//=> 这一项就是想移除的
//=> off方法执行从事件池中移除某个方法,不能使用splice移除,这样导致原始数组中的索引都改变了,此时正在执行的run循环,索引和想要获取的方法偏位了=> “数组塌陷”
ary[i] = null;
break;
}
}
}
}

//=> RUN: 把自定义事件池中存放的方法依次执行(并且处理this等问题)
var run = function(e){
//=> this:curEle
//=> e:window.event
//=> 把时间对象e的兼容处理好,在绑定方法执行的时候,我们把处理好的e传递给方法,以后在绑定的方法中直接使用即可,不用在开率兼容问题(类似于JQ中事件绑定的e)
if (type e.target === 'undefined') {
e.target = e.srcElement;
e.which = e.keyCode;
e.pageX = e.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft);
e.pageY = e.clientY + (document.documentElement.scrollTop || document.body.scrollTop);
e.preventDefault = function(){
e.returnValue = false;
}
e.stopPropagation = function(){
e.cancelBubble = true;
}
}

var ary = this[e.type + 'Pond'];
if(ary){
for (var i = 0; i < ary.length; i++){
var item = ary[i];
if (item === null) {
//=> 当前这一项再执行的过程中被off方法移除掉了(null不执行)
ary.splice(i, 1);
i--;
continue;
}
item.call(this, e);
}
}
}

bind方法的兼容

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
function fn(e){
console.log(e, this.tag.Name);
}

// document.body.onclick = fn; //=> this:body
// document.body.onclick = fn.call(obj); //=> 绑定方法的时候,就把fn执行了,把返回的undefined绑定给事件,点击的时候什么都不处理
// document.body.onclick = function(e){
fn.call(obj, e);
} //=> 这种方式可以
// document.body.onclick = fn.bind(obj); //=> bind:不仅把this预先处理为obj,对于fn原本拥有的一些参数(例如E)也没有忽略掉,执行的时候也会传递给fn,而且我们还可以自己传递一些参数
// document.body.onclick = fn.bind(obj, 100, 200); //=> 自己传递的参数不会覆盖默认的参数(先把自己传递的传递给fn,fn中最后一项参数才是默认的e)

Function.prototype.myBind = function myBind (context) {
//=> this:fn(我们需要预先处理this的函数)
//=> context:我们需要预先改变的this值,(如果不传递默认值是window)
context = context || window;
var outerArg = Array.prototypr.slice.call(arguments, 1); //=> 除了context之外传递进来的参数值
var _this = this;
if ('bind' in Function.prototype) {
outerArg.unshift(context);
return _this.bind.apply(_this, outerArg);
}

return function(e){
//=> _this:fn
//=> arguments:返回的匿名函数接收到的参数值
var innerArg = Array.prototype.slice.call(arguments, 0);
outerArg = outerArg.concat(innerArg);
_this.apply(context, outerArg);
}
};
fn.myBind(obj, 100, 200);
document.body.onclick = fn.myBind(obj, 100, 200);
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
//=> ES6 封装DOM2兼容处理
~function(){
Function.prototype.myBind = function (context=window, ...outer) {
if ('bind' in this) {
return this.bind(...arguments);
}
return (...inner) => this.apply(context, outer.concat(inner))
};
let on = function (curEle, type, fn) {
if (document.addEventListener) {
curEle.addEventListener(type, fn, false);
return;
}

if (typeof curEle['pond' + type] === 'undefined') {
curEle['pond' + type] = [];
curEle.attachhEvent('on' + type, run.myBind(curEle));
}

let pondAry = curEle['pond' + type];
for (let i = 0; i < pondAry.length; i++) {
if (pondAry[i] === fn) {
return;
}
}
pondAry.push(fn);
};

let off = function (curEle, type, fn) {
if (document.removeEventListener) {
curEle.removeEventListener(type, fn, false);
return;
}

let pondAry = curEle['pond' + type];
if (!pondAry) return;
for (let i = 0; i < pondAry.length; i++) {
let itemFn = pondAry[i];
if (itemFn === fn) {
pondAry[i] = null;
break;
}
}
};

let run = function (e) {
e = e || window.event;
if (!e.target) {
e.target = e.srcElement;
e.pageX = e.clientX + document.documentElement.scrollLeft || document.body.scrollLeft;
e.pageY = e.clientY + document.documentElement.scrollTop || document.body.scrollTop;
e.which = e.keyCode;
e.preventDefault = function () {
e.returnValue = false;
};
e.stopPropagation = function () {
e.cancelBubble = true;
};
}

let pondAry = this['pond' + e.type];
if (!pondAry) return;
for (let i = 0; i < pondAry.length; i++) {
let itemFn = pondAry[i];
if (itemFn === null) {
pondAry.splice(i, 1);
i--;
coontinue;
}
itemFn.call(this, e);
}
};

window.$event = {
on: on,
off: off
}
}()
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//=> 复习拖拽
let down = function (e) {
//=> this->oBox
this.strX = e.clientX;
this.strY = e.clientY;
this.strL = this.offsetLeft;
this.strT = this.offsetTop;

//=> 绑定方法的时候,向事件池中存放的是执行myBind返回的匿名函数(问题:移除的时候不知道移除谁)
this._MOVE = move.myBind(this);
this._UP = up.myBind(this);
$event.on(document, 'mousemove', this._MOVE);
$event.on(document, 'mouseup', this._UP);

//=> 结束当前盒子正在运行的动画
clearInterval(this.timerDrop);
clearInterval(this.timerFly);

};

let move = function (e) {
//=> this-> oBox
let curL = e.clientX - this.strX + this.strL,
curT = e.clientY - this.strY + this.strT;
curL = curL < 0 ? 0 : (curL > maxL ? maxL : curL);
curT = curT < 0 ? 0 : (curT > maxT ? maxT : curT);
this.style.left = curL + 'px';
this.style.top = curT + 'px';

//=> 计算水平方向的速度
if (!this.preFly) {
this.preFly = this.offsetLeft;
} else {
this.speedFly = this.offsetLeft - this.preFly;
this.preFly = this.offsetLeft
}

};

let up = function (e) {
//=> this -> oBox
$event.off(document, 'mousemove', this._MOVE);
$event.off(document, 'mouseup', this._UP);

//=> 松开鼠标后,让盒子运动
moveFly.call(this);
moveDrop.call(this);
};

let moveFly = function () {
//=> this:oBox
let speedFly = this.speedFly;
this.timerFly = setInterval(()=>{
//=> 由于JS盒子模型属性获取的结果是证书(会四舍五入),所以速度如果小于0.5,我们本次加的速度值在下一次获取的时候还会被忽略掉(此时盒子应该是不动的)

if (Math.abs(speedFly) < 0.5) {
clearInterval(this.timerFly);
return;
}


//=> 指数衰减的移动(速度乘以小于1的值肯定越来越小)
speedFly *= 0.98;
let curL = this.offsetLeft + speedFly;
if (curL > maxL || curL < 0) {
speedFly *= -1; //=> 控制反方向运动
curL = curL > maxL ? maxL : (curL < 0 ? 0 : null); //=> 控制不管怎么总都不要超过边界
}
this.style.left = curL + 'px';
}, 17);
}

//=> 垂直方向飞
let moveDrop = function () {
let speedDrop = 10,
flagDrop = 0;
this.timerDrop = setInterval(()=>{
if (flagDrop >= 2) {
clearInterval(this.timerDrop);
return;
}
speedDrop *= 0.98;
speedDrop += 9.8;
let curT = this.offsetTop + speedDrop;
if (curT > maxT) {
speedDrop *= -1;
curT = maxT;
flagDrop++;
} else {
flagDrop = 0;
}
this.style.top = curT + 'px';
}, 17);
}

let oBox = document.getElementById('box'),
maxL = (document.documentElement.clientWidth || document.body.clientWidth) - oBox.offsetWidth,
maxT = (document.documentElement.clientHeight || document.body.clientHeight) - oBox.offsetHeight;


$event.on(oBox, 'mousedown', down);