第十三章:事件(事件流、事件处理程序)

事件

  • JavaScript和HTML之间的交互是通过事件实现的。事件最早是在IE3和Netscape Navigator2中出现的,当时是作为分担服务器运算负载的一种手段。在IE4和Netscape Navigator4发布时,这两种浏览器都提供了相似但不相同的API。IE9、Firefox、Opera、Safari和Chrome全都实现了“DOM2级事件”模块的核心部分。IE8是最后一个仍然使用其专有事件系统的主要浏览器。

事件流

  • 当浏览器发展到第四代(IE4和Netscape Navigator4),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件?两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。他们认为:如果你单击了某个按钮,这个单击事件并不仅仅发生在按钮上。换句话说,你也单击了按钮的容器元素,甚至也单击了整个页面
  • 事件流描述的是从页面中接收事件的顺序。有意思的是,IE和Netscape开发团队居然提出了差不多是完全相反的事件流的概念。IE的事件流是事件冒泡流,而Netscape Communicator则是事件捕获流

事件冒泡

  • 即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点(或文档)。所有现代浏览器都支持事件冒泡。IE5.5-事件冒泡会跳过<html>元素。IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。

事件捕获

  • 事件捕获的顺序是从不太具体的节点到具体的节点。事件捕获的用意在于事件到达预定目标之前捕获它。虽然事件捕获是Netscape Communicator唯一支持的事件流模型,但IE9、Firefox、Chrome和Safari目前也都支持这种事件模型。“DOM 2级事件”规范要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。
DOM事件流
  • “DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段处于目标阶段事件冒泡阶段。首先发生的是事件捕获,为截取事件提供了机会。再是实际的目标接收事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。对于下面的div元素:
<!DOCTYPE html>
<html>
<head>
    <title>example</title>
</head>
<body>
    <div id="myDiv">click me</div>
</body>
</html>
  • 单击div元素会按照下面的顺序触发事件:
    这里写图片描述
  • 在DOM事件流中,实际的目标(div)在捕获阶段不会接受到事件。下一个阶段是“处于目标”阶段,于是事件在div上发生,并在事件处理中被看成冒泡阶段的一部分。最后,冒泡事件发生,事件又传播回文档(这个是DOM2级事件规范)。不过,IE9、Firefox、Chrome、Safari和Opera9.5+都会在捕获阶段触发事件对象上的事件。IE8-不支持DOM事件流。

事件处理程序

  • 事件就是用户或浏览器自身执行的某个动作。诸如:click、load、mouseover,都是事件的名字。而响应事件的函数就叫做事件处理程序(或事件监听器)。事件处理程序的名字以“on”开头,因此click事件的事件处理程序就是onclick,load就是onload。为事件指定处理程序的方式有以下几种。

HTML事件处理程序

  • 顾名思义,我们可以直接通过元素的特性为元素指定事件处理程序。这个特性的值是能够执行的JS代码。例如:
    <div id="myDiv" onclick="alert('clicked!');">click me</div>

    <div id="myDiv" onclick="fun();">click me</div>
    <script>fun(){alert('clicked!');}</script>
  • 这两种方式太常用了,不过我们还是要思考一下它具体是怎么工作的。一个最本质的问题,它的作用域是什么?通过第二种方式,可以看见全局作用域在这段代码的作用域中(因为他调用了全局作用域下的一个函数)。书上的解释是这样的,这个特性相当于创建了如下一个函数:
    function() {
        with(document) {
            with(this) {
                //特性值
            }
        }
    }
  • 首先这里的this指代的是当前元素的引用。这段代码的意义是为特性值创建了一个函数,且将document和this依次加入这个函数的作用域的前端,所以执行这么一段代码时,会先在this的作用域中寻找变量,再从document中寻找,最后因为这个匿名函数也是属于window的,所以最终会在window下查找。特殊情况,如果该元素是表单的一个输入元素,则作用域还会包含访问表单元素的入口,这个函数就变成了:
    function() {
        with(document) {
            with(this.form) {
                with(this) {
                    //特性值
                }
            }
        }
    }
  • 为了验证上面的说法,我做了下面的一个实验:
<!DOCTYPE html>
<html>
<head>
    <title>HTML Event Handler Example</title>
</head>
<body>
<form method="post">
    <input type="text" name="username" value="enter username">
    <input type="button" value="test1" onclick="alert(value)">
    <input type="button" value="test2" onclick="alert(username.value)">
    <input type="button" value="test3" onclick="alert(value_test)">
</form>
<script>
    window.value_test = "window";
    document.value_test = "document";
</script>
</body>
</html>
  • 对于第一个按钮,会先搜索this下面的value,即“test1”。对于第二个按钮,由于form在其作用域中,结果也在预想之内。对于第三个按钮,会返回“document”,如果将document.value_test = “document”;这行删除,则会返回“window”。从上面的测试可以得出,它的作用域的确如上所说。
    不过这里还有一个很容易犯错的地方。如果我不是写成alert(value)。而是写成:
<!DOCTYPE html>
<html>
<head>
    <title>HTML Event Handler Example</title>
</head>
<body>
<form method="post">
    <input type="text" name="username" value="enter username">
    <input type="button" value="test1" onclick="fun_alert()">
    <input type="button" value="test2" onclick="alert(username.value)">
    <input type="button" value="test3" onclick="alert(value_test)">
</form>
<script>
    window.value_test = "window";
    document.value_test = "document";
    window.value = "haha";
    function fun_alert() {
        alert(value);
    }
</script>
</body>
</html>
  • 其结果会是“haha”。理由也很简单,上面的写法相当于:
    function() {
        with(document) {
            with(this.form) {
                with(this) {
                    fun_alert();
                }
            }
        }
    }
    function fun_alert() {
        alert(value);
    }
    window.value = "haha";
  • 很明显这里的fun_alert()方法的作用域是函数内部->window,所以其结果也会是“haha”。那么在这里有个问题。如果我把window.value = "haha";这行去掉,那这个方法会不会正常返回this下面的value(test1)呢。自然也不会,这里还会抛一个异常(value is not defined)。因为整个执行过程是这样的。它先要执行fun_alert(),而fun_alert()方法需要查找,他依次从this->form->document->匿名函数内部(肯定没有,因为这个匿名函数就一个with块)->window。直到找到了window下的fun_alert()再执行它。而fun_alert()的作用域则没有那么复杂,他的作用域就是函数内部->全局作用域window。所以如果自身和window下面都没有value,这个函数即会报错。所以通过函数去调用alert,和直接写一个alert的区别还是挺大的
  • 这里有一个问题,如果我在这个特性值内部声明变量,该变量是否会成为该元素对象的一个属性?我们来做一个实验(其实这里涉及到了with的知识):
<!DOCTYPE html>
<html>
<head>
    <title>HTML Event Handler Example</title>
</head>
<body>
<form method="post">
    <input type="text" name="username" value="enter username">
    <input type="button" name="xx" value="test1" onclick="alert(c);alert(value);
    var c = 1;var value=2;alert(value);">
</form>
<script>
    window.value_test = "window";
    document.value_test = "document";
    //window.value = "haha";
    /*function fun_alert() {
        alert(value);
    }*/
