js 事件
# js 事件相关概念
# 事件机制组成
1.事件源:即事件的发送者
2.事件:事件源发出的一种信息或状态
3.事件侦听者:对事件作出反应的对象
# 事件流
事件流描述的是从页面中接受事件的顺序
“DOM2 级事件”规定的事件流包括三个阶段: 事件捕获阶段、处于目标阶段和事件冒泡阶段。从最不具体的元素(document) 捕获到最具体的元素,再从最具体的元素冒泡到最不具体的元素。最后在冒泡阶段对事件进行相应。
IE 的事件流是事件冒泡流(event bubbling),而 Netscape(网景开发团队) 的事件流是事件捕获流(event capturing)。
为什么一般在冒泡阶段,而不是在捕获阶段注册监听?
为事件代理(委托)提供条件,即事件代理依赖事件冒泡。
- 取消事件冒泡 event.stopPropagation()
- 取消默认行为 event.preventDefault()
- 事件代理的原理:event 有个 target 属性,永远指向最具体的元素
# 事件冒泡
事件冒泡:是自下而上的去触发事件
设置冒泡:通过 addEventListener() 的第三个属性来设置 false,(默认值为 false)事件冒泡
阻止冒泡:e.stopPropagation
支持事件冒泡的事件:
click,input
keydown,keyup
mousedown,mousemove,mouseout,mouseover,mouseup
scroll,select
drag 相关事件 dragstart 、 drag 、 dragenter 、 dragexit 、 dragleave 、 dragover 、 drop 、 dragend 均冒泡
- 不支持冒泡的事件:
blur,focus
mouseenter,mouseleave
resize
# 事件捕获
事件捕获指的是从 document 到触发事件的那个节点,即自上而下的去触发事件
通过 addEventListener() 的第三个属性来设置 true,事件捕获
# 事件委托
事件委托就是利用 js 事件冒泡的特性,将内层元素的事件委托给外层处理
优点:
可以大量节省内存占用,减少事件注册
可以实现当新增子对象时无需再次对其绑定事件
最适合采用事件委托技术的事件包括 click、mousedown、mouseup、keydown、keyup 和 keypress
# js 执行机制(事件循环)
事件的执行顺序,是先执行宏任务,然后执行微任务
宏任务队列可以有多个,微任务队列只有一个
宏任务耗费的时间是大于微任务的
JavaScript 事件循环机制分为浏览器和 Node 事件循环机制,两者的实现技术不一样,
浏览器 Event Loop 是 HTML 中定义的规范,Node Event Loop 是由 libuv 库实现。这里主要讲的是浏览器部分。
Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。
- JS 调用栈
JS 调用栈是一种后进先出的数据结构。当函数被调用时,会被添加到栈中的顶部,执行完成之后就从栈顶部移出该函数,直到栈内被清空。
- 同步任务、异步任务
JavaScript 单线程中的任务分为同步任务和异步任务。同步任务会在调用栈中按照顺序排队等待主线程执行,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。任务队列是先进先出的数据结构。
- Event Loop
调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作,就形成了事件循环。
事件循环过程:
取且仅取一个宏任务来执行(第一个宏任务就是 script 任务)。执行过程中判断是同步还是异步任务,如果是同步任务就进入主线程执行栈中,如果是异步任务就进入异步处理模块,异步任务则会在异步有了结果后将注册的回调函数添加到任务队列(消息队列)中等待主线程空闲的时候,也就是栈内被清空的时候,被读取到栈中等待主线程执行。进入任务队列后,按照宏任务和微任务进行划分,划分完毕后,执行下一步。
如果微任务队列不为空,则依次取出微任务来执行,直到微任务队列为空(即当前 loop 所有微任务执行完),执行下一步。
进入下一轮 loop 或更新 UI 渲染。
# js 任务
- 宏任务(macro)task 主要包含:
script( 整体代码)、
setTimeout、
setInterval、
I/O、UI 交互事件、
setImmediate(Node.js 环境或 IE10)
- 微任务 microtask 主要包含:
Promise 的 then 或 catch,
MutationObserver.
process.nextTick(Node.js 环境)
Object.observer(已废弃),
宏任务优先级,主代码块 > setImmediate > MessageChannel > setTimeout / setInterval
微任务优先级,process.nextTick > Promise > MutationObserver
# 消息队列
异步过程中,工作线程在异步操作完成后需要通知主线程,那么通知机制是怎么样的呢?就是利用消息队列和事件循环。
工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。
消息队列: 消息队列是一个先进先出的队列,里面放着各种各样的消息;
事件循环: 事件循环是指主线程重复从消息队列中取消息,执行的过程。
实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
事件循环用代码表示大概是这样的:
while (true) {
var message = queue.get();
execute(message);
}
2
3
4
那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:
消息就是注册异步任务时添加的回调函数。 再次以异步 AJAX 为例,假设存在如下的代码:
\$.ajax("http://segmentfault.com", function(resp) {
console.log("我是响应:", resp);
});
2
3
主线程在发起 AJAX 请求后,会继续执行其他代码。AJAX 线程负责请求 segmentfault.com,拿到响应后,它会把响应封装成一个 JavaScript 对象,然后构造一条消息:
// 消息队列中的消息就长这个样子
var message = function() {
callbackFn(response);
};
2
3
// 其中的 callbackFn 就是前面代码中得到成功响应时的回调函数。 主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是 message 函数),并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX 线程在收到 HTTP 响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。
# JS 事件的操作
# 添加监听事件和删除事件
DOM 2 级事件定义了两方法:用于处理添加事件和删除事件的操作:
添加事件 addEventListener()
删除事件 removeEventListener()
所有 DOM 节点中都包含这两个方法,并且他们都包含 3 个参数:
要处理的事件方式(例如:click,mouseover,dbclick.....)
事件处理的函数,可以为匿名函数,也可以为命名函数(但如果需要删除事件,必须是命名函数)
一个布尔值,(默认值为 false)代表是处于事件冒泡阶段处理还是事件捕获阶段(true:表示在捕获阶段调用事件处理程序;false:表示在冒泡阶段调用事件处理程序)
# JS 阻止事件冒泡
w3c 的方法是e.stopPropagation()
,IE 则是使用e.cancelBubble = true
阻止冒泡代码:
function stopBubble(e) {
//如果提供了事件对象,则这是一个非IE浏览器
if (e && e.stopPropagation)
//因此它支持W3C的stopPropagation()方法
e.stopPropagation();
//否则,我们需要使用IE的方式来取消事件冒泡
else {
window.event ? (window.event.cancelBubble = true) : "";
}
}
2
3
4
5
6
7
8
9
10
# 取消默认事件
w3c 的方法是e.preventDefault()
,IE 则是使用e.returnValue = false;
javascript 的return false
只会阻止默认行为,而是用 jQuery 的话则既阻止默认行为又防止对象冒泡。
例子:阻止 a 标签的跳转
var a = document.getElementById("testA");
a.onclick = function(e) {
if (e.preventDefault) {
e.preventDefault();
} else {
window.event.returnValue == false;
}
};
2
3
4
5
6
7
8
# 监听滚动条事件
window.addEventListener("scroll", winScroll, false);
function winScroll() {
var scrollTop = document.documentElement.scrollTop;
var clientHeight = document.documentElement.clientHeight;
var scrollHeight = document.documentElement.scrollHeight;
if (scrollHeight >= clientHeight + scrollTop) {
// 到底部了
}
}
2
3
4
5
6
7
8
9
10
11
# js 事件问题汇总
# 1.mouseover 和 mouseenter 区别
# 2.事件绑定与事件监听的区别
事件绑定:同一个对象,进行多次事件绑定,则后绑定的事件会覆盖之前绑定的事件,总之只能执行最后一个事件. -------- onclick
事件监听:同一个对象,进行多次事件监听,都会执行 -------addEventListener
# 2. javascript 中 script 整体代码属于宏任务怎么理解呢?
请问 script 整体代码是宏任务怎么理解呢,是说像 console.log()这样的同步代码也应该属于宏任务吗?还请大神指点
因为代码不是任务,所以 console.log()这句代码也不是任务,更不是宏任务。
虽然社区有人总喜欢列举 Promise.then、MutationObserver 是微任务(这里没列举完全),但是没捋清事件循环机制的前提下,死记硬背这些个“微任务”的话,面试官很容易借此挖坑请你跳。
所谓任务,浅显来说就是代码块开始执行的入口(确切地说,是函数栈的入口,但是栈的概念较为复杂,不表)。而在 JS 里,除了“script 整体代码块”之外,所有代码块的入口都是“回调函数”,回调函数被注册到事件后不会马上被执行,而是保存在一个神秘的的地方,保存起来待执行的才能算“任务”,然后才有宏/微任务之分。
“script 整体代码块”的特殊之处,在于它的入口不是回调函数,但是我们可以想象它被装在一个隐形的函数里,作为回调函数被注册到某个事件里(大概是它解析完成之后会触发的一个事件),这时候这个隐形的函数就成为了一个任务。
这只是一个浅显的说法,事件循环相关的知识请自行补全。
# 3. e.target 和 e.currentTarget 的区别
target 触发事件的源事件
currentTarget 事件触发的当前事件(当前事件,可能是触发事件的源组件,可能是触发的事件组件(即触发事件源组件的子元素)),
1、如果绑定的事件所在组件没有子元素,则用 e.target===e.currentTarget 一样;
2、如果事件绑定在父元素中,且该父元素有子元素,
当用e.currentTarget时,不管点击父元素所在区域还是子元素(当前事件),都正确执行,
若用e.target时,点击父元素所在区域无错,点击子元素区域,执行报错-------》
报错的原因是事件没绑定在子元素上,是在父元素上,子元素要用e.currentTarget才正确!
# js 单线程问题
# 1、什么是单线程?
单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。简单来说,即同一时间只能做一件事件。
# 2、Js 为什么是单线程?
Js 是一种运行在网页的简单的脚本语言,由于设计的初衷是作为浏览器脚本语言,用于与用户互动,以及操作 DOM。这决定它是单线程的。
# 3、单线程带来的问题?
单线程就意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就需要一直等着。这就会导致 IO 操作(耗时但 cpu 闲置)时造成性能浪费的问题。
# 4、如何解决单线程的性能问题?
采用异步可以解决。主线程完全可以不管 IO 操作,暂时挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。
# JS 中自定义事件的使用与触发
# 1. 事件的创建
JS 中,最简单的创建事件方法,是使用 Event 构造器:
var myEvent = new Event('event_name');
但是为了能够传递数据,就需要使用 CustomEvent 构造器:
var myEvent = new CustomEvent("event_name", {
detail: {
// 将需要传递的数据写在 detail 中,以便在 EventListener 中获取
// 数据将会在 event.detail 中得到
},
});
2
3
4
5
6
# 2. 事件的监听
JS 的 EventListener 是根据事件的名称来进行监听的,比如我们在上文中已经创建了一个名称为‘event_name’ 的事件,那么当某个元素需要监听它的时候,就需要创建相应的监听器:
复制代码 //假设 listener 注册在 window 对象上
window.addEventListener("event_name", function(event) {
// 如果是 CustomEvent,传入的数据在 event.detail 中
console.log("得到数据为:", event.detail);
// ...后续相关操作
});
2
3
4
5
6
至此,window 对象上就有了对‘event_name’ 这个事件的监听器,当 window 上触发这个事件的时候,相关的 callback 就会执行。
# 3. 事件的触发
对于一些内置(built-in)的事件,通常都是有一些操作去做触发,比如鼠标单击对应 MouseEvent 的 click 事件,利用鼠标(ctrl+滚轮上下)去放大缩小页面对应 WheelEvent 的 resize 事件。 然而,自定义的事件由于不是 JS 内置的事件,所以我们需要在 JS 代码中去显式地触发它。方法是使用 dispatchEvent 去触发(IE8 低版本兼容,使用 fireEvent): // 首先需要提前定义好事件,并且注册相关的 EventListener
var myEvent = new CustomEvent("event_name", {
detail: { title: "This is title!" },
});
window.addEventListener("event_name", function(event) {
console.log("得到标题为:", event.detail.title);
});
// 随后在对应的元素上触发该事件
if (window.dispatchEvent) {
window.dispatchEvent(myEvent);
} else {
window.fireEvent(myEvent);
}
2
3
4
5
6
7
8
9
10
11
12
// 根据 listener 中的 callback 函数定义,应当会在 console 中输出 "得到标题为: This is title!" 需要特别注意的是,当一个事件触发的时候,如果相应的 element 及其上级元素没有对应的 EventListener,就不会有任何回调操作。 对于子元素的监听,可以对父元素添加事件托管,让事件在事件冒泡阶段被监听器捕获并执行。这时候,使用 event.target 就可以获取到具体触发事件的元素。