js 事件

110 阅读9分钟

详细梳理 javascript 事件的方方面面……

一 事件流

所谓事件流,即指页面接收事件的顺序。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Document</title>
</head>
<body>
    <div id="myDiv">click me</div>
    <script>
        var myDiv = document.getElementById('myDiv');
        myDiv.onclick = function(event) {
            console.log('myDiv');
        }
        var body = document.getElementsByTagName('body')[0]
        body.onclick = function() {
            console.log('body');
        }
    </script>
</body>
</html>

1. 事件冒泡与捕获

浏览器早期发展中,IE 和 Netscape 提出了不同的事件流,分别如下。

如上 html 代码,事件冒泡是指,点击 div 时,click 事件首先发生在 div 上,然后 click 事件沿着 DOM 树向上传播,每一级节点都会发生 click 事件:

div -> body -> html -> document  -> window

事件捕获,与“事件冒泡”相反, 点击 div 时,click 事件首先发生在 window 上, 沿着 DOM 树向下传播:

window -> document -> html -> body -> div

2. 事件流

“DOM2 级事件”规定事件流包括三个阶段:事件捕获阶段,处于目标阶段,事件冒泡阶段。处于目标阶段,可看作是冒泡的一部分。整个事件流程如下:

window -> document -> html -> body -> div -> body -> html -> document  -> window

该流程主流浏览器都支持,IE8不支持。 “DOM2 级事件”规范要求捕获阶段不触发事件,但实际上主流浏览器(除IE8)都支持在捕获阶段触发事件。一般情况下,建议只在冒泡阶段触发事件,这样可以更大程度的兼容各种浏览器。

二 事件处理程序

1. html 事件处理程序

<input type="button" value="" onclick="doSomething('param')">

方法中不能使用未经转义的HTML语法字符,比如 "param" 应写为 &quot; param &quot; 这种方式的缺点:点击时方法不一定加载完成,从而报错;作用域链(this)在不同浏览器中导致不同结果;html 和 javascript 紧耦合。 根据如今的WEB标准,结构(html)应与行为(javascript)分开,这种写法已不推荐使用。

2. DOM0 级事件处理程序

var btn = document.getElementById("myBtn");
btn.onclick = function(){ console.log(1) } // 添加事件
btn.onclick = function(){ console.log(2) } // 添加事件
btn.onclick = null; // 删除事件

这种方式事件会在冒泡阶段处理。 如果一个元素绑定多个相同的事件,则后面的会覆盖前面的。如上代码,点击后将只会执行 console.log(2)。

3. DOM2 级事件处理程序

var btn = document.getElementById('myBtn');
var handler = function() {
    console.log(this.id);
}
btn.addEventListener('click', handler, false); // 添加事件
btn.removeEventListener('click', handler, false);
handler

三个参数:1 事件名;2 事件处理方法;3 boolean 值,true 表示在捕获阶段触发,false 表示在冒泡阶段触发。 通过 addEventListener 添加的事件,只能通过 removeEventListener 删除,并且参数要相同,因此处理事件的方法不能使用匿名函数的方式,必须使用具名函数,如上handler方法。 与 DOM0 不同,该方式可以绑定多个事件,按顺序先后执行。

4. IE 事件处理程序

var btn = document.getElementById('myBtn');
var handler = function() {
    console.log(this.id);
}
btn.attachEvent('onclick', handler); // 添加事件
btn.detachEvent('onclick', handler); // 删除事件

只在冒泡阶段触发。 在全局作用域中运行,即 this === window 。 attachEvent 可绑定多个相同事件,但与 addEventListener 不同的是,以相反的顺序执行。 使用 detachEvent 删除,传入相同的参数,不能使用匿名函数,否则删除不掉。

5. 跨浏览器事件处理代码

/** 只关注冒泡阶段 */
var EventUtil = {
  addHandler: function(element, type, handler) {
    if (element.addEventListener) {
      // DOM2 添加事件
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      // IE 添加事件
      element.attachEvent('on' + type, handler);
    } else {
      // DOM0 添加事件
      element['on' + type] = handler;
    }
  },
  removeHandler: function(element, type, handler) {
    if (element.removeEventListener) {
      // DOM2 删除事件
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      // IE 删除事件
      element.detachEvent('on' + type, handler);
    } else {
      // DOM0 删除事件
      element['on' + type] = null;
    }
  }
}

