DOM事件相关内容

当用户与浏览器发生的一些交互时, 如果希望去获得用户行为, 就需要借助事件来完成. 事件部分内容在 JS中重要性不言而喻.

罗列需要了解与事件相关的知识如下: 这也是面试中遇到的问题.

  • DOM 事件的级别
  • DOM 事件模型
  • DOM 事件流
  • DOM 事件处理程序
  • 描述DOM事件捕获(冒泡)的具体流程
  • Event对象常见的应用
  • 自定义事件

事件级别

  • DOM0
  • DOM2
  • DOM3

DOM 事件模型

  • 事件冒泡
  • 事件捕获

什么是事件流

要想明白事件流,必须先懂的这几个知识点

  • 事件冒泡
  • 事件捕获

先来看一个有趣的问题, 这是 4 代浏览器(IE4)开发团队遇到一个的问题:

What part of the Webpage owns a specific event?

页面的哪一部分会拥有某个特定的事件?

要想明白这个问题可以想象成在一个页面上画了一组同心圆, 当手指放在中间时,它不仅在一个圆圈内,而且在所有的圆圈内。

如下图所示:

这就是浏览器事件的工作原理, 当你点击一个按钮时, 不仅单击按钮,还单击包含的容器和整个页面。

事件生命周期, 分为三个阶段: capturing (捕获), target (目标), bubbling (冒泡). 而这三个阶段也是构成事件流基本部分.

这种处理思想, 在现在流行 NodeJS 后端框架 Koa 对中间件的处理模式也是基于这种, 熟悉Koa或许对这洋葱图并不会陌生:

先来熟悉事件的模型:

Javascript Events: Event bubbing (事件冒泡)

事件冒泡: 既事件开始由最具体的元素接收,然后逐级向上传播最后到达 Document 对象 或 window 上.

先来看一个简单的示例, 代码如下:

    <!DOCTYPE HTML>    <html>        <head>            <title>......</title>        </head>        <body>            <div id="demo"> Press here.</div>        </body>    </html>
    var target = document.getElementById("demo");    window.addEventListener("click", function(){        console.log("window bubbling");    });    document.addEventListener("click", function(){        console.log("document bubbling");    });    document.documentElement.addEventListener("click", function(){        console.log("html bubbling");    });    document.body.addEventListener("click", function(){        console.log("body bubbling");    });    target.addEventListener("click", function(){        console.log("target bubbling");    });

控制台打印输出如下

    "target bubbling"    "body bubbling"    "html bubbling"    "document bubbling"    "window bubbling"

当一个div 被点击, 这点击事件发生的顺序如下:

  • div
  • body
  • html
  • Document
  • Window (现在浏览器中, IE9+, Firfox, Chrome 等)

从执行顺序来说, click事件首先在 div 元素上触发, 然后沿着DOM Tree 向上传播, 在路径上的每个节点上触发,直到它到达文档对象(或Window对象)。

Javascript Events: Event Capturing (捕获)

事件捕获 是另外一种事件流模型, 最先由 Netscape Browser 引入.

根据上面的的模型, 刚好与前面冒泡相反. 根据该模型,最不特定(最外层)的节点首先接收事件,而最特定(目标元素)的节点最后接收事件.

它设计的目标就是在事件到达目标之前,事先进行拦截.

参考前面的示例, 修改下监听方式, 代码修改如下:

    var target = document.getElementById("demo");    window.addEventListener("click", function(){        console.log("window Capturing");    }, true);    document.addEventListener("click", function(){        console.log("document Capturing");    }, true);    document.documentElement.addEventListener("click", function(){        console.log("html Capturing");    }, true);    document.body.addEventListener("click", function(){        console.log("body Capturing");    }, true);    target.addEventListener("click", function(){        console.log("target Capturing");    }, true);

打印的结果:

    "window bubbling"    "document bubbling"    "html bubbling"    "body bubbling"    "target bubbling"

当点击 div 元素, 按照如下顺序来进行广播事件.

  • Window (现在浏览器中, IE9+, Firfox, Chrome 等)
  • Document
  • Document
  • body
  • div

注意:

DOM0 级中默认就是使用冒泡的方式, 不支持捕获的. 所以在 DOM2 级中通过 addEventListener 提供的第三个参数来控制使用哪种事件流来处理.

Javascript Events: DOM Event Flow (事件流)

根据上面两种模型, 可以总结完整事件流应该向如下:

