JS事件简单总结

本文深入讲解JavaScript事件模型,包括事件的触发与传播机制、事件委托、不同事件实现方式及兼容性处理等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对JS事件模型不算不熟悉,只是曾经面试的时候,人家问我阻止冒泡,阻止默认事件的函数是什么,那个preventDefault,stopPropagation在我口中愣是没说出来。心都碎了。。。现在整理一下,加深自己的印象,当成完善自己的知识体系。

正文从这里开始。

什么是事件

这个其实不是很好描述,有可能是由用户发起的,比如鼠标事件,键盘事件,窗口事件,表单事件,也有可能是页面加载资源过程中的事件。常见的事件比如:click, dbclick, keydown, keypress, keyup, mousemove, wheel, scroll, focus, blur, load, unload, abort, error, resize, change, select, submit, 大概这些及其相关的。

事件传播机制

标准的事件是怎么触发以及传播的呢?一图胜千言。

总体来说,事件传播分为三个阶段:

1. 事件捕获阶段(capture phase),就是事件从window开始一层一层向目标传递的阶段
2. 目标阶段(target phase),当事件到达事件发生现场的阶段
3. 事件冒泡阶段(bubble phase),和事件捕获阶段相反,事件从目标到window传递的阶段

P.S. IE8及其以下的事件只有冒泡,没有捕获。

事件委托/代理

有个提到比较多的就是事件委托。因为事件有冒泡机制,我们可以不在目标元素监听事件,而在它的父元素监听。一个比较经典的例子,一个拥有很多子项的无序列表,需要监听子项上的点击事件。此时,我们将监听绑定在父元素ul上,当点击li元素时,由于事件的冒泡机制,ul层也能触发点击事件。但是如何判断你点击的就是你想要的li元素,而不是ul元素本身,用事件对象event的目标元素属性target来判断就好了,即event.target。

这样做的优点有:提高性能,从监听多个事件减少到监听一个事件,效率肯定得到提升;能动态地自适应,比如再在ul元素内加一个li子元素,传统的方法就需要再增加一个监听事件,而利用事件委托就可以以不变应万变。

事件实现方式

三种实现方式:

原始事件实现方式(DOM0)

也称DOM0事件处理方式,DOM0不是W3C的标准,由于历史发展的原因,它存在过也一直存在着。很简单,只要在html元素中添加on+事件的属性即可。

<button id='myButton' onclick='doSomething()'> 按钮 </button>

或者用js的方式给元素添加这个属性。

var btEle = document.getElementById('myButton');
btEle.onclick = doSomething;

这样的优点是简单方便,并且兼容所有的浏览器;缺点也不少,违反了行为与表现分离的准则,只能添加一个事件,也不能利用事件委托机制去更多的事。

IE事件实现方式

上文提到IE8及其以下的事件传播机制只有冒泡,没有捕获,实现方式为

//监听事件
element.attachEvent('on' + eventType, callback);
//解除监听
element.detachEvent('on' + eventType, callback);
//手动触发事件,兼容性IE6-10
element.fireEvent('on' + eventType)

需要注意的是,匿名函数无法被解除监听。

标准事件实现方式(DOM2)

DOM2规定的标准应该大一统了浏览器,而之后变得不多。目前W3C最新的标准是15年出的DOM4。我们来简单看下。

//监听事件
element.addEventListener(eventType, callback, useCapture);
//解除监听
element.removeEventListener(eventType, callback, useCapture);
//手动触发事件
element.dispatchEvent(eventType)
//回调函数中有个event对象记录事件的属性 
function callback(event){
    //do something
}

因为标准的事件传播机制有捕获和冒泡阶段,这里最后一个参数useCapture表示是否在事件捕获阶段就执行回调函数,默认为false。一般情况下,我们使用默认值,即在事件冒泡阶段执行函数。为什么?主要是为了兼容旧版的IE。

事件常见属性

我们已经知道,事件的回调函数中有个事件对象的参数。从第一部分,我们已经看到Event对象的继承关系,比如我们最常见的鼠标事件或者键盘事件,MouseEvent继承自UIEvent,UIEvent继承自Event。我们参考最新的W3C规范,来具体看下。

