DOM事件原理

DOM 的版本

DOM1链接
DOM Level 1 主要有两个版本,但主要学习的是 DOM Level 2 的事件标准, 因为 DOM Level 1 太简单了

所以事件标准用的最广泛的是 DOM Level 2

例子

<button id=X onclick="print">A</button>  // 错误使用
<button id=Y onclick="print()">B</button> // 正确使用
<button id=Z onclick="print.call()">C</button> // 正确使用

<script>
function print() {
console.log('hi');
}
X.onclick = print // 正确使用 // 类型为函数对象
Y.onclick = print() // 错误使用 // undefined
Z.onclick = print.call() // 错误使用 //undefined
</script>

html 中 onclick="要执行的代码"
一旦用户点击,浏览器就 eval("要执行的代码")

而 js 中,一旦用户点击,那么浏览器就点击
X.onclick.call(X, {})

还有一个例子

<button id=xxx>xxx</button>
// 属性,唯一
xxx.onclick = function() {
console.log(2);
}
xxx.onclick = function() {
console.log(3);
}
// 队列,不唯一
// xxx 拥有一个队列 eventListeners
function f1() {
console.log(1);
}
function f2() {
console.log(2);
}
xxx.addEventListener('click', f1)
xxx.addEventListener('click', f2)

再再再一个例子

<div id='grand1'>
<div id='parent1'>
<div id='child1'>
</div>
</div>
</div>

所以

  • 当点击 ‘child1’ 时,有没有点击到 ‘parent1’ 和 ‘grand1’ (是都点击到)
  • 代码 1
// 当点击 'child1' 时,三个函数是否调用 (是的都调用到了)
grand1.addEventListener('click', function fn1(){
console.log('爷爷');
});
parent1.addEventListener('click', function fn2(){
console.log('爸爸');
});
child1.addEventListener('click', function fn3(){
console.log('儿子');
});
  • 代码 2
// 那么 fn1 fn2 fn3 的执行顺序是
// 1 2 3 还是 3 2 1 ???

// 然而 w3c 觉得都可以
// flasy: undefined null 0 '' NaN

grand1.addEventListener('click', function fn1(){
console.log('爷爷');
}, false);
parent1.addEventListener('click', function fn2(){
console.log('爸爸');
}, false);
child1.addEventListener('click', function fn3(){
console.log('儿子');
}, false);

// 不传第三个参数 or 传 flase : 儿子爸爸爷爷
// 传第三个参数为 true : 爷爷爸爸儿子
  • 代码 3
// 假如再多一个事件呢
grand1.addEventListener('click', function fn1(){
console.log('爷爷');
});
parent1.addEventListener('click', function fn2(){
console.log('爸爸');
});
child1.addEventListener('click', function fn3(){
console.log('儿子1');
});
child1.addEventListener('click', function fn4(){
console.log('儿子2');
});

// 那就是队列呗,先进先出
  • 代码 4
// 如果是最后一个节点,既有"冒泡" 也有 "捕获"的情况下,那么是先执行哪个呢
child1.addEventListener('click', function fn3(){
console.log('儿子捕获');
}, true);
child1.addEventListener('click', function fn3(){
console.log('儿子冒泡');
}, false);

// 当然是 " 捕获" 先执行,然而这是跟其执行顺序有关,比如像这样
child1.addEventListener('click', function fn3(){
console.log('儿子冒泡');
}, false);
child1.addEventListener('click', function fn3(){
console.log('儿子捕获');
}, true);

// 以上就是 "冒泡" 先执行

所以事件模型如下图所示

写个例子

bootstrap 上的 popover 例子 bootstrap popover

自己写来验证

html 代码

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div class="wrapper">
<button id="clickMe">点我</button>
<div id="popover" class="popover">
浮层
</div>
</div> </body>
</html>

css

.wrapper {
position: relative;
display: inline-block;
}
.popover {
border: 1px solid red;
position: absolute;
left: 100%; top: 0;
white-space: nowrap;
padding: 10px;
margin-left: 10px;
background: white;
}
.popover::before {
content: '';
position: absolute;
right: 100%; top:0;
border: 10px solid transparent;
border-right-color: red;
}
.popover::after {
content: '';
position: absolute;
right: 100%; top:0;
border: 10px solid transparent;
border-right-color: white;
margin-right: -1px;
}

效果图如下所示

想要做到点击 ‘点我’ 按钮屏蔽 ‘浮层’ 元素,以下有一个简单的方法,即通过 监听 body 的方式
javascript

clickMe.addEventListener('click', function() {
popover.style.display = 'block';
});

// 监听 body
document.body.addEventListener('click', function() {
console.log('click body');
// 执行代码逻辑...
});

