javascript 中的事件机制

js之事件机制

1、事件初探

1.1 js事件的概述

JavaScript事件:JavaScript是基于事件驱动模型的,所有的内容几乎都可以和事件挂钩。它首先定义了一些事件,然后在具体的某个环境触发了该事件,再完成相应的操作。

事件可以这么理解:它拥有事件三要素(事件源、事件、监听器)。

  • 事件源:在哪个元素上发生的,p、a、div、form表单
  • 事件:到底发生了什么事件,click、mouseover、load、submit、focus
  • 监听器:如何应对事件的发生,如何回应发生的事件,通常以函数的形式来出现。

重申事件的几个坑:

  • 事件实际上应该称之为事件模型,事件本身只是一个单纯的行为。
  • 事件不是以 on 开头的那个名称,如 onclick 不是事件,click才是事件。onclick引用的是一个元素对象的属性,它指向click事件类型绑定的实际处理函数。

1.2 js事件的几个概念

  • 事件:指的是文档或者浏览器窗口中发生的一些特定交互瞬间。我们可以通过侦听器(或者处理程序)来预定事件,以便事件发生的时候执行相应的代码。

  • 事件处理程序:我们用户在页面中进行的点击这个动作、鼠标移动的动作,网页页面加载完成的动作等,都可以称之为事件名称,即:click、mousemove、load等都是事件的名称。响应某个事件的函数则称为事件处理程序,或者叫做事件侦听器。事件处理程序的名字是以“on”开头,因此click事件的处理程序就是onclick。

  • 事件类型:

    • UI事件:如load、unload、error、resize、scroll、select、DOMActive,是用户与页面上的元素交互时触发的。
    • 焦点事件:如blur、DOMFocusIn、DOMFocusOut、focus、focusin、focusout,在元素获得或失去焦点的时候触发,这些事件当中,最为重要的是blur和focus,有一点需要引起注意,这一类事件不会发生冒泡!
    • 鼠标与滚轮事件:如click、dblclick、mousedown、mouseenter、mouseleave、mousemove、mouseout、mouseover、mouseup,是当用户通过鼠标在页面执行操作时所触发的。
    • 滚轮事件:mousewheel(IE6+均支持)、DOMMouseScroll(FF支持的,与mousewheel效果一样)。是使用鼠标滚轮时触发的。
    • 文本事件:textInput,在文档中输入文本触发。
    • 键盘事件:keydown、keyup、keypress,当用户通过键盘在页面中执行操作时触发。
    • 合成事件:DOM3级新增,用于处理IME的输入序列。所谓IME,指的是输入法编辑器,可以让用户输入在物理键盘上找不到的字符。compositionstart、compositionupdate、compositionend三种事件。
    • 变动事件:DOMsubtreeModified、DOMNodeInserted、DOMNodeRemoved、DOMAttrModified、DOMCharacterDataModified等,当底层DOM结构发生变化时触发。IE8-不支持。
    • 变动名称事件:指的是当元素或者属性名变动时触发,当前已经弃用!

对于事件的基本类型,随着HTML5的出现和发展,又新增了HTML5事件、设备事件、触摸事件、手势事件

  • 事件流:描述的是从页面中接收事件的顺序。
    • 事件冒泡:事件开始的时候由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

    • 事件捕获:事件开始的时候由最不具体的节点接收,然后逐级向下传播到最具体的节点。

IE与原来的NetScape(网景),对于事件流提出的是完全不同的顺序。IE团队提出的是事件冒泡流;NetScape的事件流是事件捕获流

  • 事件对象:在触发DOM上的某个事件的时候,会产生一个事件对象event,而在这个对象当中会包含着所有与事件有关的信息。

  • 事件委托:给元素的父级或者祖级,甚至页面绑定事件,然后利用事件冒泡的基本原理,通过事件目标对象进行检测,然后执行相关操作。

  • 移除事件处理程序:每当将一个事件处理程序指定给一个元素时,在运行中的浏览器代码与支持页面交互的JavaScript代码之间就会建立一个连接。连接数量也直接影响着页面的执行速度。所以,当内存中存在着过时的“空事件处理程序”的时候,就会造成Web应用程序的内存和性能问题。

上述的这些概念会在下面几个环节中逐个提出并解释,下面大概会有事件源(DOM)、事件绑定(处理程序)、事件对象、事件类型等几个部分介绍。

2、事件源

之所以这环节说DOM,一方面是因为它是事件三要素之一,另一方面可以看出javascript在操作HTML文档时,中间折现出的一些原理。下面对DOM只说大概历程,不谈具体方法。

2.1 DOM

