JS 事件触发机制

做~自己de王妃 提交于 2020-01-03 08:58:44

目录

一、JS 事件机制

二、事件阶段划分

三、注册事件

四、取消事件传递

五、为什么要阻止事件传递?

六、e.preventDefault() 和 e.stopPropagation() 的区别

七、事件解绑

八、事件代理 (事件委托)


一、JS 事件机制

JS 的事件机制其实就是事件在 DOM 上的传递顺序,并且我们可以对应阶段对这些事件进行获取和处理。

可以看到 li 标签被 ul 标签包装了, 而 ul 标签又被 div 标签包装了。当我们点击 li 时,其实也点击了 ul 和 div。

<div>
    <ul>
        <li>l1</li>
        <li>l2</li>
        <li>l3</li>
        <li>l4</li>
        <li>l5</li>
    </ul>
</div>

 

二、事件阶段划分


Javascript事件分为三个阶段:

  • 捕获阶段
  • 目标阶段
  • 冒泡阶段

我们可以通过事件目标对象的 eventPhase 属性来得知当前事件在什么阶段。

eventPhase事件值:

点击代码黄色的right模块之后,可以发现:

捕获阶段是从外部一层层往内部传递至目标元素,eventPhase 的值为1,当发现是目标元素时,eventPhase 的值变为2。

冒泡阶段是从目标元素开始,一层层向外部传递至根节点,当当前是目标元素时,eventPhase 的值为2,若不是,则为3。

关键代码:

<script type="text/javascript">
      var p = document.getElementById('p');
	  var child = document.getElementById("child")
	  var rgt = document.getElementById("right-container")
      p.addEventListener('click', function (e) {
        console.log('父节点捕获 e = ' + e.eventPhase );
      }, true);
      p.addEventListener('click', function (e) {
	    console.log('父节点冒泡 e = ' + e.eventPhase );
      }, false);
	  child.addEventListener('click', function (e) {
		console.log('子节点捕获 e = ' + e.eventPhase );
	  }, true);
	  child.addEventListener('click', function (e) {
		console.log('子节点冒泡 e = ' + e.eventPhase );
	  }, false);
	  rgt.addEventListener('click', function (e) {
	  		console.log('子rgt节点捕获 e = ' + e.eventPhase );
	  }, true);
	  rgt.addEventListener('click', function (e) {
	  		console.log('子rgt节点冒泡 e = ' + e.eventPhase );
	  }, false);
</script>

 

三、注册事件


使用 addEventListener 监听事件,默认是监听冒泡阶段的事件,但可以通过 useCapture 参数,来指定在什么阶段触发事件。

useCapturetrue,则是在捕获阶段就触发事件。若 useCapturefalse,则是在冒泡阶段触发事件。

JQuery中,若使用 on 来监听事件,则是在捕获阶段触发事件。

p.addEventListener('click', function (e) {
   console.log('捕获阶段监听 e = ' + e.eventPhase );
}, true);
p.addEventListener('click', function (e) {
   console.log('冒泡阶段监听 e = ' + e.eventPhase );
}, false);
p.addEventListener('click', function (e) {
   console.log('冒泡阶段监听 e = ' + e.eventPhase );
});

 

四、取消事件传递


我们可以通过 e.stopPropagation 阻止事件继续向上(冒泡)或向下(捕获)传递。

捕获阶段阻止事件:

若捕获阶段中,有元素监听捕获事件并阻止事件继续下传,则事件不会再下传到更内部的元素节点中。同时,这个被阻止的事件也不会再有冒泡阶段。

<div id="p">
    parent
	<div id="child">
	    child
		<div id="right-container">right</div>
	</div>
</div>
<script type="text/javascript">
    var p = document.getElementById('p');
    var child = document.getElementById("child")
    var rgt = document.getElementById("right-container")
    p.addEventListener('click', function (e) {
        console.log('捕获阶段监听 e = ' + e.eventPhase );
        e.stopPropagation(); // 阻止事件继续向下
    }, true);
    child.addEventListener('click', function (e) {
        console.log('子节点捕获 e = ' + e.eventPhase );
    }, true);
    rgt.addEventListener('click', function (e) {
        console.log('子rgt节点捕获 e = ' + e.eventPhase );
    }, true);