DOM Level 2 Events 指定的事件流模型分为三个阶段:

  • Event Capturing Phase (事件捕获阶段)
  • At the target (目标阶段)
  • Event Bubbling Phase (事件冒泡阶段)

从上面流程图中, 首先发生的是 事件捕获阶段 为截获事件提供了机会, 再到目标阶段 然后进入 事件冒泡阶段, 可以在这个阶段对事件进行响应.

引用前面的例子, 点击 DIV 元素时, 事件将按照上图顺序进行触发.

这就是完整的事件流, 内容看起来挺多的, 实际上一句话就概述事件流.

用来描述事件发生顺序(页面接收事件顺序).

事件处理方式

通过前面的知识点, 事件就是表示用户或浏览器自身执行某种动作. 例如: click, dbclick, load, unload, mouseover,mouseout, mouseenter ,mouseleave 等等, 这些都是事件的名字. 而响应并处理某个事件的函数称为 事件处理程序(或事件监听器(观察者模式)).

常见事件处理方式包括如下几种:

  • HTML 事件处理程序
  • DOM0 级处理程序
  • DOM2 级处理程序

HTML 事件处理程序

直接来看个简单示例:

    <input type="button" onclick="alert('<Clicked')"/>

这种模式, 可以理解为 CSS 行内样式

    <div style="color: #ccc;"></div>

直接在 HTML 元素上绑定相应事件名, 并指定对应的事件处理程序. 从前面语法来说, 事件处理程序是一个函数, 上面传递是一个语句, 也就是说默认执行时, JS引擎会进行相应处理. 等价于这种方式:

    <input type="button" onclick="(function(){alert('Clicked')})()"/>

如果把事件处理函数直接以这种内联值的方式提供, 应该注意不能值中指定未经转义的HTML语法字符 例如: 和号(&) 、双引号("") 等等, 否则会解析出错.

如上面示例,如果想使用双引号, 必须这么处理.

    <input type="button" onclick="alert(&quot;Clicked&quot;)"/>

这种对于简单语句还行, 除了提供元素属性值的方式, 那么有木有其它方式咧? 答案:当然有 , HTML事件处理程序可以调用在页面其它地方定义的脚本.

示例如下:

    <input type="button" onclick="handleClick()"/>    <script>        function handleClick(){            alert("Clicked");        }    </script>

通过这种方式指定事件处理程序具有一些特别的地方.

  • 前面讲述事件处理程序引擎在执行时, 默认封装函数. 在封装的函数中包含一个局部变量 event, 也就是事件对象.
        <input type="button" onclick="alert(event)"/>    // => 等价    <input type="button" onclick="(function(){var e = event; alert(e);})()"/>
  • 该函内部 this 指针表示是当前的目标对象
    <input type="button" onclick="alert(this)"/>    // this === [object HTMLInputElement] === input元素    

这样可以通过 this来获取目标元素相关的内容了. 比如取值

    <input type="button" onclick="alert(this.value)"/>    <!--或简写成这样-->    <input type="button" value="Click Me" onclick="alert(value)"/>

为什么可以简写方式, input 对象是存在于当前函数的作用域链中(scope). 为了调试方式, 把上面方式作如下改变

    <input type="button" value="Click Me" onclick="(function aa(){debugger;console.log(value)})()"/>

其实理解上面那句话, 我们可以借助开发这工具来协助理解:

  • 从执行栈(Call Stack)可以看出, 证明前面说的默认会创建封装函数 .
  • 从 Scope 作用链中可以看出, 前面说的 input 对象会被添加在当前匿名函数执行的作用域链上.

根据开发工具来看, 我们是可以模拟引擎帮做的事情, 伪代码如下:

    function () {        with(document) {            with(input){                // dosomething                console.log(value);                }        }    }

本质通过 with 来扩展作用域.

上面把基本内容说, 那么这种通过HTML事件处理程序有什么问题么 ?

  • 时差问题

    事件处理程序必须优先元素加载, 有可能DOM加载出来, 用户就开始操作, 此时事件处理程序可能未加载导致报错

  • 前面提及扩展程序的作用域链可能在不同浏览器中兼容不一样,导致程序出现错误
  • 视图和行为偶合在一起,也就是说事件处理程序修改相应JS部分、HTML部分都需要去修改.

引用前面的示例:

    <input type="button" onclick="handleClick()"/>    <script>        function handleClick(){            alert("Clicked");        }    </script>

如果用户想把函数名为: "onClick", 这时是不是需要同时去修改.

未完, 后续...