但是这种方式存在缺陷,因为 body 的宽和高被限定了,在 body 中加入 css body{ border: 1px solid green},如下图所示

就造成了点击不是 body 的别处就不会响应相应逻辑
所以可以通过 改变 body 宽高,监听文档 document 或者 监听 html document.documentElement 的方式来进行,如下的 js 代码

clickMe.addEventListener('click', function() {
console.log('display block');
popover.style.display = 'block';
});

// 监听 document
document.addEventListener('click', function() {
console.log('display none');
// 执行代码逻辑...
popover.style.display = 'none';
});

然而以上代码还是有 bug,用户点击按钮时页面没有反应,这是由于 DOM 事件响应的顺序造成的,出现 bug 的原因如下图所示

原因其实很简单, documentbutton 的两个函数都是在冒泡阶段执行的

解决方法

阻止冒泡,思路: 切断 button 向上冒泡那一层,使用 stopPropagation 函数, js 代码如下

clickMe.addEventListener('click', function(e) {
console.log('display block');
popover.style.display = 'block';
e.stopPropagation();
});

// 监听 document
document.addEventListener('click', function() {
console.log('display none');
// 执行代码逻辑...
popover.style.display = 'none';
});

但是点击 ‘浮层’,浮层就会消失,原因是在 wrapper 那里没有阻止冒泡, 加上即可

clickMe.addEventListener('click', function(e) {
console.log('display block');
popover.style.display = 'block';
});

wrapper.addEventListener('click', function(e) {
e.stopPropagation();
});

// 监听 document
document.addEventListener('click', function() {
console.log('display none');
// 执行代码逻辑...
popover.style.display = 'none';
});

更好的解决方法

document 只用监听一次,如下代码
使用 jQuery 来做

$(clickMe).on('click', function() {
$(popover).show();
$(document).one('click', function(){
$(popover).hide();
});
})

$(wrapper).on('click', function(e){
e.stopPropagation();
})

好处是不用每次点击 document 就增加一个函数,比较节省内存, one 只执行一次,执行完就销毁

或者使用异步代码

$(clickMe).on('click', function(){
$(popover).show();
setTimeout(function() {
$(document).one('click', function(){
$(popover).hide();
});
}, 0);

})

这里异步代码执行的逻辑是,先 show ,然后执行 setTimeout 并且将 click 事件绑定在函数 function 上,然后等待 button 的 click 事件冒泡结束后,下次若有点击出现在 document 上时,便执行已经绑定好的 function 里的 hide 函数

最后的 bug

假如最后的代码是这样

$(clickMe).on('click', function() {
$(popover).show();
$(document).one('click', function(){
$(popover).hide();
});
})

即去掉了 wrapper 的点击事件的添加,那么呈现出来的效果就还是点击按钮没反应,原因如下图所示

由于代码并非异步执行,所以在冒泡阶段,代码首先执行了 show,而后 document 立即执行了添加事件监听,将 document 已经绑定好的函数执行了,也就是执行到了最后的 hide

DOM 一些注意事项

  • 尽量不用全局变量(window),要用局部变量,不然变量名跟全局变量一样的话就会互相覆盖,局部变量就用函数包起来,让这个变量有个局部作用域
  • 立即执行函数,声明一个函数,立即执行这个函数 (避免使用全局变量)

    • 代码如下

      function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }.call()
      // 或者
      function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }()
  • 以上的代码,在刷新页面时会报错,解决方法如下

    • 加括号

      (function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }.call())
      // 或者
      (function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }).call()
    • 加正/负号, ~,!号

      +function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }.call()
      -function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }.call()
      ~function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }.call()
      !function() {
      var parent = document.querySelecor('#self');
      console.log(parent);
      }.call()
    • 用代码块

      {
      // 这里不能用 var ,用 var 就相当于声明了全局变量,因为 var 声明的变量会提升
      let parent = document.querySelecor('#self');
      console.log(parent);
      // let 的作用域始终在包含它的花括号里
      }

DOM 事件小总结

DOM 事件响应的模型

如上图所示,这里有三个 div,其嵌套关系为 ‘爷爷 > 爸爸 > 儿子’,对于添加事件监听来说,如果给 addEventListener 后传参为 true,则将其事件添加进 捕获阶段;如果给 addEventListener后传参为 false 或者不传参,则将其事件添加进 冒泡阶段

若三个 div 节点监听的事件均在捕获阶段,那么对应函数执行的顺序就是 fn1 > fn2 > fn3;

若三个 div 节点监听的事件均在冒泡阶段,那么对应函数执行的顺序就是 fn33 > fn22 > fn11;

对于 ‘儿子’ 来说,若同时存在 fn3 和 fn33,那么优先响应先添加进的事件