</script>
</body>
</html>
  • 首先要明确的是在with块里使用var 声明变量是不会将该变量设置成with中指定的作用域中的属性的(前提是该变量名是新的,如果已存在,则会修改,这种情况和这句话并不矛盾)。所以this中不会添加c这个属性。那上面这段代码为什么不会报错呢?我在var c之前就先alert了。其实这里存在变量声明提升,此处的var c = 1相当于:
    function() {
        var c;
        var value;
        with(document) {
            with(this.form) {
                with(this) {
                    alert(c);//undefined
                    alert(value);//undefined 这个地方非常有意思,我觉得应该是this.value才对。
                    c = 1;
                    value = 2;//这个操作也很有意思,如果真如我所想,这个操作能改变this.value;
                    alert(value);//2 因为上一句话不能改变this.value。所以这个地方也应该alert(this.value),也不为2才对。可见事情并没有我想的那么简单。
                }
            }
        }
    }
  • 如果真如上面的代码,只要把上面的this改成通过document.getxx获得的节点引用,则结果完全不一样。所以,我觉得这里的作用域链更像是:匿名函数内部->this->form->document->window,但是这样的逻辑按照我现在的认知还无法写出一套代码,等我以后再来解决。
  • 这里还有一个问题,如果当前区域(或元素,这是另外一个例子)存在fun_alert()这个方法呢?结果会如何?还是会弹出“haha”吗?有可能是“test1”吗?我们稍微修改一下代码再做一个实验(我的想法:因为没有为fun_alert()指定调用的对象,fun_alert的内部的this必定为window,也就是说alert(this.value)必定是window.value。但alert(value)则不然):
<!DOCTYPE html>
<html>
<head>
    <title>HTML Event Handler Example</title>
</head>
<body>
<form method="post">
    <input type="text" name="username" value="enter username">
    <input id="ts" type="button" name="xx" value="test1" onclick="
    function fun_alert() {
      alert(value);
    };fun_alert();">
</form>
<script>
    window.value_test = "window";
    document.value_test = "document";
    var value = "haha";
</script>
</body>
</html>
  • 点击按钮后会输出test1,这里function fun_alert()就是一个闭包,它的value先是访问闭包内部,再接着就是之前那样的顺序,所以结果是test1也没有什么稀奇的。然后我把fun_alert设置为该节点的一个方法:
<!DOCTYPE html>
<html>
<head>
    <title>HTML Event Handler Example</title>
</head>
<body>
<form method="post">
    <input type="text" name="username" value="enter username">
    <input id="ts" type="button" name="xx" value="test1" onclick="
    fun_alert();">
</form>
<script>
    document.getElementById("ts").fun_alert = function () {
        alert(value);
    };
    window.value_test = "window";
    document.value_test = "document";
    var value = "haha";
</script>
</body>
</html>
  • 结果为haha。其实这些例子更多和with,作用域链有关,这里是再巩固一下。之所以为haha,是因为这里找到了Node下的fun_alert(),但是这个fun_alert()的写法不是输出this.value(this对象是在运行时基于函数的执行环境绑定的,当函数作为某个对象的方法执行调用时,this等于那个对象),而是输出value。我们看一下这个方法的位置,它并不是一个闭包,复习前面的知识:在创建fun_alert函数时,会创建一个预先包含全局变量对象的作用域链,也就是说fun_alert函数的Scope Chain中的已经指向了全局变量对象了。然后它不是闭包(没有被其他函数包着),那么函数的Scope Chain就只包含全局作用域。最终在调用的时候,再加入函数内部的执行环境,也就是说它的作用域链中是不包含节点的(但是this依然指向节点,因为它是对象的一个方法,且我是在with(this){}内部寻找fun_alert。相当于with(this){this.fun_alert()} )。而fun_alert();只是返回了这样一个方法并且去执行,这个方法的作用域链中没有节点,所以最终会输出全局作用域下的value
  • 这样指定事件处理程序会创建一个封装着元素属性值的函数(可以想成之前的匿名函数),这个函数中除了arguments还有一个局部变量event事件对象,下一篇文章细说)。
  • 这种写法的缺点是扩展作用域链在不同浏览器可能会导致不一样的结果,且HTML和JavaScript代码紧密耦合。所以多数开发人员更乐意用JavaScript指定事件处理程序

DOM0级事件处理程序

  • 通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。每个元素(包括window和document)都有自己的事件处理程序属性。这些属性通常全部小写
    var btn = document.getElementById("myBtn");
    btn.onclick = function(){alert('clicked!')};
