浏览器事件模型 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lucifer210
V2EX    前端开发

浏览器事件模型

  •  1
     
  •   lucifer210
    azl397985856 2019-11-16 18:11:29 +08:00 2537 次点击
    这是一个创建于 2234 天前的主题,其中的信息可能已经有所发展或是发生改变。

    什么是事件

    我想你很能听说过事件驱动, 但是事件驱动到底是什么?为什么说浏览器是事件驱动的呢?为什么 NodeJS 也是事件驱动的 ? 两者是一回事么?

    实际上不管是浏览器还是 Nodejs 都是事件驱动的,都有自己的事件模型。在这里,我们只讲解浏览器端的事件模型,如果对 Nodejs 事件模型感兴趣的,请期待我的 Nodejs 部分的讲解。

    事件驱动通俗地来说就是什么都抽象为事件

    • 一次点击是一个事件
    • 键盘按下是一个事件
    • 一个网络请求成功是一个事件
    • 页面加载是一个事件
    • 页面报错是一个事件
    • ...

    浏览器依靠事件来驱动 APP 运行下去,如果没有了事件驱动,那么 APP 会直接从头到尾运行完,然后结束,事件驱动是浏览器的基石。

    本篇文章不讲解事件循环的内容,事件循环部分会在本章的其他章节讲解,敬请期待。

    一个简单的例子

    其实现实中的红绿灯就是一种事件,它告诉我们现在是红灯状态,绿灯状态,还是黄灯状态。 我们需要根据这个事件自己去完成一些操作,比如红灯和黄灯我们需要等待,绿灯我们可以过马路。

    WeChatf64afbca051509ddcf379077490e9a2e.png

    下面我们来看一个最简单的浏览器端的事件:

    html 代码:

    <button>Change color</button> 

    js 代码:

    var btn = document.querySelector('button'); btn.Onclick= function() { console.log('button clicked') } 

    代码很简单,我们在 button 上注册了一个事件,这个事件的 handler 是一个我们定义的匿名函数。当用户点击了这个被注册了事件的 button 的时候,这个我们定义好的匿名函数就会被执行。

    如何绑定事件

    我们有三种方法可以绑定事件,分别是行内绑定直接赋值,用addEventListener

    • 内联

    这个方法非常不推荐

    html 代码:

    <button Onclick="handleClick()">Press me</button> 

    然后在 script 标签内写:

    function handleClick() { console.log('button clicked') } 
    • 直接赋值

    和我上面举的例子一样:

    var btn = document.querySelector('button'); btn.Onclick= function() { console.log('button clicked') } 

    这种方法有两个缺点

    1. 不能添加多个同类型的 handler
    btn.Onclick= functionA; btn.Onclick= functionB; 

    这样只有 functionB 有效,这可以通过addEventListener来解决。

    1. 不能控制在哪个阶段来执行,这个会在后面将事件捕获 /冒泡的时候讲到。这个同样可以通过addEventListener来解决。

    因此 addEventListener 横空出世,这个也是目前推荐的写法。

    • addEventListener

    旧版本的addEventListener第三个参数是 bool,新版版的第三个参数是对象,这样方便之后的扩展,承载更多的功能, 我们来重点介绍一下它。

    addEventListener 可以给 Element,Document,Window,甚至 XMLHttpRequest 等绑定事件,当指定的事件发生的时候,绑定的回调函数就会被以某种机制进行执行,这种机制我们稍后就会讲到。

    语法:

    target.addEventListener(type, listener[, options]); target.addEventListener(type, listener[, useCapture]); target.addEventListener(type, listener[, useCapture, wantsUntrusted ]); // Gecko/Mozilla only 

    type 是你想要绑定的事件类型,常见的有 click, scroll, touch, mouseover 等,旧版本的第三个参数是 bool,表示是否是捕获阶段,默认是 false,即默认为冒泡阶段。新版本是一个对象,其中有 capture (和上面功能一样),passive 和 once。once 用来执行是否只执行一次,passive 如果被指定为 true 表示永远不会执行preventDefault(),这在实现丝滑柔顺的滚动的效果中很重要。更多请参考Improving scrolling performance with passive listeners

    框架中的事件

    实际上,我们现在大多数情况都是用框架来写代码,因此上面的情况其实在现实中是非常少见的,我们更多看到的是框架封装好的事件,比如 React 的合成事件,感兴趣的可以看下这几篇文章。

    虽然我们很少时候会接触到原生的事件,但是了解一下事件对象,事件机制,事件代理等还是很有必要的,因为框架的事件系统至少在这方面还是一致的,这些内容我们接下来就会讲到。

    事件对象

    所有的事件处理函数在被浏览器执行的时候都会带上一个事件对象,举个例子:

    function handleClick(e) { console.log(e); } btn.addEventListener('click', handleClick); 

    这个 e 就是事件对象,即event object。 这个对象有一些很有用的属性和方法,下面举几个常用的属性和方法。

    • 属性

      1. target
      2. x, y 等位置信息
      3. timeStamp
      4. eventPhase

      ...

    • 方法

      1. preventDefault 用于阻止浏览器的默认行为,比如 a 标签会默认进行跳转,form 会默认校验并发送请求到 action 指定的地址等
      2. stopPropagation 用于阻止事件的继续冒泡行为,后面讲事件传播的时候会提到。

      ...

    事件传播

    前面讲到了事件默认是绑定到冒泡阶段的,如果你显式令 useCapture 为 true,则会绑定到捕获阶段。

    事件捕获很有意思,以至于我会经常出事件的题目加上一点事件传播的机制,让候选人进行回答,这很能体现一个人的水平。了解事件的传播机制,对于一些特定问题有着非常大的作用。

    一个 Element 上绑定的事件触发了,那么其实会经过三个阶段。

    • 第一个阶段 - 捕获阶段

    从最外层即 HTML 标签开始,检查当前元素有没有绑定对应捕获阶段事件,如果有则执行,没有则继续往里面传播,这个过程递归执行直到触达触发这个事件的元素为止。

    伪代码:

    function capture(e, currentElement) { if (currentElement.listners[e.type] !== void 0) { currentElement.listners[e.type].forEach(fn => fn(e)) } // pass down if (currentElement !== e.target) { // getActiveChild 用于获取当前事件传播链路上的子节点 capture(e, getActiveChild(currentElement, e)) } else { bubble(e, currentElement) } } // 这个 Event 对象由引擎创建 capture(new Event(), document.querySelector('html')) 
    • 第二个阶段 - 目标阶段

    上面已经提到了,这里省略了。

    • 第三个阶段 - 冒泡阶段

    触发这个事件的元素开始,检查当前元素有没有绑定对应冒泡阶段事件,如果有则执行,没有则继续往里面传播,这个过程递归执行直到触达 HTML 为止。

    伪代码:

    function bubble(e, currentElement) { if (currentElement.listners[e.type] !== void 0) { currentElement.listners[e.type].forEach(fn => fn(e)) } // returning if (currentElement !== document.querySelector('html')) { bubble(e, currentElement.parent) } } 

    上述的过程用图来表示为:

    如果你不希望事件继续冒泡,可以用之前我提到的stopPropagation

    伪代码:

    function bubble(e, currentElement) { let stopped = false; function cb() { stopped = true; } if (currentElement.listners[e.type] !== void 0) { currentElement.listners[e.type].forEach(fn => { fn({ ...e, stopPropagation: cb }); if (stopped) return; }) } // returning if (currentElement !== document.querySelector('html')) { bubble(e, currentElement.parent) } } 

    事件代理

    利用上面提到的事件冒泡机制,我们可以选择做一些有趣的东西。 举个例子:

    我们有一个如下的列表,我们想在点击对应列表项的时候,输出是点击了哪个元素。

    HTML 代码:

    <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> 

    JS 代码:

    document.querySelector('ul').addEventListener('click', e => console.log(e.target.innerHTML)) 

    在线地址

    上面说了addEventListener会默认绑定到冒泡阶段,因此事件会从目标阶段开始,向外层冒泡,到我们绑定了事件的 ul 上,ul 中通过事件对象的 target 属性就能获取到是哪一个元素触发的。

    “事件会从目标阶段开始”,并不是说事件没有捕获阶段,而是我们没有绑定捕获阶段,我描述给省略了。

    我们只给外层的 ul 绑定了事件处理函数,但是可以看到 li 点击的时候,实际上会打印出对应 li 的内容( 1,2,3 或者 4 )。 我们无须给每一个 li 绑定事件处理函数,不仅从代码量还是性能上都有一定程度的提升。

    这个有趣的东西,我们给了它一个好听的名字“事件代理”。在实际业务中我们会经常使用到这个技巧,这同时也是面试的高频考点。

    总结

    事件其实不是浏览器特有的,和 JS 语言也没有什么关系,这也是我为什么没有将其划分到 JS 部分的原因。很多地方都有事件系统,但是各种事件模型又不太一致。

    我们今天讲的是浏览器的事件模型,浏览器基于事件驱动,将很多东西都抽象为事件,比如用户交互,网络请求,页面加载,报错等,可以说事件是浏览器正常运行的基石。

    我们在使用的框架都对事件进行了不同程度的封装和处理,除了了解原生的事件和原理,有时候了解一下框架本身对事件的处理也是很有必要的。

    当发生一个事件的时候,浏览器会初始化一个事件对象,然后将这个事件对象按照一定的逻辑进行传播,这个逻辑就是事件传播机制。 我们提到了事件传播其实分为三个阶段,按照时间先后顺序分为捕获阶段,目标阶段和冒泡阶段。开发者可以选择监听不同的阶段,从而达到自己想要的效果。

    事件对象有很多属性和方法,允许你在事件处理函数中进行读取和操作,比如读取点击的坐标信息,阻止冒泡等。

    最后我们通过一个例子,说明了如何利用冒泡机制来实现事件代理

    本文只是一个浏览器事件机制的科普文,并没有也不会涉及到很多细节。希望这篇文章能让你对浏览器时间有更深的理解,如果你对 nodejs 时间模型感兴趣,请期待我的 nodejs 事件模型。 事件循环和事件循环也有千丝万缕的联系,如果有时间,我会出一篇关于时间循环的文章。

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1066 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 23:45 PVG 07:45 LAX 15:45 JFK 18:45
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86