[Constructor(DOMString type, optional EventInit eventInitDict),
Exposed=(Window,Worker)]
interface Event {
  // 事件的类型,比如click,submit,load等
  readonly attribute DOMString type;
  // 触发事件的那个目标元素
  readonly attribute EventTarget? target;
  // 事件传播所在的当前元素
  readonly attribute EventTarget? currentTarget;

  const unsigned short NONE = 0;
  const unsigned short CAPTURING_PHASE = 1;
  const unsigned short AT_TARGET = 2;
  const unsigned short BUBBLING_PHASE = 3;
  // 事件所在的阶段,枚举值如上
  readonly attribute unsigned short eventPhase;
  // 阻止事件的冒泡,在当前元素的冒泡执行完之后
  void stopPropagation();
  // 立即阻止冒泡,包括当前元素还有其他的回调事件
  // DOM3增加新属性 
  void stopImmediatePropagation();
  // 是否在冒泡阶段
  readonly attribute boolean bubbles;
  // 是否可以被取消,用preventDefault取消
  readonly attribute boolean cancelable;
  // 取消当前元素的默认事件,前提是cancelable为true
  void preventDefault();
  // 是否被preventDefault过,DOM3增加新属性 
  readonly attribute boolean defaultPrevented;
  // 事件是否有用户触发的
  [Unforgeable] readonly attribute boolean isTrusted;
  // 当前时间戳
  readonly attribute DOMTimeStamp timeStamp;

  void initEvent(DOMString type, boolean bubbles, boolean cancelable);
};

dictionary EventInit {
  boolean bubbles = false;
  boolean cancelable = false;
};
[Constructor(DOMString type, optional MouseEventInit eventInitDict)]
interface MouseEvent : UIEvent {
  // 相对屏幕的x,y坐标
  readonly attribute long screenX; 
  readonly attribute long screenY;
  // 相对浏览器可视区域的x,y坐标
  readonly attribute long clientX;
  readonly attribute long clientY;
  //是否按下这些特殊的键
  readonly attribute boolean ctrlKey;
  readonly attribute boolean shiftKey;
  readonly attribute boolean altKey;
  readonly attribute boolean metaKey;
  //哪个鼠标键被按下,0主键,1滚轮,2附键,只在mouseup,mousedown事件中有效
  readonly attribute short button;
  // 被激活的鼠标,可以是多个,0没有,1主键,2附键,4滚轮,8后退?,16前进?
  // 还可以相加,比如按下了主键和滚轮,就是1+4 = 5
  readonly attribute unsigned short buttons; 
  readonly attribute EventTarget? relatedTarget; 
  boolean getModifierState(DOMString keyArg);
};

以上是规范,但实际上浏览器的实现上好像还有很多杂七杂八的属性。以下是在Chrome 56输出的结果。

    1. MouseEvent
      1. altKey:false
      2. bubbles:true
      3. button:0
      4. buttons:0
      5. cancelBubble:false
      6. cancelable:true
      7. clientX:97
      8. clientY:23
      9. composed:true
      10. ctrlKey:false
      11. currentTarget:null
      12. defaultPrevented:false
      13. detail:1
      14. eventPhase:0
      15. fromElement:null
      16. isTrusted:true
      17. layerX:97
      18. layerY:23
      19. metaKey:false
      20. movementX:0
      21. movementY:0
      22. offsetX:28
      23. offsetY:13
      24. pageX:97
      25. pageY:23
      26. path:Array[5]
      27. relatedTarget:null
      28. returnValue:true
      29. screenX:144
      30. screenY:181
      31. shiftKey:false
      32. sourceCapabilities:InputDeviceCapabilities
      33. srcElement:button
      34. target:button
      35. timeStamp:2414.235
      36. toElement:button
      37. type:”click”
      38. view:Window
      39. which:1
      40. x:97
      41. y:23
      42. proto:MouseEvent

感觉眼睛都要花了,比较常用的感觉不多,也看场景吧。一般比如target,preventDefalut,stopPropagation,type,以及坐标的属性比较常用吧。