Document Object Model:文档对象模型,是一组描述脚本与结构化文档进行交互的web的标准,它定义了一系列对象、方法和属性,用于访问、操作和创建文档中的内容、结构和行为。

  • D:documnet,文档,html文档或者xml文档
  • O:object,对象,在转成树模型的时候,得到的对象,它有相应的属性和方法,利用他们可以完成任何操作。
  • M:model,模型,树模型,有节点构成的一颗树。节点(元素、属性和文本)转成对象。

在W3C的标准中,DOM是独于平台和语言的接口,它允许程序和脚本动态地访问和更新文档的内容、结构和样式。

W3C DOM由以下三部分组成:

  • 核心DOM - 针对任何结构化文档的标准模型
  • XML DOM - 针对 XML 文档的标准模型
  • HTML DOM - 针对 HTML 文档的标准模型

javascript是ECMAscript的产物、DOM是W3C的产物、两者结合相得益彰。

2.1.1 DOM0、DOM1、DOM2、DOM3的区别

DOM0

实际上是未形成标准的试验性质的初级阶段的DOM,现在习惯上被称为DOM0。它定义了一些document的属性和方法,提供了查询和操作Web文档的内容API。

常在使用的有:forms,cookie,title,其它不建议使用。

W3C结合网景和IE的优点于1998年10月推出了一个标准化的DOM,完成了第一级DOM,即:DOM1。W3C将DOM定义为一个与平台和编程语言无关的接口,通过这个接口程序和脚本可以动态的访问和修改文档的内容、结构和样式。

DOM1级主要定义了HTML和XML文档的底层结构。在DOM1中,DOM由两个模块组成:DOM Core(DOM核心)和DOM HTML。其中,DOM Core规定了基于XML的文档结构标准,通过这个标准简化了对文档中任意部分的访问和操作。DOM HTML则在DOM核心的基础上加以扩展,添加了针对HTML的对象和方法,如:JavaScript中的Document对象。

DOM2级在原来DOM的基础上又扩充了鼠标、用户界面事件、范围、遍历等细分模块,而且通过对象接口增加了对CSS的支持。

在DOM2中引入了下列模块,在模块包含了众多新类型和新接口:

  • DOM视图(DOM Views):定义了跟踪不同文档视图的接口
  • DOM事件(DOM Events):定义了事件和事件处理的接口
  • DOM样式(DOM Style):定义了基于CSS为元素应用样式的接口
  • DOM遍历和范围(DOM Traversal and Range):定义了遍历和操作文档树的接口

DOM3进一步扩展了DOM,在DOM3中引入了以下模块:

  • DOM加载和保存模块(DOM Load and Save):引入了以统一方式加载和保存文档的方法
  • DOM验证模块(DOM Validation):定义了验证文档的方法
  • DOM核心的扩展(DOM Style):支持XML 1.0规范,涉及XML Infoset、XPath和XML Base

其整体的发展历程如下:

DOM发展图

3、事件绑定

3.1 事件的绑定方式

第一种方式:DOM0级,即以属性的方式直接写在行内。一般的验证码切换就有这样的机制。

<a href="#" id="dom0" onclick="changeCaptcha();">

第二种方式:DOM1级,给元素添加属性(例:onclick),属性的值就是一个具体的函数(click事件类型绑定的处理函数)。这里就有一个问题,无法允许团队不同人员对同一元素监听同一事件但做出不用的响应。

<body>
  <div id="event">这是事件机制学习</div>
  <script>
    var div=document.getElementById('event');
    // 甲程序猿
    div.onclick=function(){
        console.log('甲要红背景');
        div.setAttribute('style', 'background: #ff0000');
    };
    // 乙程序猿
    div.onclick=function(){
        console.log('乙要黄背景');
        div.setAttribute('style', 'background: #ffff00');
    };
  </script>
</body>
DOM1点击响应效果

这里面出现的问题:无法给同一个元素绑定多个相同的事件,然而在web开发中,这个是非常普遍的一个应用。

第三种方式:DOM2级,对主流浏览器来说,使用addEventListenerremoveListener方法,它们都接受3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后的布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。若最后的布尔值不填写,则和false效果一样。这里它支持同一dom元素注册多个同种事件,还有新增了捕获冒泡的概念。

<body>
  <div id="event">这是事件机制学习</div>
  <script>
    var div=document.getElementById('event');
    div.addEventListener('click', function(){
        console.log('这是DOM2级,甲绑定事件的的响应');
    });
    div.addEventListener('click', function(){
        console.log('这是DOM2级,乙绑定事件的的响应');
    });
  </script>
</body>
DOM2点击响应效果

它也有问题:兼容性,IE8对此自定义了两个自己的方法attachEventdetachEvent方法。同时,需要注意IE在这里是‘onclick’。

