JS知识体系梳理-5

262 阅读17分钟

事件

定义:事件就是一件事情或者一个行为(对于元素来说,它的很多事件都是天生自带的),只要我们去操作这个元素,就会触发这些行为

事件就是元素天生自带的行为,我们操作元素,就会触发相关的事件行为

oBox.onclick=function(){};

元素天生自带的事件

鼠标事件

click

点击,PC端是点击,移动端的click代表单击

移动端使用click会有300MS延迟的问题,第一次点击后,如果在300MS内又点击了,浏览器认为其不是单击

dblclick

双击

mouseover

鼠标经过

mouseout

鼠标移出

mouseenter

鼠标进入

mouseleave

鼠标离开

mousemove

鼠标移动

mousedown

鼠标按下(鼠标左右键都起作用,它是按下即触发,click是按下抬起才会触发,而且是先把down和up触发,才会触发click)

mouseup

鼠标抬起

mousewheel

鼠标滚轮滚动,只要鼠标滚轮在滚动;而scroll是滚动条在滚动,让滚动条滚动有多种方式,可以用鼠标移动,也可以用滚轮移动,也可以用pagedown

键盘事件

keydown

键盘按下,ASCII码中对应的十进制的值(字母大写,其他如数字没有区别)

keyup

键盘抬起

keypress

和keydown类似,只不过keydown返回的是键盘码,keypress返回的是ASCII码值,ASCII码中对应的十进制的值(字母小写,其他如数字没有区别)

input

由于PC端有实体物理键盘,可以监听到键盘的按下和抬起,但是移动端是虚拟触控键盘,所以keydown,keyup在大部分手机上都没有,我们使用input事件统一代替他们(内容改变事件)

表单元素常用事件

focus

获取焦点

blur

失去焦点

change

内容改变

其它常用事件

load

加载完成

unload

用户离开页面

  • 点击某个离开页面的链接 -在地址栏中输入了新的URL
  • 使用前进或者后退按钮 -关闭浏览器 -重新加载页面

beforeunload

优先于unload事件,可以阻止onunload事件的执行

beforeunload事件在新窗口被打开的时候并不会执行,只在刷新和关闭时执行

scroll

滚动条滚动事件

resize

大小改变事件

window.onresize=function(){};
当浏览器窗口大小发生改变,会触发这个事件,执行对应的事情
移动端手指事件

单手指操作

touchstart

手指按下

touchmove

手指移开

touchend

手指离开

touchcancel

因为意外情况导致手指操作取消

多手指操作

gesturestart

多手指按下

gesturechange

手指改变

gestureend

手指离开

H5的AUTO/VIDEO音视频事件

canplay

可以播放,播放过程中可能出现由于资源没有加载完成导致的卡顿

canplaythrougth

资源加载完成,可以正常无障碍播放

页面(浏览器)事件

移动端和PC端都有

load

加载成功

error

加载失败

readystatechange

AJAX状态改变

scroll

滚动

resize

改变大小 window.onresize浏览器窗口大小改变触发

事件绑定

给元素天生自带的事件行为绑定方法,当事件触发,会把对应的方法执行

oBox.onclick=function(){};

DOM0级事件绑定

语法:[element].onxxx=function(){};

每一个元素对象都是对应类的实例,浏览器为其设置了很多私有属性和公有的属性方法,而onclick就是其中的一个私有属性(事件类私有属性,还有很多其它的事件私有属性),这些属性默认值是null

原理:就是给元素的某一个事件私有属性赋值(浏览器会建立监听机制,当我们触发元素的某个行为,浏览器会自己把属性中赋的值去执行)

DOM0事件绑定:只允许给当前元素的某个事件行为绑定一个方法,多次绑定,后面绑定的内容会替换前面绑定的,以最后一次绑定的方法为准

box.onclick=function(){
	console.log(1);
}

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

最后输出了2

DOM2级事件绑定

增加语法:[element].addEventListener("xxx",function(){},false);

移除语法:[element].removeEventListener("xxx",function(){},false);

addEventListener,removeEventListener这两个方法都是EventTarget.prototype上的方法

IE6~8增加:[element].attachEvent("onxxx",function(){});

IE6~8移除:[element].detachEvent("onxxx",function(){});