---------------------------------------------------
    //书上说:
    <input type="button" id="myBtn" value="Click Me" onclick="alert('clicked!')"/>
    等价于btn.onclick = function(){
        alert('clicked!');
    };
    //但是如果改成alert(value);结果完全不一样(一个输出this.value,一个报错),所以还是有待思考。
  • 使用DOM0级方法指定的事件处理程序被认为是元素的方法。所以该方法中的this指向当前元素。以这种方式添加的事情处理程序会在事件流的冒泡阶段被处理。通过btn.onclick = null;也可以删除事件处理程序。再次单击按钮将不会有任何动作发生。

DOM2级事件处理程序

  • IE9+、其他浏览器支持DOM2级事件。该模块定义了两个方法:addEventListener()removeEventListener()。所有DOM节点都包含这两个方法,并且他们接收3个参数:要处理的事件名作为事件处理程序的函数、一个布尔值(false表示在冒泡阶段调用函数,true代表在捕获阶段调用函数,一般用false)。
    var fun1 = function(){
        alert(this.id);
    }
    var fun2 = function(){
        alert("hello");
    }
    var btn = document.getElementById("myBtn");
    btn.addEventListener("click", fun1, false);
    //按照fun1->fun2的顺序,如果都是冒泡阶段。
    //不过在这里将false改成true不会影响执行顺序,因为btn的目标节点(目标节点的捕获阶段即冒泡阶段,也可以说捕获阶段不包含目标节点)。
    //如果把btn改成document就会有不一样的结果。
    btn.addEventListener("click", fun2, true);
    btn.removeEventListener("click", fun2, false);
  • 可以多次添加事件处理程序,通过同样的参数也可以移除相应的事件处理程序。如果add的时候传入了匿名函数,则无法通过removeEventListener移除,因为你没有匿名函数的引用。多次添加同一事件处理程序(比如add fun2两次,且都在同一阶段,也就是第三个布尔值一致)就只有一次的效果。

IE事件处理程序

  • IE中提供了和上述两个方法类似的attachEvent()和detachEvent()。这两个方法只接受两个参数:事件名事件处理程序函数。因为IE8-中只支持事件冒泡,所以attachEvent()添加的事件处理程序都会被添加到冒泡阶段
  • attachEvent()和detachEvent()中,传入的第一个值是特性名,而不是省略“on”的名称。且attachEvent()添加函数顺序,与执行顺序刚好相反(IE8-,如果是IE9+则是和添加顺序相同),且添加的程序函数的this指向window,而不是当前节点
<!DOCTYPE html>
<html>
<head>
    <title>Internet Explorer Event Handler Example</title>
</head>
<body>
    <input type="button" id="myBtn" value="Click Me" />
    <p>This example works only in Internet Explorer.</p>
    <script type="text/javascript">
        var btn = document.getElementById("myBtn");
        btn.attachEvent("onclick", function(){
            alert("Clicked");//IE8下后弹出
            alert(this === window);//true
        });
        btn.attachEvent("onclick", function(){
            alert("Hello world!");//IE8下先弹出
        });

    </script>
</body>
</html>

跨浏览器的事件处理程序

  • 了解了IE事件处理程序和一般的事件处理程序的区别后,我们可以写出一套通用的代码:
var EventUtil = {
    addHandler: function(element, type, handler){
        if (element.addEventListener){
            element.addEventListener(type, handler, false);
        } else if (element.attachEvent){
            element.attachEvent("on" + type, handler);
        } else {
            element["on" + type] = handler;
        }
    },

    removeHandler: function(element, type, handler){
        if (element.removeEventListener){
            element.removeEventListener(type, handler, false);
        } else if (element.detachEvent){
            element.detachEvent("on" + type, handler);
        } else {
            element["on" + type] = null;
        }
    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值