div.attachEvent('onclick', function(){
    console.log('这是DOM2级,IE绑定事件的的响应');
});

那么为了保持浏览器兼容性问题,我们还需要自己封装一个简单的函数去实现事件的绑定。同时,由于attachEvent()方法中的this指向window(下面会有说明),所以需要对this进行显式修改。

<body>
<div id="event">这是事件机制学习</div>
<script>
    var div=document.getElementById('event');
    function reponseCode(){
        console.log('这是封装的绑定事件');
    }
    // 事件的绑定机制
    function addHandle(obj, type, handle){
        if(obj.addEventListener){
            obj.addEventListener(type, handle,false);
        }else if(obj.attachEvent){
            obj.attachEvent('on'+type, function(event) {
                return handler.call(target, event);
            });
        }else{
            obj['on'+type]=handle;
        }
    }
    addHandle(div, 'click', reponseCode);
</script>
</body>

移除事件绑定:通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除,移除时传入的参数与添加处理程序时使用的参数相同。这意味着,addEventListener()添加的匿名函数将无法移除。同理attachEvent()和detachEvent();

无效代码:

<div id="box" style="height:30px;width:200px;background-color:pink;"></div>
<script>
    box.addEventListener("click",function(){
        this.innerHTML += '1'
    },false);
    box.removeEventListener('click',function(){
        this.innerHTML += '1'
    },false);
</script>

有效代码:

<div id="box" style="height:30px;width:200px;background-color:pink;"></div>
<script>
    var handle = function(){
        this.innerHTML += '1'
    };
    box.addEventListener("click",handle,false);
    box.removeEventListener('click',handle,false);    
</script>

说完上面绑定事件的几种方式,这里还要指出一点,即事件处理程序中的this所指。

<div id="box" style="height:100px;width:300px;background-color:pink;"
     onclick = "console.log(this)">//<div>
</div>

<div id="box" style="height:100px;width:300px;background-color:pink;"></div>
<script>
    box.onclick= function(){
        console.log(this);//<div>
    }
</script>

<div id="box" style="height:100px;width:300px;background-color:pink;"></div>
<script>
    box.addEventListener('click',function(){
        console.log(this);//<div>
    });
</script>

<div id="box" style="height:100px;width:300px;background-color:pink;"></div>
<script>
    box.attachEvent('onclick',function(){
        console.log(this);//window
    });
</script>

与其他三个事件处理程序不同,IE事件处理程序的this指向window,而非被绑定事件的元素。

3.2 事件流

javascript操作CSS称为脚本化CSS,而javascript与HTML的交互是通过事件实现的。事件就是文档或浏览器窗口中发生的一些特定的交互瞬间,而事件流(又叫事件传播)描述的是从页面中接收事件的顺序。

3.2.1 历史渊源

当浏览器发展到第四代时(IE4及Netscape4),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件?想象画在一张纸上的一组同心圆。如果把手指放在圆心上,那么手指指向的不是一个圆,而是纸上的所有圆。

两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。如果单击了某个按钮,他们都认为单击事件不仅仅发生在按钮上,甚至也单击了整个页面。

但有意思的是,IE和Netscape开发团队居然提出了差不多是完全相反的事件流的概念。IE的事件流是事件冒泡流,而Netscape的事件流是事件捕获流

一个普通的HTML文档,下面统一使用。

<!DOCTYPE HTML>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <body>
    <div></div>
  </body>    
</html>

3.2.2 'IE'的'事件冒泡流'

IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。

如果单击了页面中的'div'元素,那么这个click事件沿DOM树向上传播,在每一级节点上都会发生,按照如下顺序传播:

(1)    <div>
(2)    <body>
(3)    <html>
(4)    document

所有现代浏览器都支持事件冒泡,但在具体实现在还是有一些差别。IE9、Firefox、Chrome、Safari将事件一直冒泡到window对象,如下。

(1)    <div>
(2)    <body>
(3)    <html>
(4)    document
(5)    window

事件冒泡流测试代码:

<body>
  <div id="box" style="height: 100px;width: 300px;background: pink;"></div>
  <button id="reset">还原</button>
  <script>
    var box=document.getElementById('box');
    var reset=document.getElementById('reset');
    reset.onclick=function(){
        history.go();
    };
    box.onclick=function(){
        box.innerHTML+='div、';
    };
    document.documentElement.onclick=function(){
        box.innerHTML+='html、';
    };
    document.body.onclick=function(){
        box.innerHTML+='body、';
    };
    document.onclick=function(){
        box.innerHTML+='document、';
    };
    window.onclick=function(){
        box.innerHTML+='window<br>';
    };
  </script>