</script>

冒泡阶段阻止事件:

在冒泡阶段中,有元素监听冒泡事件并阻止事件继续上浮,则事件不会再上浮到更外层的元素节点中。

<div id="p">
    parent
	<div id="child">
	    child
		<div id="right-container">right</div>
	</div>
</div>
<script type="text/javascript">
    var p = document.getElementById('p');
    var child = document.getElementById("child")
    var rgt = document.getElementById("right-container")
    p.addEventListener('click', function (e) {
        console.log('捕获阶段监听 e = ' + e.eventPhase );
        e.stopPropagation(); // 阻止事件继续向下
    }, true);
    child.addEventListener('click', function (e) {
        console.log('子节点捕获 e = ' + e.eventPhase );
    }, true);
    rgt.addEventListener('click', function (e) {
        console.log('子rgt节点捕获 e = ' + e.eventPhase );
    }, true);

    // 冒泡
    p.addEventListener('click', function (e) {
        console.log('冒泡阶段监听 e = ' + e.eventPhase );
        e.stopPropagation(); // 阻止事件继续上浮
    }, false);
    child.addEventListener('click', function (e) {
        console.log('子节点冒泡 e = ' + e.eventPhase );
    }, false);
    rgt.addEventListener('click', function (e) {
        console.log('子rgt节点冒泡 e = ' + e.eventPhase );
    }, false);
</script>

 

五、为什么要阻止事件传递?


阻止事件传递可以防止点透现象的发生。

点透现象:

当父子元素同时监听了同一种事件时(如click事件),由于事件机制,点击了子元素,父元素监听的事件也会触发。

<div id="div1">
    <div id="div2"></div>
</div>
<script type="text/javascript">
    var el1 = document.getElementById('div1')
    var el2 = document.getElementById('div2')
    el1.addEventListener('click', function(e) {
        console.log('div1点击')
    })
    el2.addEventListener('click', function(e) {
        console.log('div2点击')
    })
</script>

处理点透现象:

只需要在div2的点击事件发生后,阻止事件继续传递

<div id="div1">
    <div id="div2"></div>
</div>
<script type="text/javascript">
    var el1 = document.getElementById('div1')
    var el2 = document.getElementById('div2')
    el1.addEventListener('click', function(e) {
        console.log('div1点击')
    })
    el2.addEventListener('click', function(e) {
        console.log('div2点击')
        e.stopPropagation() // 阻止事件继续上浮
    })
</script>

 

六、e.preventDefault() 和 e.stopPropagation() 的区别


e.preventDefault()  取消预设行为,阻止默认事件的发生,但不会阻止事件传递

e.stopPropagation()  阻止事件传递,阻止事件传递,但不会阻止默认事件的发生

看起来很绕,做一个小实验就能充分理解了:

阻止默认事件:

当我们使用 e.preventDefault() 阻止默认事件发生时:

阻止事件传递:

当我们使用 e.stopPropagation() 时,虽然冒泡事件被阻止了,但由于a标签的点击默认行为是跳转页面,页面还是被跳转过去了

实验代码:

<body>
    <div id="p">
        parent
		<div id="child">
			child
			<a href="http://www.baidu.com" id="right-container">测试</a>
		</div>
    </div>
    <script type="text/javascript">
      var p = document.getElementById('p');
	  var child = document.getElementById("child")
	  var rgt = document.getElementById("right-container")

	  p.addEventListener('click', function (e) {
	    console.log('冒泡阶段监听 e = ' + e.eventPhase );
	  });
	  child.addEventListener('click', function (e) {
        console.log('子节点冒泡 e = ' + e.eventPhase );
	  }, false);
	  rgt.addEventListener('click', function (e) {
        e.preventDefault(); // 阻止默认事件
        // e.stopPropagation(); // 阻止事件传递
        console.log('子rgt节点冒泡 e = ' + e.eventPhase );
	  }, false);
    </script>