DOM2事件可以给当前元素的某一个事件行为绑定"多个不同的方法"

    function fn1(){console.log(1);}
    function fn2(){console.log(2);}
    function fn3(){console.log(3);}
    function fn4(ev){
        console.log(4,this===box,ev.target);
        box.removeEventListener('click',fn5);
        box.removeEventListener('click',fn8);
    }
    function fn5(){console.log(5);}
    function fn6(){console.log(6);}
    function fn7(){console.log(7);}
    function fn8(){console.log(8);}
    function fn9(){console.log(9);}
    function fn10(){console.log(10);}
    function fn11(){console.log(11);}
    function fn12(){console.log(12);}

    box.addEventListener('click',fn1);
    box.addEventListener('click',fn3);
    box.addEventListener('click',fn5);
    box.addEventListener('click',fn7);
    box.addEventListener('click',fn9);
    box.addEventListener('click',fn2);
    box.addEventListener('click',fn2);//=>重复
    box.addEventListener('click',fn2);//=>重复
     box.addEventListener('mouseenter',fn2);//=>增加到事件池中的
    box.addEventListener('click',fn4);
    box.addEventListener('click',fn6);
    box.addEventListener('click',fn8);
    box.addEventListener('click',fn10);
    box.addEventListener('click',fn11);
    box.addEventListener('click',fn12);


//=>输出结果
//2,1,3,5,7,9,2,4,6,10,11,12

DOM2事件要注意堆内存的问题

//=>变量提升阶段,fn1被声明加定义,地址指向第二个函数(console.log(2))的空间地址aaa,接下来JS代码自上而下执行,给box的click事件绑定了一个方法(该方法是addEventListener的一个私有变量,只是和全局中的指向同一个堆内存),方法的堆内存指向aaa,同时向浏览器的事件池中添加一个click事件的方法,再往下执行,执行removeEventListener方法,并把fn1也传给他作为私有变量,但是也是指向全局下的空间地址,执行时找到fn1的空间地址,把它从事件池中移除,此时全局下的fn1还是存在的,只是从事件池中移除了而已
function fn1(){
    console.log(1);
}
function fn1(){
    console.log(2);
}
box.addEventListener("click",fn1,false);
setInterval(function(){
    box.removeEventListener("click",fn1,false);
},5000);
setTimeout(function () {
    fn1();
},6000);
//=>页面加载完成5s内点击box都会执行fn1,5s后fn1从事件池内清除,但是全局下的fn1还是存在的,所以第6s还是会输出2

标注 VS IE低版本

标准:addEventListener/removeEventListener

IE低版本:attachEvent/detachEvent

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

  • 1.机制不一样

    • DOM0采用的是给私有属性赋值,所以只能绑定一个方法
    • DOM2采用的是事件池机制,所以能绑定多个不同的方法
  • 2.移除的操作

    • DOM0级事件只要手动赋值为NULL即可移除,所以不必须要考虑绑定的是什么方法
    • DOM2在移除的时候必须清楚要移除哪一个方法,才能在事件池中移除掉,所以基于DOM2做事件绑定,尽量不要绑定匿名函数,都绑定实名函数
box.onclick=function(){};
box.onclick=null;//=>赋值为null就移除了(所以不需要考虑绑定的是谁)

box.addEventListener("click",function(){console.log(1);});

box.removeEventListener("click",function(){
	console.log(1);
})
//=>移除的并不是上面刚刚增加的方法,addEventListener和removeListener都是EventTarget原型上的方法,相当于调取原型上的方法并执行,所以括号里的相当于实参,那么就是这个函数的私有变量方法被执行的时候,fn指向一个堆内存,下面的被执行的时候,fn又指向另外一个堆内存
  • 3.DOM2事件绑定中增加了一些DOM0无法操作的事件行为,例如:DOMContentLoaded事件(当页面中的HTML结构加载完成就会触发执行)
box.addEventListener("DOMCotentLoaded",fn);//=>可以

box.onDOMContentLoaded=fn;//=>不可以,box没有这个属性
--------------
window.onload=function(){
	//=>当页面中的资源都加载完成(HTML结构加载完、CSS和JS等资源加载完成等)才会触发执行,而且只能执行一次
}
window.addEventListener("load",function(){});//=>这样处理就可以执行多次了

--------
jQuery中传递的selector是函数也是模拟了window.onload的方法,但是等到页面中的HTML结构加载完成就会执行,jQuery中使用的是DOM2事件,就是基于DOMContentLoad完成的,所以可以执行多次
else if ( jQuery.isFunction( selector ) ) {
			return typeof rootjQuery.ready !== "undefined" ?
				rootjQuery.ready( selector ) :
				selector( jQuery );
		}
rootjQuery=jQuery(document);//=>把document转换为JQ对象
所以$(function(){})相当于$(document).ready(function(){})
//=>原理:基于DOMContentLoaded完成的(IE中用的是onreadystatechange监听的,在document.readyState==="complete"的时候执行函数)