</body>

效果如下:

事件冒泡流

3.2.3 'Netscape'的'事件捕获流'

网景的事件捕获流,思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前就捕获它。

在事件捕获过程中,document对象首先接收到click事件,然后事件沿DOM树依次向下,一直传播到事件的实际目标,即<div>元素。

(1)    document
(2)    <html>
(3)    <body>
(4)    <div>

IE9、Firefox、Chrome、Safari等现代浏览器都支持事件捕获,但是从window对象开始捕获。

(1)    window
(2)    document
(3)    <html>
(4)    <body>
(5)    <div>

事件捕获流代码:

<body>
  <div id="box" style="height: 100px;width: 300px;background: #ccc;overflow: auto;"></div>
  <button id="reset">还原</button>
  <script>
    //IE8-浏览器返回div body html document
    //其他浏览器返回div body html document window
    var box=document.getElementById('box');
    var reset=document.getElementById('reset');
    reset.onclick=function(){
        history.go();
    };
    box.addEventListener('click', function(){
        box.innerHTML+='div\n';
    }, true);
    document.documentElement.addEventListener('click', function(){
        box.innerHTML+='html\n';
    }, true);
    document.body.addEventListener('click', function(){
        box.innerHTML+='body\n';
    }, true);
    document.addEventListener('click', function(){
        box.innerHTML+='document\n';
    }, true);
    window.addEventListener('click', function(){
        box.innerHTML+='window\n';
    }, true);
  </script>
</body>

addEventListener()方法中的第三个参数设置为true时,即为事件捕获阶段。

3.2.4 'W3C'的'DOM2'

事件流又称为事件传播,DOM2级事件规定的事件流包括三个阶段:事件捕获阶段(capture phase)、处于目标阶段(target phase)和事件冒泡阶段(bubbling phase)。

首先发生的是事件捕获,为截获事件提供了机会,然后是实际的目标接收到事件,最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应,如下图。

DOM2事件流

即真正触发事件的dom元素,是捕获事件的终点,是冒泡事件的起点,所以这里就不区分事件了,哪个先注册,就先执行哪个。

3.3 事件委托

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。它能让你避免对特定的每个节点添加事件监听器;相反,事件监听器是被添加到它们的父元素上。事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。

3.3.1 事件委托现象比拟

网摘的大牛基本都用取快递去描述事件委托这个现象,大体是这么个内容。

有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。

这里其实还有2层意思的:

第一,现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的;

第二,新员工也是可以被前台MM代为签收的,即程序中新添加的dom节点也是有事件的。

3.3.2 事件委托实现

我们以下面的代码举例:

<body>
  <ul id="outul">
    <li id="innerli1">事件冒泡流</li>
    <li id="innerli2">事件捕捉流</li>
    <li id="innerli3">事件委托</li>
    <li id="innerli4">事件对象</li>
  </ul>
</body>

1、假设我们要实现,点击每个li元素会在终端输出标签内的文本内容,传统的思想是这样的:我们遍历li标签,然后为他们绑定点击事件。那么脚本是这样的;

<script>
  var outul=document.getElementById('outul');
  var allli=document.getElementsByTagName('li');
  for(var i=0; i<allli.length; i++){
    allli[i].onclick=function(){
      console.log(this.innerHTML);
    }
  }
</script>

2、那么假设我们使用事件委托的方式呢。即我们为li标签的父级标签ul绑定点击事件,从事件冒泡流的原理来说,li的点击事件会冒泡到ul上面,这时,因为之前ul已经绑定过了点击事件,那么这个点击事件就会被触发。这里面就存在了一个问题:我们想要对不同的li标签在响应各自的点击事件时,即事件三要素之一的监听器(事件处理程序)是不一样的,那我们为ul绑定的点击事件又有什么意义呢?

为了解决上面的问题,即我们要知道点击事件被触发时,如何找到相对应的不同的li标签。Event对象(下面的环节会单独介绍)提供了一个了一个属性叫做target,可以返回事件的目标节点,即事件三要素之一的事件源,也就是说target可以表示为当前的事件操作的DOM,但不是真正操作DOM。

Event对象存在兼容性问题,代码大体如下,在下环节里面会具体解释。

  <script>
    var outul=document.getElementById('outul');
    outul.onclick=function(event){
        // event即Event对象
        var ent=event || window.event;
        var target=ent.target || ent.srcElement;
        console.log(target);
        if(target.nodeName.toLowerCase()=='li'){
            console.log(target.innerHTML);
        }
    }
  </script>

这时有人说,上面的都是点击li操作的是同样的效果,要是每个li被点击的效果都不一样,那么用事件委托还有用吗?继续来。

