事件机制
事件机制的组成:事件源(事件的发送者,当前操作的元素)、事件(事件源发出的一种信息或状态)、事件侦听者(对事件作出反应的对象)
DOM事件流:网页元素接收事件的顺序。
- 事件捕获(Event Capturing):事件从window对象自上而下向目标节点传播。(如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。)
- 事件冒泡(Event Bubbling):事件从目标节点自下而上向window对象传播。
既然事件传递顺序有这两种机制,那我怎么知道事件是依据哪种机制执行的呢?
答案是:两种都会执行。DOM标准事件流的触发的先后顺序为:先捕获后冒泡
事件监听 EventTarget.addEventListener(event, listener, useCapture)
addEventListener()基本上有三个参数,分别是「事件名称」、「事件的处理程序」(事件触发时执行的function),以及一个「Boolean」值,由这个Boolean决定事件是以「捕获」还是「冒泡」机制执行,当为true
时,表示在捕获阶段调用事件处理程序,为false
时,表示在冒泡阶段调用事件处理程序,若不指定则预设为「冒泡」。
- 事件的eventPhase属性:它返回一个数字,表示当前的事件处于哪个阶段,可能的值:
0:NONE
1:事件流程处于捕获阶段
2:事件流程处于目标阶段
3:事件流程处于冒泡阶段
<body>
<div id="parent">
父元素
<div id="child">子元素</div>
</div>
<script>
var parent=document.getElementById("parent");
var child=document.getElementById("child");
document.body.addEventListener("click", function(e){
console.log("click-body", e.eventPhase);
}, false);
parent.addEventListener("click", function(e){
console.log("click-parent", e.eventPhase);
}, true);
child.addEventListener("click", function(e){
console.log("click-child", e.eventPhase);
e.stopPropagation(); // 这里加了阻止事件冒泡的方法
}, false);
</script>
</body>
点击‘父元素’输出:
click-body 1
click-parent 3
点击‘子元素’输出:
click-parent 1 // 由于parent绑定的事件在捕获阶段就执行了,所以最先输出
click-child 2 // 目标阶段
click-body 3 // 由于body绑定的事件在冒泡阶段执行,所以最后输出
事件触发顺序是由内到外的,这就是事件冒泡,虽然只点击子元素,但是它的父元素也会触发相应的事件,因为子元素在父元素里面,点击子元素也就相当于变相的点击了父元素。如果点击子元素不想触发父元素的事件怎么办?可以停止事件传播 — event.stopPropagation
。
大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。.IE10及以下(不支持addEventListener)不支持捕获型事件,所以就少了一个事件捕获阶段,IE11、Chrome
、Firefox、Safari等浏览器则同时存在。
若要移除事件的监听,需要通过removeEventListener()
来取消,用法和addEventListener
相同,但是需要注意的是,由于addEventListener()
可以同时针对摸个时间绑定多个函数,所以通过removeEventListener()
解除事件的事件的时候,第二个参数的函数必须要与先前在addEventListener()
绑的函数是同一个。
var parent = document.getElementById('parent');
var child = document.getElementById('child');
parent.addEventListener('click', function(){
console.log('aaa')
})
parent.removeEventListener('click', function(){
console.log('bbbb')
})
上面的代码,看似使用removeEventListener()
解除了click
事件,但是其实你在点击时,仍然会打印出“aaa”
,因为addEventListener
与removeEventListener
所移除的函数实际上是两个不是的function对象,为保证两个事件执行的对象是同一个,需要单独定义函数:
var clickFun = function(){
console.log('ccc'); //不会被打印
}
parent.addEventListener('click', clickFun);
parent.removeEventListener('click', clickFun);
参考链接:
1、js的事件机制
2、JavaScript事件三部曲之事件机制的原理
Event Loop(事件循环)
为什么JavaScript是单线程语言?
答:因为JavaScript作为浏览器的脚本语言,它的主要用途是与用户互动以及操作DOM,这决定了他只能是单线程,否则会带来很复杂的同步问题。例如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。多个程序,交替执行。
进程:运行的程序就被称为进程。CPU资源分配的最小单位。
线程:CPU调度的最小单位。
线程和进程的关系:进程相当于工厂里的车间,代表CPU所能处理的单个任务,线程相当于车间里的工人,一个进程可以包括多个线程。
图文解释链接:进程与线程的一个简单解释
- 一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
- 进程有单独的专属自己的资源,是系统分配的独立的内存,同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)。
- 一个进程是由一个或多个线程组成的,每个线程是一个进程中代码的不同执行路线。
- 进程之间相互独立,进程之间相互独立。
HTML5提出的Web Worker,标准,虽然允许JavaScript监本创建多个线程,但是子线程完全受主线程的控制,并且不能操作DOM,所以这个标准并没有改变JavaScript单线程的本质。
浏览器执行线程: 浏览器是多进程的,浏览器每个tab页(renderer进程)都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等。
- GUI渲染线程:主要负责页面渲染,解析HTML、CSS构成DOM树和RenderObject树、布局和绘制等;
- JS引擎线程(JS内核)(单线程):负责处理JavaScript脚本,运行代码,JS引擎值等待着任务队列中的任务的到来,然后再处理,一个tab页中无论什么时候都只有一个JS线程在运行JS程序;
- 事件触发线程:用来控制事件循环,当JS引擎执行代码块是,会将对应的任务添加到事件线程中,当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎处理;
- 定时器触发线程:setInterval与setTimeout所在线程,浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确因此通过单独的线程来计时并触发定时。W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。(也就是0ms也算4ms);
- 异步http请求线程:在XMLHttpRequest连接后是通过浏览器新开的一个线程请求,检测到状态变更是,如果设置由回调函数,异步线程就产生状态变更事件,将这个回调放入事件队列中,等待JS引擎的执行。
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程就会被挂起,GUI更新会被在一个队列中等到JS引擎空闲时立即被执行;如果JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
执行中的线程:
- 主线程:也就JS引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行;
- 工作线程(幕后线程):这个线程可能存在于浏览器或JS引擎内,与主线程是分开的,处理文件的读取、网络请求等异步事件。
- 任务队列:任务分为同步任务和异步任务,同步任务一般会直接进入主线程中执行,异步任务(例如ajax请求、setTimeout定时函数等)会通过任务队列的机制(先进先出)来进行协调。
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
一个Event Loop中,可以有一个或者多个任务队列(task queue),每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:
- 执行一个宏任务(栈中没有就从事件队列中获取) 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行) 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
-
- 宏任务(macro task):每次执行栈执行的代码就是一个宏任务。
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task
执行开始前,对页面进行重新渲染, 流程如下: (macro)task->渲染->(macro)task->…- 微任务(micro task):当前task执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
Promise.then
Object.observe
MutaionObserver
process.nextTick(Node.js 环境)
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
注意:
- JS分为同步任务和异步任务
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
例1:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
执行结果:
script start
script end
promise1
promise2
setTimeout
分析:
- 整体的script代码作为第一个宏任务进入主线程,依次同步执行,首先执行
console.log(script start )
- 遇到
setTimeout
作为第二个宏任务,放到宏任务的队列中 - 遇到第一个宏任务中的
promise.then()
微任务,放到微任务的队列中 - 再次遇到第一个宏任务同步代码
console.log(script end)
- 第一个宏任务中的所有同步代码执行完毕后,然后释放微任务队列,依次执行输出 promise1 promise2
- 微任务执行完毕后,执行第二个宏任务,输出setTimeout
例2:
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
执行结果:
script start
promise1
script end
then1
timeout1
timeout2
分析:
- 整体的script代码作为第一个宏任务依次执行,首先执行
console.log(script start)
- 遇到
setTimeout
作为第二个宏任务,放到宏任务的队列中 - 遇到
new promise
中的同步代码依然立即执行,输出 promise1, 然后执行resolve
- 遇到
setTimeout
作为第三个宏任务放到宏任务队列中 - 遇到
promise.then()
微任务,放到微任务的队列中 - 再次遇到第一个宏任务同步代码
console.log(script end)
- 接着检查微任务队列,发现有个
promise.then()
微任务,执行,输出then1 - 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行第二个宏任务
setTimeout
,输出 timeout1 - 接着执行第三个宏任务
setTimeout
,输出 timeout2 至此,所有的都队列都已清空,执行完毕
例3:
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(res=>{
setTimeout(()=>{console.log('000')},0)
res()
}).then(res=>{setTimeout(()=>{console.log('11111')},0)})
setTimeout(()=>{console.log('2222222')},0)
console.log('script end');
执行结果:
script start
script end
000
2222222
11111
timeout1
分析:
- 整体的script代码作为第一个宏任务依次执行,首先执行
console.log(script start)
,输出script start - 遇到
setTimeout
作为第二个宏任务,延时为10,放到宏任务的队列中 - 遇到
new promise
中的同步代码依然立即执行,然后执行res
,遇到setTimeout
作为第三个宏任务,延时为0,放到宏任务的队列中 - 遇到
promise.then()
微任务,放到微任务的队列中 - promise外面遇到第四个宏任务
setTimeout
,延时为0, - 再次遇到第一个宏任务同步代码
console.log(script end)
,输出script end - 此时第一个宏任务全部执行完毕,检查微任务队列,然后遇到微任务里面的
setTimeout
作为第五个宏任务,放入宏任务的队列中,此时微任务执行完毕 - 再依次执行宏任务,由于第二个宏任务的延时时间为10,所以等待,执行第三个宏任务,输出000
- 执行第四个宏任务,输出2222222
- 执行第五个宏任务,输出11111
- 执行第二个宏任务,输出 timeout1
参考链接:
1、【JS】深入理解事件循环,这一篇就够了!(必看)
2、 js中的宏任务与微任务