乐趣区

整理DOM事件相关知识点

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”, 这时是不是需要同时去修改.

未完, 后续 …

退出移动版