这次换原代码。

<div id="box">
  <input type="button" id="add" value="添加" />
  <input type="button" id="remove" value="删除" />
  <input type="button" id="move" value="移动" />
  <input type="button" id="select" value="选择" />
</div>

假设我们对不用的按钮实现相应的功能,简单输出终端名称来代替。

3、同父元素不同子元素事件监听器不同,传统代码。

<script>
  var add=document.getElementById("add");
  var remove=document.getElementById("remove");
  var move=document.getElementById("move");
  var select=document.getElementById("select");
  add.onclick = function(){
      console.log('添加');
  };
  remove.onclick = function(){
      console.log('删除');
  };
  move.onclick = function(){
      console.log('移动');
  };
  select.onclick = function(){
      console.log('选择');
  }
</script>

4、同父元素不同子元素事件监听器不同,事件委托代码。

<script>
  var box=document.getElementById('box');
  box.onclick=function(event){
      var ent=event || window.event;
      var target=ent.target || ent.srcElement;
      if(target.nodeName.toLowerCase()=='input'){
          switch(target.id){
              case 'add':
                  console.log('添加');
                  break;
              case 'remove':
                  console.log('删除');
                  break;
              case 'move':
                  console.log('移动');
                  break;
              case 'select':
                  console.log('选择');
                  break;
              default:
                  console.log('测试');
          }
      }
  };
</script>

5、同父元素不同子元素+新添加子元素,事件委托。

<body>
<input type="button" id="add" value="添加" />
<ul id="outul">
  <li id="innerli1">事件冒泡流</li>
  <li id="innerli2">事件捕捉流</li>
  <li id="innerli3">事件委托</li>
  <li id="innerli4">事件对象</li>
</ul>
<script>
  var add=document.getElementById('add');
  var outul=document.getElementById('outul');
  add.onclick=function(){
      var li=document.createElement('li');
      var text=document.createTextNode('新事件');
      li.appendChild(text);
      outul.appendChild(li);
  };
  outul.onclick=function(event){
      var ent=event || window.event;
      var target=ent.target || ent.srcElement;
      if(target.nodeName.toLowerCase()=='li'){
          console.log(target.innerHTML);
      }
  };
</script>
</body>

最适合使用事件委托技术的事件包括click、mousedown、mouseup、keydown、keyup和keypress。

4、事件对象

Event 对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。

在触发DOM上的某个事件时,会产生一个事件对象event。这个对象中包含着所有与事件有关的信息。包括导致事件的元素,事件的类型以及其他与特定事件相关的信息。

例:当用户单击某个元素的时候,我们给这个元素注册的事件就会触发,该事件的本质就是一个函数,而该函数的形参接收一个event对象。

有浏览器都支持event对象,但支持方式不同。

4.1 获取事件对象

event对象是事件函数的第一个参数,如文本内容一样,IE8不支持;再者如终端输出,火狐不支持。

<body>
<b>IE8-浏览器输出undefined,其他浏览器则输出事件对象[object MouseEvent]</b><br />
<div id="box" style="height: 30px; line-height: 30px; width: 200px; background: #ccc;"></div>
<script>
  var box=document.getElementById('box');
  box.onclick=function(ent){
      box.innerHTML=ent;
      // 直接访问event变量、firefox浏览器不支持
      console.log(event);
  }
</script>
</body>

然后产生一个兼容所有浏览器的写法:

<body>
<div id="box" style="height: 30px; line-height: 30px; width: 200px; background: #ccc;"></div>
<script>
  var box=document.getElementById('box');
  box.onclick=function(ent){
      ent=ent || event;
      box.innerHTML=ent;
      console.log(ent);
  }
</script>
</body>

这里在之前事件委托中已经使用到。

4.2 事件的属性和方法

事件对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都有些共有的属性和方法。

4.2.1 事件类型

事件有很多类型,事件对象中的type属性表示被触发的事件类型。

<body>
<div id="box" style="height: 30px; line-height: 30px; width: 200px; background: #ccc;"></div>
<script>
  var box=document.getElementById('box');
  box.onclick=box.onmouseover=box.onmouseout=function(ent){
      ent=ent || event;
      box.innerHTML=ent.type;
  }
</script>
</body>

上述代码分别在鼠标移入、点击、移出时显示:mouseover、click、mouseout。

4.2.2 事件目标

关于事件目标,共有currentTarget、target和srcElement这三个属性。

1、currentTarget

currentTarget属性返回事件当前所在的节点,即正在执行的监听函数所绑定的那个节点,但IE8-浏览器不支持。

一般地,currentTarget与事件中的this指向相同。但在attachEvent()事件处理程序中,this指向window。之前事件绑定中已提及。