jQuery.ready.promise = function( obj ) {
	if ( !readyList ) {
		readyList = jQuery.Deferred();
		if ( document.readyState === "complete" ) {//=>标准浏览器中的
			setTimeout( jQuery.ready );
		} else if ( document.addEventListener ) {
			document.addEventListener( "DOMContentLoaded", completed, false );
			window.addEventListener( "load", completed, false );
		} else {//=>IE中的
			document.attachEvent( "onreadystatechange", completed )
			window.attachEvent( "onload", completed );

JQ中的事件绑定

on/off:基于DOM2事件绑定实现时间的绑定和移除(兼容所有的浏览器)

$(document).on("click",fn);
$(document).off("click",fn);

one:只绑定了一次,第一次执行完成后,会把绑定的方法移除(基于ON/OFF完成的,在on的事件的末尾加上移除这个事件)

function fn(){
	$(document).off("click",fn);
}
$(document).on("click",fn);

click/mouseover/mouseout...:JQ提供快捷绑定方法,但是这些方法最后都是基于ON/OFF完成的

$(document).click(fn);

delegate:事件委托方法

$(document).delegate("#box","click",fn)
//=>把点击事件委托给document,不管点击文档中的哪一个元素,都会触发文档的点击行为,第一个参数代指事件源是#box我们执行fn这件事

bind/unbind:正常绑定

事件中低版本浏览器和标准浏览器的区别

  • 1、:给事件绑定的方法被触发是低版本浏览器不会给事件传递事件对象,需要通过window.event获取,对应的事件对象上的一些属性的兼容问题

  • 2、:冒泡传播的机制,标准浏览器是一直传播到window,IE高版本浏览器是传播到window,但是少了document,IE低版本浏览器一直传播到html为止

  • 3、:DOM2事件的相关兼容问题:语法、this指向、重复问题、事件池顺序

事件对象

事件对象中记录了很多属性名和属性值(存储当前鼠标操作的信息),这些信息中包含了当前操作的基础信息,例如:鼠标点击位置的x/y轴坐标,鼠标点击的是谁(事件源)等信息

每一次事件触发浏览器传递进来的事件对象其实是一个实例,例如MouseEvent是MouseEvent这个类的实例

MouseEvent

鼠标事件对象

box.onclick=function(ev){
	console.log(ev.target);
};

KeyboardEvent

键盘事件对象

button.onkeydown=function(ev){
	console.log(ev.code);
};

Event

普通事件对象

window.onscroll=function(ev){
	console.log(ev.preventDefault);
}

FocusEvent

表单事件对象

myInput.onfocus=function(ev){
	console.log(ev.target);
};

WheelEvent

鼠标滚轮滚动对应的时间对象

myInput.onmousewheel=function(ev){
	console.log(ev.target);//=>body
};
MouseEvent/wheelEvent中常用的属性

type

记录的是事件类型

path

记录的是事件冒泡传播的路径 [div#box,body,html,document,Window]

target

事件源(操作的是那个元素),MouseEvent实例上的私有属性

低版本浏览器中不存在target,所以低版本浏览器下,使用srcElement

clientX/clientY

当前鼠标出发点距离当前可视窗口左上角的x/y轴轴坐标,MouseEvent实例上的私有属性

pageX/pageY

当前鼠标出发点距离BODY(第一屏幕)左上角的X/Y轴坐标,MouseEvent实例上的私有属性

低版本浏览器中不存在pageX/pageY,所以低版本浏览器下,使用scrollTop+clientY

preventDefault

阻止默认行为,不兼容IE6~8,Event这个类原型上的方法

低版本浏览器中不存在preventDefault,所以低版本浏览器下,使用returnValue=false

stopPropagation

阻止事件的冒泡传播,不兼容IE6~8,Event这个类原型上的方法

低版本浏览器中不存在stopPropagation,所以低版本浏览器下,使用cancelBubble=true

KeyboardEvent中常用的属性

code

当前按键"keyE"

key

当前按键"e"

which/keyCode

当前按键的ASCII编码

keyCode是用于兼容IE的

let code=ev.which||ev.keyCode;

常用键盘码

**`左-上-右-下`**:37-38-39-40
**`Backspace`**:8
**`Enter`**:13
**`Space`**:32
**`数字键`**:48~57
**`小写字母`**:65~90

`Delete`:46
`Shift`:16
`Alt`:18
`Ctrl`:17
`ESC`:27

事件的默认行为

事件本身就是天生就有的,某些事件触发,即使你没有绑定方法,也会存在一些效果,这些默认效果就是"事件的默认行为"

举例1--A标签

A标签的点击操作就存在默认行为

1、 页面跳转 2、锚点定位(HASH定位[哈希定位])

  • 首先会在当前页面URL地址栏末尾设置一个HASH值,浏览器检测到HASH值后,会默认定位到当前页面中ID和HASH相同的盒子的位置(基于HASH值我们还可以实现SPA单页面应用)

举例2--input标签

1.输入内容可以呈现到文本框中

2.输入内容的时候会把之前输入的一些信息呈现出来(并不是所有浏览器和所有情况都有)

举例3--submit标签

1.点击按钮页面会刷新,在form中设置action属性,点击submit,会默认按照action指定的地址进行页面的跳转,并且把表单中的信息传递进去(非前后端分离中,由服务器进行页面渲染,由其它语言实现数据交互,一般都是这样处理)

<form action="http://www.zhufengpeixun.cn/">
	<input type="submit" value="提交">
</form>
如何阻止默认行为

阻止A标签中的默认行为

在结构中阻止

<a href="javascript:;">阻止默认行为</a>;
javascript:void 0/undefined/null...;

//=>冒号和分号之间可以用其它占位

在JS中也可以阻止,给其click事件绑定方法,当我们点击A标签的时候,先触发click事件,其次才会执行自己的默认行为

link.onclick=function(ev){
	ev=ev||window.event;
	ev.preventDefault?ev.preventDefault():ev.returnValue=false;
}

-------------
link.onclick=function(ev){
	ev=ev||window.event;
	return false;//=>原理:点击A的时候,首先触发的是click,其次才会触发它的默认行为(我们在click中返回false,其意思是阻止下一步继续操作)
}

input text文本框内最多输入6位

myInput.onkeydown = function (ev) {
    ev = ev || window.event;
    let value = ev.target.value;
    let reg = /^\s+|\s+$/g;
    if (reg.test(value)) {
        value = value.replace(reg, "");
    }
    let len = value.length;
    if (len >= 6) {
        let reg = /^(8|13|46|37|38|39|40)$/;
        if (!reg.test(ev.which)) {
            ev.preventDefault ? ev.preventDefault() : ev.returnValue = false;
        }
    }
};

事件的传播机制

冒泡传播

触发当前元素的某一个事件(点击事件)行为,不仅当前元素事件行为触发,而且其祖先元素的相关事件行为也会依次被触发,这就是"事件的冒泡传播机制"

事件传播的三个阶段

1、捕获阶段:点击inner的时候,首先会从最外层开始向内查找(找到操作的事件源),查找的目的是,构建出冒泡传播阶段需要传播的路线(查找是按照HTML层级结构找的)

2、目标阶段 把事件源的相关操作行为触发(如果绑定了方法,则把方法执行)

3、冒泡传播 按照捕获阶段规划的路线,自内而外,把当前事件源的祖先元素的相关事件行为依次触发(如果某个祖先元素事件行为绑定了方法,则把方法执行,没绑定方法,行为触发了,什么都不做,继续向上传播即可)

window.onclick = function () {
    console.log('window');
};

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

document.documentElement.onclick = function () {
    console.log('html');
};
outer.onclick = function (ev) {
    console.log('outer');//=>TRUE
};

inner.onclick = function (ev) {
    console.log('inner');
};
//=>inner outer html document window

不同浏览器对于最外层祖先元素的定义是不一样的

  • 谷歌:window->document->html->body
  • IE高:window->html->body
  • IE低:html->body...

1、事件对象是用来存储当前本次操作的相关信息,和操作有关,和元素无必然关联

2、当我们基于鼠标或者键盘等操作的时候,浏览器会把本次操作的信息存储起来(标准浏览器存储到默认的内存中(自己找不到),IE低版本存储到window.event中了),存储的值是一个对象(堆内存);操作肯定会触发元素的某个行为,也就会把绑定的方法执行,此时标准浏览器会把之前存储的对象(准确来说是堆内存地址),当做实参传递给每一个执行的方法,所以操作一次,即使再多方法中都有EV,但是存储的值都是一个(本次操作信息的对象而已)

//=>三个click事件用的是同一个事件对象
let aa = null;
document.body.onclick = function (ev) {
    console.log('body', ev, ev === aa);//=>TRUE
};

outer.onclick = function (ev) {
    console.log('outer', ev, ev === aa);//=>TRUE
};

inner.onclick = function (ev) {
    ev = ev || window.event;
    aa = ev;
};

想要阻止冒泡传播的话,可以通过事件对象的stopPropagation方法(Event原型上的)

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

outer.onclick = function (ev) {
    ev = ev || window.event;
    ev.stopPropagation ? ev.stopPropagation() : ev.cancelBubble = true;
    console.log('outer', ev);
};

inner.onclick = function (ev) {
    console.log('inner', ev);
};

onmouseenter和onmouseover的区别

1、over属于滑过(覆盖)事件,从父元素进入到子元素,属于离开了父元素,会触发父元素的out,触发子元素的over

2、enter属于进入,从父元素进入子元素,并不算离开父元素,不会触发父元素的leave,触发子元素的enter

3、enter和leave阻止了事件的冒泡传播,而over和out还存在冒泡传播的,所以对于父元素嵌套子元素这种情况,使用over会发生很多不愿意操作的事情,此时我们使用enter会更加简单,操作方便,所以真实项目中enter的使用会比over多

4、over和enter,over先触发;out和leave,out先触发

事件委托(事件代理)

利用事件的冒泡传播机制,如果容器的后代元素中,很多元素的点击行为(其它事件行为也是)都要做一些处理,此时我们不需要再像以前那样一个一个获取一个一个的绑定了,我们只需要给容器的click绑定方法即可,这样不管点击的是哪个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而做不同的事情

发布订阅设计模式(观察者模式)

准备一个容器,把到达指定时候要处理的事情,首先一一的增加到容器中(发布计划并且向计划表中订阅方法),当到达指定时间点,通知容器中的方法依次执行即可

发布订阅设计模式源码

(function(){
	class Subscribe{
		constructor(){
			this.pond=[];
		}
		add(fn){
			let pond=this.pond,
				isExist=false;//=>代表fn在pond中不存在,如果不存在就加入pond中
			for(let i=0;i<pond.length;i++){//=>那fn和pond中的每一项进行比较,如果有相等的项,证明fn在pond中已经存在了,那么让isExist变成true就不用往里面存了
				let item=pond[i];
				item===fn?isExist=true:null;
				if(isExist){
					return;
				}
			}
			!isExist?pond.push(fn):null;
		}
		remove(fn){
			let pond=this.pond;
			for(let i=0;i<pond.length;i++){
				let item=pond[i];
				item===fn?pond[i]=null:null;//=>移除的时候赋值为null而不是直接删除掉,因为如果执行容器的时候,假如同时操作了remove函数,那会导致数组的索引发生改变
			}
		}
		fire(...arg){
			let pond=this.pond;
			for(let i=o;i<pond.length;i++){
				if(item===null){//=>把从容器中移除的事件去除掉,而且不执行
					pond.splice(i,1);
					i--;
					continues;
				}
				item(...arg);
			}
		}
	}
	window.Subscribe=Subscribe;
})()

jQuery中提供的发布订阅设计模式

let $plan=$.Callbacks();//=>创建一个空的计划表:空容器
setTimeout(function(){
	$plan.fire(10,20);//=>fire就是通知容器中的方法按照顺序依次执行的:10和20是执行容器中每一个方法的时候,都给他们传递两个参数值
},1000);

function fn(){};

$plan.add((x,y)=>{//ADD是向容器中增加方法
	console.log(1);
});
$plan.add(fn);

$plan.remove(fn);//=>remove是从容器中移除方法

Function原型上bind的原理

执行bind方法,会形成一个不销毁的私有栈内存(AAA),返回一个新的匿名函数(每一次返回的都不不一样,不是相同的堆内存)

Function.prototype.myBind=function myBind(context,...arg){
//=>this:当前调取bind方法的函数
//=>context:把调取方法的函数中的this改为context
//=>arg:需要传递给调取方法的函数的实参
//=>以上这些都是执行bind方法中的私有变量
   let _this=this;
   return function anonymous(...innerArg){
//=>inneArg可能有值,可能没有值,如果把bind的返回结果给元素的时间绑定,则事件触发,执行这个匿名函数,会把事件对象传递进来ev  
         _this.apply(context,arg.concat(innerArg));
	   console.log(this);//=>如果绑定了事件,这里的this指的是绑定事件的元素
   }
}
function fn(){
    console.log(1);
}
let obj={};
box1.onclick=fn.myBind(obj,10);//=>bind方法中的this指的是fn
相当于 box1.onclick=function anonymous(ev){
	fn.apply(obj,[10,ev]);
	//=>匿名函数中的this指的是box1
}
//=>当我们触发box的点击行为,执行的是返回的匿名函数