由于浏览器的历史原因,关于鼠标坐标的属性真是好多对:

属性描述
x, y在整个浏览器窗口所处的位置
screenX, screenY在整个屏幕中所处于位置
clientX, clientY在整个浏览器窗口所处的位置,与x, y属性相同
layerX, layerY在整个元素的区域鼠标的位置(但算上了scroll的距离),当不随页面滚动而变化
pageX, pageY在整个页面所处的位置,当不随页面滚动而变化
offsetX, offsetY在整个元素的区域,鼠标的位置,当不随页面滚动而变化
movementX, movementY两个事件之间鼠标的位移差

最好使用W3C规范里面的两对属性,是最安全和最保险的选择,screenX, screenY, clientX, clientY。

简单的兼容实现

主要是考虑对IE8及其以下的兼容性。

function addEventListener(target, event, callback)
    if( window.addEventListener ){
        target.addEventListener(event, callback)l
    }
    else if(window.attachEvent){
        target.attachEvent('on' + event, callback);
    }
    else{
        target['on' + event] = callback;
    }
}

事件的执行顺序

我们用一个简单的例子在Chrome 56中做测试。

// HTML
<div id="parent" onclick="parentCB(window.event)">
    <div id="child" onclick="childCB(window.event)"></div>
</div>

//SCRIPT
var phases = {
  1: 'capture',
  2: 'target',
  3: 'bubble'
};

function parentCB(e){
    console.log('parent dom0 html ' + phases[e.eventPhase] );
}

function childCB(e){
    console.log('child dom0 html ' + phases[e.eventPhase] );
}

var parent = document.getElementById('parent');
parent.addEventListener('click', function(e){
    console.log('parent capture ' + phases[e.eventPhase]);
}, true)
parent.onclick = function(e){
    console.log('parent dom0 js ' + phases[e.eventPhase]);
}
parent.addEventListener('click', function(e){
    console.log('parent bubble ' + phases[e.eventPhase]);
})

var child = document.getElementById('child');
child.addEventListener('click', function(e){
    console.log('child bubble ' + phases[e.eventPhase]);
})
child.addEventListener('click', function(e){
    console.log('child capture ' + phases[e.eventPhase]);
}, true)

// child.onclick = function(e){
//     console.log('child dom0 js ' + phases[e.eventPhase]);
// }

当点击parent div的时候输出如下:

parent capture target
parent dom0 js target
parent bubble target

而点击child div的时候输出如下:

parent capture capture
child dom0 html target
child bubble target
child capture target
parent dom0 js bubble
parent bubble bubble

当你不断地调整那些事件的顺序,或者引入js的onclick函数,你会发现以下规律:

1. 当html中有onclick事件,js中也有onclick事件的时候,js中onclick事件会覆盖html中的事件。因为其实它们是同一个属性。
2. 目标元素的事件执行顺序跟定义的顺序一致,而不会按照先捕获后冒泡的顺序。在html中定义的事件排在第一个。
3. 非目标事件的函数执行遵守先捕获后冒泡的规律,并且DOM0级元素定义的事件会在冒泡阶段先执行,不管是在html定义还是用js定义。

另外,如果一个事件多次绑定,只会执行一次。

事件this指向

对于DOM0级的this情况如下。

//HTML 
<a href="#"  onclick ="go(this)"> hhhhhh </a>
<button> 按钮 </button>

//SCRIPT
function go(e){
     console.log(e);  // <a href="#"  onclick ="go(this)"> hhhhhh </a>
     console.log(this); //window
}

var bt = document.getElementsByTagName('button')[0];
bt.onclick = function(e){
    console.log(this); // <button> 按钮 </button>
}

而attachEvent中的this指向window,addEventListener指向当前的元素。

自定义事件

数据双向绑定的分析和简单实现中,已经简单提到了,事件的机制抽象出来就是发布订阅模式。我们可以利用自定义事件去实现我们自己想要的发布订阅模式。

// 新建事件实例
var event = new Event('build');

// 添加监听函数
child.addEventListener('build', function (e) {
    console.log(' custom event build');
}, false);

// 触发事件
child.dispatchEvent(event);

参考资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值