<body>
<ul id="outul">
  <li>currentTarget</li>
  <li>target</li>
</ul>
<script>
    var outul=document.getElementById('outul');
    var allli=document.getElementsByTagName('li');
    outul.onclick=function(ent){
        ent=ent || event;
        allli[0].innerHTML=ent.currentTarget;
        allli[1].innerHTML=(this===ent.currentTarget);
    }
</script>
</body>

2、target

currentTarget属性返回事件正在执行的监听函数所绑定的节点,而target属性返回事件的实际目标节点,但IE8-浏览器不支持。

<body>
<ul id="outul" style="background: #ccc; width: 300px; height: 60px; line-height: 30px;">
  <li>currentTarget</li>
  <li>target</li>
</ul>
<script>
    var outul=document.getElementById('outul');
    outul.onmouseover=function(ent){
        ent=ent || event;
        ent.target.style.background='red';
        console.log(ent.target);
    };
    outul.onmouseout = function(ent){
        ent=ent || event;
        ent.target.style.background='#ccc';
        console.log(ent.target);
    };
</script>
</body>

上部分代码分别有三部分可以移入、移出,分别是左部、右侧上部、右侧下部。效果是移入变红、移出回显灰色,两者终端都会输出相应节点。

3、srcElement

srcElement属性与target的功能一致,但火狐不兼容。

<body>
<ul id="outul" style="background: #ccc; width: 300px; height: 60px; line-height: 30px;">
  <li>currentTarget</li>
  <li>target</li>
</ul>
<script>
    var outul=document.getElementById('outul');
    outul.onmouseover=function(ent){
        ent=ent || event;
        ent.srcElement.style.background='red';
        console.log(ent.srcElement);
    };
    outul.onmouseout = function(ent){
        ent=ent || event;
        ent.srcElement.style.background='#ccc';
        console.log(ent.srcElement);
    };
</script>
</body>

故结合以上三点,一般兼容代码如下:

<script>
    var handler = function(ent){
        ent=ent || event;
        var target=ent.target || ent.srcElement;
    };
</script>

4.2.3 事件冒泡

事件冒泡是事件流的第三个阶段,通过事件冒泡可以在这个阶段对事件做出响应。

关于冒泡,事件对象中包含bubbles、cancelBubble、stopPropagation()和stopImmediatePropagation()这四个相关的属性和方法。

1、属性bubbles

bubbles属性返回一个布尔值,表示当前事件是否会冒泡。该属性为只读属性。

发生在文档元素上的大部分事件都会冒泡,但focus、blur和scroll事件不会冒泡。所以,除了这三个事件bubbles属性返回false外,其他事件该属性都为true。

<body>
<button id="btn">按钮</button>
<input id="test">
<script>
    var btn=document.getElementById('btn');
    var test=document.getElementById('test');
    btn.onclick=function(ent){
        ent=ent || event;
        btn.innerHTML=ent.bubbles;
        console.log(ent.bubbles);
    };
    test.onfocus=function(ent){
        test.innerHTML=ent.bubbles;
        console.log(ent.bubbles);
    }
</script>
</body>

按钮点击后,上面的值会变成true,输入框聚焦后没反应,但两者终端都会输出相应的bubbles的值。

2、方法stopPropagation()

stopPropagation()方法表示取消事件的进一步捕获或冒泡,无返回值,但IE8-浏览器不支持。

<body>
<button id="btn" style="width: 100px;">按钮</button>
<input id="test">
<script>
    var btn=document.getElementById('btn');
    var test=document.getElementById('test');
    btn.onclick=function(ent){
        ent=ent || event;
        test.value+='按钮栏、';
        // ent.stopPropagation();
    };
    document.body.onclick=function(ent){
        ent=ent || event;
        test.value+='文档。';
    }
</script>
</body>

正常代码这样,从W3C事件流的说法,假设window->div这样是从外到内,点击事件被由外到内分别捕捉,目标接收事件,再由内到外冒泡响应。

所以上面的代码结果是在点击按钮之后,输入框会显示“按钮栏、文档。”;那么如果把阻止冒泡的语句注释删去的话,响应结果就会变成这样“按钮栏、”。

3、方法stopImmediatePropagation()

stopImmediatePropagation()方法不仅可以取消事件的进一步捕获或冒泡,而且可以阻止同一个事件的其他监听函数被调用,无返回值,但IE8-浏览器不支持。