三 事件对象

对象属性/方法类型说明
bubblesBoolean是否冒泡
cancelableBoolean是否可以取消事件的默认行为
currentTargetElement事件当前正在处理的元素
defaultPreventedBoolean为 true 时表明已调用了 preventDefault() (DOM3)
detailInteger与事件相关的细节信息
eventPhaseInteger1: 捕获阶段;2:处于目标阶段;3:冒泡阶段
typeString被触发的事件类型
targetElement事件的目标
srcElementElementIE,事件的目标,与 target 同功能
trustedBoolean为 true 表示事件是浏览器生成的,为 false 表示由 javascript 创建的(DOM3)
preventDefault()Function取消事件的默认行为
returnValueBooleanIE,设为 false,取消事件的默认行为,与 preventDefault() 同功能
stopPropagation()Function取消事件进一步的捕获或冒泡
cancelBubbleBooleanIE,设为 true,阻止冒泡,与 stopPropagation() 同功能
viewAbstractView与事件关联的抽象视图,等同于发生事件的 window 对象

IE 事件对象只有 type / srcElement / returnValue / cancelBubble 四个属性

注: srcElement / returnValue / cancelBubble 虽然各种资料说明被 IE 支持,实测至少 Chrome(v89) / Firefox(v88) / Safari(v14) 均支持。

跨浏览器事件对象代码处理

var EventUtil = {
    getEvent: function(e) {
        return e ? e : window.e;
    },
    getTarget: function(e) {
        return e.target || e.srcElement;
    }
    preventDefault: function(e) {
        if (e.preventDefault) {
            e.preventDefault();
        } else {
            e.returnValue = false;
        }
    },
    stopPropagation: function(e) {
        if (e.stopPropagation) {
            e.stopPropagation();
        } else {
            e.cancelBubble = true;
        }
    }
}

四 事件类型

1. UI & HTML 事件

事件说明
load当页面完全加载后(包括所有图像,javascript 文件,css 文件等外部资源),触发 window 上面的 load 事件
DOMContentLoaded在形成完整的 DOM 树之后触发,不管 javascript 文件,css 文件等外部资源是否加载完成
unload文档被完全卸载后触发。比如用户从一个页面切换到另一个页面。这个事件一般用来清除引用,避免内存泄漏。在<body>或者 window 上使用
beforeunload在浏览器卸载页面之前触发,可以用来取消卸载。IE / Firefox 设置 event.returnValue = "message", Chrome / Safari return "message" , 可弹框提示信息
resize浏览器窗口变化时在 window 上触发。一般浏览器窗口变化 1px 时就会触发,Firefox 浏览器只会在窗口不变时触发
scroll滚动带滚动条的元素,在该元素中触发。可通过元素的 scrollLeft / scrollTop 来监听滚动
contextmenu鼠标右键调出上下文菜单。
readystatechange有个readyState属性,用来提供元素加载阶段的信息。只有 IE / FireFox / Opera 支持
hashchangeURL参数列表变化时触发,必须添加到 window 对象上

2. 焦点事件

事件说明是否支持冒泡
focus元素获得焦点时触发
blur元素失去焦点时触发

3. 鼠标与滚轮事件

事件说明是否支持冒泡
click单击鼠标主按钮
dbclick双击鼠标主按钮
mousedown按下任意鼠标按钮
mouseup松开鼠标按钮
mouseenter鼠标从元素外首次移动到元素内
mouseleave鼠标移动到元素范围外
mousemove鼠标在元素内部移动
mouseout鼠标在元素内部,然后将其移入另一个元素
mouseover鼠标在元素外部,然后将其移入另一个元素边界之内
mousewheel鼠标滚轮事件(向前滚属性 wheelDelta = 120 * n),反之取负
DOMMouseScroll鼠标滚轮事件(FireFox)(向前滚属性 detail = -3 * n),反之取正
selectstart鼠标选中页面内容事件,与此相关的api:window.getSelection()

1). 鼠标位置

鼠标位置属性说明
clientX / clientY相对于浏览器位置
pageX / pageY在浏览器页面中的位置(包含有滚动条的情况)
screenX / screenY相对于显示屏的位置

2). 修改键 点击鼠标的同时按下某些键盘键,可能会有别的处理。一般修改键指:Shift / Ctrl / Alt / Meta(Windows 键),对应在事件的属性分别为 shiftKey / ctrlKey / altKey / metaKey。如果相应的键处于按下的状态,则值为 true。如 event.shiftKey === true。

