事件流
什么是事件流
每个事件发生时,都会有一个触发并执行的过程,也就是事件的传播过程,我们称为事件流;简单来说,事件流就是事件从发生到执行结束的流程。
事件冒泡
微软提出了名为事件冒泡(Event Bubbling
)的事件流。事件会从最内层的元素开始发生,一直向上传播,直到document
对象。
事件捕获
网景提出另一种事件流名为事件捕获(Event Capturing
)。与事件冒泡相反,事件会从最外层开始发生,直到最具体的元素。
事件处理程序
事件是用户或浏览器自身执行的某种动作。诸如click
、load
网页对用户做出的反应,叫做事件处理程序(别名:事件侦听器) 事件处理程序的名字是以on
开头
也就是当用户与页面上某个元素进行某种交互(click
)时,会执行事件处理程序内(onclick
)的函数。如果没有为这个元素设立事件处理程序,那么用户与该元素进行的交互不会有反应。
四种事件处理程序
HTML事件处理
直接添加到HTML
结构中
<input onclick="alert(value)" type="button" name="btn" value="点我一下!" />
<input ondblclick="alert(value)" type="button" name="btn" value="点我两下!" />
优点
扩展作用域,我们可以直接在onclick
的属性值中使用其他属性。alert(value)
缺点
-
存在一个时差问题,用户可能会在
HTML
元素一出现就触发相应的事件,但是该事件处理程序可能在JS
脚本内,还没有被解析,就会发生错误。 -
扩展事件的处理程序的作用域链在不同浏览器中会导致不同的结果
-
HTML
代码与JS
脚本代码紧密耦合。
DOM0
级事件处理程序
JS
指定事件处理程序,将一个函数赋值给一个元素的事件处理程序的属性。
<input type="button" name="btn" id="d1" value="点我一下!" />
//第一步:获得目标元素
var btn = document.getElementById("d1");
//第二步:为元素的事件处理程序属性赋值
btn.onclick = function(){
alert(this.value);
}
btn.onclick = null;//将对匿名函数的引用变为0,从而删除。
优点
-
简单 实现了页面与js代码分离
-
具有跨浏览器的优势
缺点
-
如果代码位于按钮后面,就有可能一段时间内怎么单击都没有反应。
-
为一个元素添加多个相同事件处理程序前者会被后者覆盖。
DOM2级事件处理程序
定义了两个方法addEventListener() removeEventListener()
,用于处理指定和用于删除事件处理程序的操作。
这两个方法都接受三个参数:要处理的事件名,作为事件处理程序的函数,和一个布尔值。布尔值为true
表示在捕获阶段调用时间处理程序,为false
表示在冒泡阶段调用。
上述代码中,为click事件制定了两个事件处理程序,结果分别按声明顺序执行。
//第一步:获得目标元素
var btn = document.getElementById("d1");
//第二步:指定事件处理程序
btn.addEventListener('click',function(){
alert(this.value);
},false);
btn.addEventListener('click',function(){
alert(this.id);
},false);
特例IE事件处理程序
在IE9
以前包括IE9
的浏览器都支持DOM2
级事件处理程序,IE8
是最后一个仍然使用其专有事件系统的主要浏览器。
而IE8
及更早版本只支持事件冒泡,所以在指定事件处理程序时,就可以不指定采用怎么样的事件流机制啦。
他也是通过两个方法实现指定与移除事件处理程序的,移除时,也是需要先将处理程序函数赋值给一个变量。
//第一步:获得目标元素
var btn = document.getElementById('d1');
//第二步:指定事件处理程序
function handler(){
alert('clicked');
};
btn.attachEvent("onclick",handler);
btn.detachEvent('onclick',handler);
但总是有区别的:
-
事件处理程序的作用域不是在所属元素,而是全局对象。
-
为一个元素指定多个事件处理程序时,事件处理程序的执行函数与dom2级相反。即与声明顺序相反。
DOM0
级事件和DOM2
级事件的区别
-
DOM0
级事件只能触发事件冒泡阶段不能触发事件捕获阶段。 -
DOM0
级事件同一元素绑定相同的事件,后面的会覆盖前面的。 -
DOM0
级事件this
指的是事件流传播到的这个元素,就是元素本身。
事件委托
事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?
就是事件从最深的节点开始,然后逐步向上传播事件,
举个例子:页面上有这么一个节点树,div>ul>li>a
;比如给最里面的a
加一个click
点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div
,有这样一个机制,那么我们给最外面的div
加点击事件,那么里面的ul,li,a
做点击事件的时候,都会冒泡到最外层的div
上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。
<ul id="box_ul">
<li>第一个</li>
<li>第二个</li>
<li>第三个</li>
</ul>
如果点击页面中的li
元素,然后输出li
当前下标,我们通常会这样写:
//传统方法拿下标
var allLi = document.getElementsByTagName('li')
for(var i = 0;i < allLi.length;i++){
var item = allLi[i]
item.onclick = (function(i) {
return function(){
console.log(i)
}
})(i)
}
//或者
Array.prototype.slice.call(allLi).forEach((v,i) =>{
v.onclick=function(){console.log(i);}
})
通过事件代理的写法:
let boxUl = document.getElementById('box_ul')
var allLi = document.getElementsByTagName('li')
//事件代理的方法 事件最终都会冒泡到父元素上边,只需绑定一次
boxUl.onclick = function(ev) {
varliArr = Array.prototype.slice.call(allLi)
vare= ev ||event,target =e.target||e.srcElement;
console.log(liArr.indexOf(target))
}
优点
- 减少事件注册,节省内存。
比如:
-
在
table
上代理所有td
的click
事件。 -
在
ul
上代理所有li
的click
事件。
- 简化了
Dom
节点更新时,相应事件的更新。
比如:
-
不用在新添加的
li
上绑定click
事件。 -
当删除某个
li
时,不用解绑上面的click
事件。
缺点
-
层级过多,冒泡过程中,可能会被某层阻止掉。
-
理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在
table
上代理td
,而不是在document
上代理td
。 -
把所有事件都用代理就可能会出现事件误判。比如,在
document
中代理了所有button
的click
事件,另外的人在引用该JS
时,可能不知道,造成单击button
触发了两个click
事件。
为什么 Javascript 是单线程的?
JavaScript
这门脚本语言诞生的使命所致!
JavaScript
为处理页面中用户的交互,以及操作DOM
树、CSS
样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。如果JavaScript
是多线程的方式来操作这些 UI DOM
,则可能出现UI
操作的冲突。
如果 JavaScript
是多线程的话,在多线程的交互下,处于UI
中的DOM
节点就可能成为一个临界资源,假设存在两个线程同时操作一个 DOM
,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,JavaScript
在最初就选择了单线程执行。
Event Loop
JS
分为同步任务和异步任务
同步任务都在主线程上执行,形成一个执行栈
主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
一旦执行栈中的所有同步任务执行完毕(此时JS
引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop
(事件循环)。
宏任务和微任务
ES6Promise
里有了一个一个新的概念:microtask
或者,进一步,JS
中分为两种任务类型:宏任务(macrotask
)和微任务( ****microtask
),在ECMAScript
中,microtask称为jobs
,macrotask
可称为task
。
macrotask
(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行),每一个task
会从头到尾将这个任务执行完毕,不会执行其它。
浏览器为了能够使得JS
内部task
与DOM
任务能够有序的执行,会在一个task
执行结束后,在下一个 task
执行开始前,对页面进行重新渲染(task->渲染->task->...
)
microtask
(又称为微任务),可以理解是在当前 task
执行结束后立即执行的任务。也就是说,在当前task
任务后,下一个task
之前,在渲染之前执行。
所以它的响应速度相比setTimeout
(setTimeout
是task
)会更快,因为无需等渲染
也就是说,在某一个macrotask
执行完后,就会将在它执行期间产生的所有microtask
都执行完毕(在渲染前)
所以,总结下运行机制:
-
执行一个宏任务(栈中没有就从事件队列中获取)
-
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
-
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
-
当前宏任务执行完毕,开始检查渲染,然后
GUI
线程接管渲染 -
渲染完毕后,
JS
线程继续接管,开始下一个宏任务(从事件队列中获取)