<body>
<button id="btn" style="width: 100px;">按钮</button>
<input id="test">
<script>
    var btn=document.getElementById('btn');
    var test=document.getElementById('test');
    btn.addEventListener('click', function(ent){
        ent=ent || event;
        test.value+='按钮栏、';
        // ent.stopImmediatePropagation()
    }, false);
    btn.addEventListener('click', function(ent){
        ent=ent || event;
        btn.style.background='#ff0000';
    }, false);
    document.body.addEventListener('click', function(ent){
        ent=ent || event;
        test.value+='文档。';
    }, false);
</script>
</body>

上面的代码结果是在点击按钮之后,输入框会显示“按钮栏、文档。”,且按钮底色会变红;那么如果把阻止冒泡的语句注释删去的话,响应结果就会变成这样“按钮栏、”,它既阻止了点击事件向body层冒泡,还阻止了同层监听点击事件底色变化。

这里面我们可以知道,事件是先注册,先调用的原则。

4、cancelBubble

cancelBubble属性只能用于阻止冒泡,无法阻止捕获阶段。该值可读写,默认值是false。当设置为true时,cancelBubble可以取消事件冒泡。该属性全浏览器支持,但并不是标准写法

<body>
<button id="btn" style="width: 100px;">按钮</button>
<input id="test">
<script>
    var btn=document.getElementById('btn');
    var test=document.getElementById('test');
    btn.addEventListener('click', function(ent){
        ent=ent || event;
        test.value+='按钮栏、';
        ent.cancelBubble=true;
    }, false);
    document.body.addEventListener('click', function(ent){
        ent=ent || event;
        test.value+='文档。';
    }, false);
</script>
</body>

当使用stopIPropagation()方法或stopImmediatePropagation()方法时,关于cancelBubble值的变化,各浏览器表现不同。

  • chrome/safari/opera中,cancelBubble的值为false。
  • IE9+/firefox中,cancelBubble的值为true。

兼容处理,阻止冒泡:

var handler = function(ent){
    ent=ent || event;
    if(ent.stopPropagation){
        ent.stopPropagation();
    }else{
        ent.cancelBubble = true;
    }
}

4.2.4 事件流:eventPhase属性

eventPhase属性返回一个整数值,表示事件目前所处的事件流阶段,但IE8-浏览器不支持。

0表示事件没有发生,1表示捕获阶段,2表示目标阶段,3表示冒泡阶段。

<body>
<button id="btn" style="width: 100px;">按钮</button>
<script>
  document.body.addEventListener('click', function(ent){
      ent=ent || event;
      btn.innerHTML=ent.eventPhase;
  }, true);
</script>
</body>

效果为“按钮”变成“1”。
换脚本:

<script>
  var btn=document.getElementById('btn');
  btn.addEventListener('click', function(ent){
      ent=ent || event;
      btn.innerHTML=ent.eventPhase;
  }, false);
</script>

效果为“按钮”变成“2”。

换脚本:

<script>
  document.body.addEventListener('click', function(ent){
      ent=ent || event;
      btn.innerHTML=ent.eventPhase;
  }, false);
</script>

效果为“按钮”变成“3”。

这里大致可以看出W3C对事件流定义的三个阶段。

4.2.5 取消默认行为

常见的浏览器默认行为有点击链接后,浏览器跳转到指定页面;或者按一下空格键,页面向下滚动一段距离。

关于取消默认行为的属性包括cancelable、defaultPrevented、preventDefault()和returnValue。

使用:

  • 在DOM0级事件处理程序中取消默认行为,使用returnValue、preventDefault()return false都有效

  • 在DOM2级事件处理程序中取消默认行为,使用return false无效

  • IE事件处理程序中取消默认行为,使用preventDefault()无效

1、cancelable属性

cancelable属性返回一个布尔值,表示事件是否可以取消。该属性为只读属性。返回true时,表示可以取消。否则,表示不可取消。IE8-浏览器不支持。

<a id="test" href="#">链接</a>
<script>
var test=document.getElementById('test');
test.onclick= function(ent){
    ent=ent || event;
    test.innerHTML=e.cancelable;
}
</script>

效果:点击“链接”变成“true”。

2、preventDefault()方法

preventDefault()方法取消浏览器对当前事件的默认行为,无返回值,IE8-浏览器不支持。

<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
  var test=document.getElementById('test');
  test.onclick= function(ent){
      ent=ent||event;
      ent.preventDefault();
  }
</script>

效果:不转跳。

3、returnValue属性

returnValue属性可读写,默认值是true,但将其设置为false就可以取消事件的默认行为,与preventDefault()方法的作用相同,firefox和IE9+浏览器不支持。

做兼容处理:

var handler = function(ent){
  ent=ent || event;
  if(ent.preventDefault){
      ent.preventDefault();
  }else{
      ent.returnValue=false;
  }
}

4、return false