</body>

 

七、事件解绑


监听了事件的元素,需要在适当时解除监听,这就叫事件解绑

当元素被绑定事件后,若没有在适当的时候解绑事件,可能会导致元素被重复绑定一样的事件,导致一次点击,触发两次。

如何解绑事件:

js: 绑定: addEventListener  => 解绑: removeEventListener

jq: 绑定:   on     =>    解绑: off

重复绑定的情况:

点击一次div1,div1的点击事件却会运行两次

<body>
    <div id="div1">div1</div>
    <script type="text/javascript">
        var a = 1;
        function Init() {
		  var el1 = document.getElementById('div1')
          // 由于一个元素可以绑定多个不同的click事件,故解绑也要指定要解绑的事件
		  el1.addEventListener('click', addCount) 
	  }
	  
	  function addCount() {
		  a++
		  console.log('a = ' + a)
	  }
	  
        Init();
        Init(); // 多绑定了一次
    </script>
</body>

正常解绑的情况:

<body>
    <div id="div1">div1</div>
    <script type="text/javascript">
        var a = 1;
        function Init() {
            var el1 = document.getElementById('div1')
            el1.removeEventListener('click',addCount)
            // 由于一个元素可以绑定多个不同的click事件,故解绑也要指定要解绑的事件
            el1.addEventListener('click', addCount) 
	  }
	  
	  function addCount() {
		  a++
		  console.log('a = ' + a)
	  }
	  
        Init();
        Init(); // 多绑定了一次
    </script>
</body>

 

八、事件代理 (事件委托)


事件代理即将本来要绑定在子元素的事件,将其绑定到这个子元素的父元素身上,通过父元素的事件去处理触发的子元素。

假设有这样的情况:

<ul id="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    ...
    ...
    ... // 动态添加的
</div>

若要给li元素绑定点击事件,则要遍历整个li,给每个li元素绑定事件。

这样有两个弊端:

  • 要for循环给子元素绑定事件,浪费效率
  • 若li元素是动态的,则需要写更多代码处理
  • 当事件要解绑时,需要重新遍历li元素,逐个解绑

使用事件代理

看起来就很麻烦吧。由于事件冒泡的从下至上的原则,子元素li的冒泡事件,可以被父元素监听到,我们可以将点击事件绑定到父元素身上进行统一处理:

  • 父元素要监听子元素的触发事件
  • 父元素要识别出这是哪一个子元素触发的事件
  • 只需要给一个父元素绑定,无需遍历子元素逐个绑定
  • 只需要解绑父元素一个,无需遍历子元素逐个解绑
  • 子元素个数动态变化时,无需再专门处理

父元素如何识别是哪个子元素触发?

通过 e.target e.srcElement (老版本IE) 识别到是哪一个子元素触发的

通过 nodeName 来区别是父元素触发的事件,还是子元素触发的事件,避免影响到父元素

实验代码:

<body>
	<ul id="list">
		<li>1</li>
		<li>2</li>
		<li>3</li>
		<li>4</li>
		<li>5</li>
		<li>6</li>
	</div>
	
    <script type="text/javascript">
      var list = document.getElementById('list');
	  list.addEventListener('mouseover', function(e) {
		  var ev = e || window.event;
		  var childLi = ev.srcElement || ev.target;
		  console.log('mouseover ' + childLi.nodeName);
		  if(childLi.nodeName.toLowerCase() == 'li'){
			childLi.style.background = 'green';
		  }
	  })
	  list.addEventListener('mouseout', function(e) {
		  var ev = e || window.event;
		  var childLi = ev.srcElement || ev.target;
		  console.log('mouseout ' + childLi.nodeName);
		  if(childLi.nodeName.toLowerCase() == 'li'){
			childLi.style.background = 'white';
		  }
	  })
    </script>
</body>

 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!