DOM事件
DOM 0
在前端领域一片混沌的时候,浏览器DOM事件的实现并没有一个统一的标准,各浏览器互相借鉴实现了一些事件监听。W3C出来之后,把这些之前分别实现的事件监听做了一个汇总,称为DOM level 1,在此之前这些浏览器实现的叫做DOM level 0。所以DOM 0是那时候的事实标准。
注意点:
当时浏览器实现了包括onclick
在内的一些事件,其中有一点需要特别注意的是onclick
可以写在js里边,同样也可以写在html里边,但是用法上是有区别的。
<button id="aaa" onclick="console.log()">click me</button>
<script>
aaa.onclick = function(){ console.log('aaa被点击了') }
</script>
在html代码块里,onclick后边接的是要执行的代码,一旦用户点击,浏览器就eval('要执行的代码')
,所以如果是一个函数的话,我们应该在后边写上函数的调用方法而不是函数名,如果写的是函数名的话那么这就只是一个函数声明。
在js代码块里,onclick对应的是一个函数对象。
<script>
function print(){ console.log('hello') }
aaa.onclick = print // hello
bbb.onclick = print() // 不好使
ccc.onclick = print.call() // 不好使
<script>
<button id="aaa" onclick="print">click me</button> // 不好使
<button id="bbb" onclick="print()">click me</button> // hello
<button id="ccc" onclick="print.call()">click me</button> // hello
DOM 1
DOM1有两个章节,DOM Core(核心)和DOM HTML。
DOM Core有DOM结构,DOM内存管理,DOM名称约定…..
DOM HTML有HTML Collection,HTMLDocument…对象有哪些属性,哪些定义等等。
DOM1的事件并没有单独讲,只是显示支持哪些事件。
DOM 2
这次把事件单独拿了出来做了一个整理,并且添加了很多的新功能。DOM Events,把事件写的很详细。
在DOM0里边的onclick事件显然不是那么的好用,因为onclick实际上是一个属性,它只能等于一个函数,我们不能让它执行多个函数。并且很容易不小心覆盖掉之前绑定的函数。另一个绑定click事件的方法是使用addEventListener来添加监听事件。比如像下边这样:
xxx.addEventListener('click', function(){ console.log('hello') })
xxx.addEventListener('click', function(){ console.log('world') })
这里我们为元素xxx依次添加了两个匿名函数,在触发点击事件的时候,两个函数按照声明顺序依次执行。
如果要移除这个函数的话必须要为这个函数加一个名字。
function f1(){ console.log('hello world') }
xxx.addEventListener('click', f1)
xxx.removeEventListener('click', f1) //这样就可以把f1从click的事件队列中移出去了
如果只想执行一次这个函数,也就是jQuery中的.one
,我们只需要在函数f1的最后一句把这个事件移出去就好了。
function f1(){
console.log('hello world')
xxx.removeEventListener('click', f1)
}
xxx.addEventListener('click', f1)
另一个有趣的知识点是事件的捕获与冒泡。
<div id="grandpa">爷爷
<div id="parent">父亲
<div id="son">儿子</div>
</div>
</div>
如果有这样一段代码,那么在我点击儿子的时候,是先触发爷爷的点击事件,还是先触发儿子的点击事件?
事实是,你想先触发哪个就先触发哪个。如果你在js里边这样写,那当你点击了son的时候触发顺序就依次是papa son grandpa
。
son.addEventListener('click', function(){
console.log('son')
})
papa.addEventListener('click', function(){
console.log('papa')
}, true)
grandpa.addEventListener('click', function(){
console.log('grandpa')
}, false)
所以这是为什么呢?这就涉及到addEventListener的第三个参数了。
在提到这第三个参数之前,我们需要明确的是事件的捕获和冒泡的概念。在你点击son的时候,实际上你同时也点击了papa和grandpa,就像先生打你手心的话,同时也是在打你。有人认为点击了son,应该先触发son的点击事件,然后才是papa和grandpa。也有人认为点击了son,应该先触发grandpa的点击事件,然后才是papa和grandpa。互相争执不下最终的解决办法是把事件分为捕获阶段和冒泡阶段。
其中捕获阶段是从大到小,也就是从grandpa到son,冒泡是从小到大,也就是从son到grandpa。在你执行一个点击事件的时候,先捕获再冒泡。如此一来看似完美的解决了这个问题。
所以addEventListener
有了第三个参数,如果这第三个参数为true的话,那么这个事件会在捕获阶段执行,如果为false或者不写的话,那么在冒泡阶段执行。其实想想还挺有意思的,默认是冒泡(其实我觉得冒泡阶段执行比较合理),但是冒泡的话第三个参数是false,这样有一种两种人的情绪都顾及到了的感觉呢!
所以上边的代码里,捕获阶段只有papa会执行,所以先打印出papa,到了冒泡阶段,先从son开始,然后才是grandpa。所以执行结果是papa son grandpa
。
值得一提的是如果只是在son,也就是最内层元素上,是不存在捕获和冒泡的。在所有同一个外层元素上,第三个参数是true的永远优先于第三个函数是false的执行。但是这个规则在son本身却行不通。
son.addEventListener('click', function(){
console.log('son冒泡')
}, false)
son.addEventListener('click', function(){
console.log('son捕获')
}, true)
papa.addEventListener('click', function(){
console.log('papa冒泡')
}, false)
papa.addEventListener('click', function(){
console.log('papa捕获')
}, true)
按说上述代码的执行结果应该是papa捕获 son捕获 son冒泡 papa冒泡
,但事实上不是这样的。我们前面说过,这个规则在son本身是行不通的。对于son本身而言,不论你的第三个参数是什么,都是按照事件的可执行队列来执行的。也就是说,你先写哪个,点击的时候就先执行哪个。
以及,你可以使用function(e){ e.stopPropagation()}
来阻止事件的冒泡。在jQuery里边,如果你想同时阻止事件的冒泡和默认事件的话,可以使用$(xxx).on('click', false)
第二个参数的返回值如果是false的话,就同时阻止了冒泡和默认事件。相当于
$(xxx).on('click', function(e){
e.stopPropagation()
e.preventDefault()
})
一个有一点难懂的例子(事件的可视化):
其实这个例子可以不用看,因为我觉得跟事件没什么很大的关系。但是我花了很久才捋清楚了,所以想做个笔记。
<!DOCTYPE html>
<html>
<head>
<script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
<style>
.{margin: 0; padding: 0;}
div{
margin: 15px;
display: inline-block;
border-radius: 50%;
transition: all 1s;
border: 1px solid black;
background-color: white;
}
.purple.active{
background-color: purple;
}
.cyan.active{
background-color: cyan;
}
.blue.active{
background-color: blue;
}
.green.active{
background-color: green;
}
.yellow.active{
background-color: yellow;
}
.orange.active{
background-color: orange;
}
.red.active{
background-color: red;
}
.purple{
width: 50px;
height: 50px;
}
</style>
</head>
<body>
<div class="red">
<div class="orange">
<div class="yellow">
<div class="green">
<div class="blue">
<div class="cyan">
<div class="purple"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let divs = $('div').get() //变成数组
var time = 0
for (let i = 0; i < divs.length; i++) {
divs[i].addEventListener('click', () => {
if(time>=divs.length*2){time = 0}
setTimeout( ()=>{
divs[i].classList.add('active')
}, 500*time)
time++
}, true)
divs[i].addEventListener('click', () => {
setTimeout( ()=>{
divs[i].classList.remove('active')
}, 500*time)
time++
}, false)
}
</script>
</body>
</html>
效果预览(基础测试通过,但是有bug,不想改了。所以使用的时候注意点击中心点,并且要在事件冒泡完成之后再进行下一次的点击…….)
主要有两点不好理解。
一是click事件里边的函数在我们点击之前并没有执行,此时time不会执行加一的操作。在点击中心点的时候,这些圆圈的click事件被依次触发。先捕获,从最外层到最内层,然后再冒泡,从最内层到最外层。
二是为什么用let作为for循环的循环变量的声明click事件里边调用的i就可以是我们真正想要调用的i(我们知道用var是不行的)。这个问题的话我想了很久,后来发现画内存图可以解决。因为let是局部变量,i在内存中并不是只有一个取值,所以事实是这里的每个click事件调用的都是不同的i,彼此之间并没有冲突,因此可以得到我们想要的结果。
DOM 3
DOM3并没有对事件做修正。