4. 键盘与文本事件

事件说明
keydown按下键盘上任意键触发,按住不放重复触发
keypress按下键盘上字符键(包括 ESC)触发,按住不放重复触发
keyup释放键盘上的键
textInput文本插入文本框之前触发

1). 键盘事件 执行顺序:keydown -> keypress -> keyup。 除了按下字符键,按下能够插入或删除字符的键也会触发 keypress,并且 keypress 事件包含一个 charCode 属性,对应键代表的 ASCII 编码。

2). textInput textInput, 在可编辑区域输入字符时触发。与keypress的区别在于:

  • 只有在可编辑区才能触发 textInput ,可以获得焦点的元素都可以触发 keypress 。
  • 只会在输入实际字符时才能触发 textInput ,而能够影响字符显示的键也能触发 keypress, 比如退格键。

textInput 事件的属性:

属性说明
data用户输入的字符
inputMethod0-不确定是怎么输入的;1-键盘输入;2-粘贴输入;3-拖放输入;4-IME输入;5-通过表单选择输入;6-手写输入;7-语音输入;8-几种方法组合输入;9-脚本输入。(只有IE支持)

5. 变动事件

事件说明
DOMSubtreeModifiedDOM 结构中发生任何变化时触发
DOMNodeInserted在一个节点作为子节点被插入到另一个节点中时触发
DOMNodeRemoved在节点从其父节点中被移除时触发

6. 触摸事件

事件说明
touchstart手指触摸屏幕时触发,即使已经有个手指放在了屏幕上
touchmove手指在屏幕上滑动时连续触发。发生期间,调用 preventDefault() 可以阻止滚动
touchend手指从屏幕上移开时触发
touchcancel系统停止跟踪触摸时触发

触摸事件包含的一般属性:bubbles / cancelable / view / clientX / clientY / screenX / screenY / detail / altKey / shiftKey / ctrlKey / metaKey 。

除了这些属性外,触摸事件有三个独特的属性:

触摸事件属性说明
touches当前跟踪的触摸操作的 Touch 对象的数组
targetTouches特定于事件目标的 Touch 对象数组
changeTouches自上次触摸以来发生了什么改变的 Touch 对象的数组

这三个属性同时又包含一下属性:clientX / clientY / identifier / pageX / pageY / screenX / screenY / target

7. 手势事件

事件说明
gesturestart一个手指已经按在屏幕上,另一个手指又触摸屏幕时触发
gesturechange屏幕上的任何一个手指位置发生变化时触发
gestureend任何一个手指从屏幕上移开上触发

手势事件都有一般鼠标事件的属性:bubbles / cancelable / view / clientX / clientY / screenX / screenY / detail / altKey / shiftKey / ctrlKey / metaKey / rotation / scale 。

五 模拟事件(自定义事件)

mockevent.jpg

图中 createEvent 方法被定义为已过时的方法,被 CustomEvent 函数替代:

var event = new CustomEvent(typeArg ,customEventInit)

typeArg 是 DOMString 代表事件的名称,一个表示 event 名字的字符串。customEventInit(可选):一个字典类型参数,有如下字段:

  • detail, 可选的默认值是 null 的任意类型数据,是一个与 event 相关的值,可用来给事件传值。
  • bubbles 一个布尔值,表示该事件能否冒泡。 来自 EventInit。注意:测试chrome默认为不冒泡。
  • cancelable 一个布尔值,表示该事件是否可以取消。 来自 EventInit
// document 监听dog自定义事件
document.addEventListener('dog'function(e){
  console.log('e', e);
})
// 定义dog事件, detail 可以传入参数
var event = new CustomEvent('dog', { detail: {atrue}, bubblestrue });
// 触发事件
document.dispatchEvent(event);

IE 不支持 CustomEvent 函数,以下是兼容性代码,可供参考:

// 兼容ie9-11方案
(function(){
  try{
    new window.CustomEvent('T');
  }catch(e){
    var CustomEvent = function(event, params){
       params = params || { bubblesfalsecancelablefalsedetailundefined };

       var evt = document.createEvent('CustomEvent');
       evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
       return evt;
    };

    CustomEvent.prototype = window.Event.prototype;
    window.CustomEvent = CustomEvent;
  }
})();