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("Clicked")"/>
这种对于简单语句还行, 除了提供元素属性值的方式, 那么有木有其它方式咧? 答案:当然有 , 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”, 这时是不是需要同时去修改.
未完, 后续 …