<script>
  var test=document.getElementById('test');
  test.onclick= function(ent){
    return false;
  }
</script>

效果:不转跳。

5、defaultPrevented属性

defaultPrevented属性表示默认行为是否被阻止,返回true时表示被阻止,返回false时,表示未被阻止。

<a id="test" href="http://www.cnblogs.com">链接</a>
<script>
  var test=document.getElementById('test');
  test.onclick= function(ent){
    ent=ent || event;
    if(ent.preventDefault){
        ent.preventDefault();
    }else{
        ent.returnValue=false;
    }
    test.innerHTML=ent.defaultPrevented;
  }
</script>

效果:点击“链接”变为“true”。

5、完整的简单事件相关代码

<script>
  var EventUtil={
    // 事件对象
    getEvent: function(event){
      return event||window.event;
    },
    // 事件目标节点
    getTarget: function(event){
      return event.target||event.srcElement;
    },
    // 阻止事件的默认行为
    preventDefault: function(){
      if(event.preventDefault){
        event.preventDefault();
      }else{
        event.returnValue=false;
      }
    },
    // 阻止向上冒泡
    stopPropagation: function(){
      if(event.stopPropagation){
          event.stopPropagation();
      }else{
          event.cancelBubble=true;
      }
    },
    // DOM2级添加事件  
    addHandler: function(element, type, handler){
      if(element.addEventListener){
          element.addEventListener(type, handler, false);
      }else if(element.attachEvent){
        element["e"+type]=function(){
          handler.call(element)
        };
        element.attachEvent("on"+type, element["e"+type]);
      }else{
        element["on"+type]=handler;
      }
    },
    // DOM2级移除事件    
    removeHandler: function(element, type, handler){
      if(element.removeEventListener){
        element.removeEventListener(type, handler, false);
      }else if(element.detachEvent){
        element.detachEvent("on"+type, element["e"+type]);
        element["e"+type]=null;
      }else{
        element["on"+type]=null;
      }
    }
  };
</script>

6、内存泄漏

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

6.1 垃圾回收机制

大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"(garbage collector)。

垃圾回收机制最常使用的方法叫做"引用计数"(reference counting):语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放。

let arr = [1, 2, 3, 4];
console.log('hello world');
arr = null;

数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。

如果增加最下面那行代码,解除arr对[1, 2, 3, 4]引用,这块内存就可以被垃圾回收机制释放了。

JavaScript中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能用到它们。而当变量离开环境时,这将其 标记为“离开环境”。虽然JavaScript 会自动垃圾收集,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态,无法被回收。

6.2 意外的全局变量

JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是 window 。

可能一般写了这段代码:

function foo(arg) { 
  bar = "this is a hidden global variable"; 
}

然而这段代码的执行是这样:

function foo(arg) { 
  window.bar = "this is a hidden global variable"; 
}

另一种意外导致定了了全局变量:

function foo() { 
    this.variable = "potential accidental global"; 
} 
// foo 调用自己,this 指向了全局对象(window),而不是 undefined 
foo();

在JavaScript文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。

6.3 被遗忘的计时器或回调函数

var someResource=getData();
setInterval(function() {
  var node=document.getElementById('Node');
  if(node) {
    // 处理 node 和 someResource 
    node.innerHTML = JSON.stringify(someResource);
  }
}, 1000);

如果idNode的元素从 DOM 中移除,该定时器仍会存在,同时,因为回调函数中包含对 someResource的引用,定时器外面的someResource也不会被释放。

6.4 脱离DOM的引用

保存DOM节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON键值对)或者数组很有意义。此时,同样的DOM元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。

  var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
  };

  function doStuff() {
    image.src = 'http://some.url/image';
    button.click();
    console.log(text.innerHTML);
    // 更多逻辑 
  }

  function removeButton() {
    // 按钮是 body 的后代元素 
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用 
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。 
  }

然后还有'DOM树'内部或子节点的引用问题。假如你的JavaScript代码中保存了表格某一个'td'的引用。将来决定删除整个表格的时候,直觉认为'GC'会回收除了已保存的'td'以外的其它节点。实际情况并非如此:此'td'是表格的子节点,子元素与父元素是引用关系。由于代码保留了'td'的引用,导致整个表格仍待在内存中。保存'DOM'元素引用的时候,要小心谨慎。

6.5 闭包

闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。

简单来说是这个样子的:

<script>
  var leaks=(function(){
    var leak='closure';
    return function(){
      console.log(leak);
    }
  })();
</script>

参考博文:一个意想不到的js内存泄漏

参考博文:JS常见4种内存泄漏和chrome开发工具监测介绍

全文来源博文:javascript学习目录

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容