likes
comments
collection
share

JS - Event Flow(事件流)

作者站长头像
站长
· 阅读数 57

一、基础知识点

事件流概念

当有人通过某种方式(click、mouse、scroll、touch、focus、blur)操作了页面时所引发的处理该次操作的全过程

事件流全过程

捕获阶段 ->执行阶段->冒泡阶段

  • 捕获阶段:从根节点一直到触发事件的最底层DOM节点(target)的逐层向下遍历过程;
  • 执行阶段:根据红宝书第四版,通常执行阶段会被理解为是冒泡阶段的一部分,具体指在冒泡过程中会逐个执行各个元素上的事件处理函数的过程。在后续讲解Event对象的章节中会提到Event对象的eventPhase只读属性可以表示当前执行是否为执行阶段(eventPhase === 2);
  • 冒泡阶段:和捕获阶段刚好相反,从触发事件的最底层DOM节点(target)到document(IE5.5及以前的版本会跳过html直达document,现在的浏览器一般是window)的遍历过程;
  • 下面用html举个小🌰:
<html>
    <body>
        <div onclick="console.log('div clicked')">
            <button  onclick="console.log('button clicked')" />
        </div>
    </body>
</html>

如上有树形结构 (window ->) document -> html -> body -> div -> button
现点击一下button按钮,会引发如下事件流
1. 捕获阶段:(window ->) document -> html -> body -> div -> button;
2. 冒泡阶段:button -> div -> body -> html -> document (-> window);
3. 执行阶段:在冒泡阶段先发现button身上有个onclick事件处理函数就直接执
   行,然后冒泡到div发现它身上也有个onclick事件处理函数,于是也执行;
执行结果: 
// 'button clicked'
// 'div clicked'

事件流前世今生

Long long time ago,浏览器巨头 网景(Netscape)IE 分别提出了自己对于事件的处理方法,网景(Netscape) 提出捕获流概念,即在向下捕获的时候搞定事件处理,而 IE 提出冒泡流概念,即在向上冒泡的时候搞定事件处理,所以老的IE浏览器只认冒泡不认捕获。后来好大哥W3C(World Wide Web Consortium) 出面把捕获和冒泡做了个合体并生成了现在使用的事件流

二、进阶知识点

绑定事件处理函数的几种方式

1. 通过DOM自身属性绑定

  1. DOM自身挂载了很多on${eventName}属性(如onclick),默认为undefined,当复制为某具体FunctionA后就会在执行阶段执行;
  2. 因为实际上就是对DOM挂载的属性的重新赋值,因此该方法只能让一个DOM的每个on${eventName}属性拥有唯一事件处理函数
  3. 通过设置属性值的方式绑定的事件处理函数会在冒泡阶段执行;
  4. 通过设置属性值的方式绑定所拥有的event对象currentTarget会恒为null
  5. 如果绑定的FunctionA是通过Function声明,则函数this会指向绑定的DOM对象;
  6. 如果绑定的FunctionA是通过() => {}声明,则函数this会指向箭头函数声明环境;
    const $body = document.querySelector('body')
    
    // 1. 箭头函数绑定
    $body.onclick = (event) => {
        console.log(event) // Event Object
        console.log(this)  // Window or undefined
    }
    
    // 2. Function函数绑定
    $body.onclick = function(event) {
        console.log(event) // Event Object
        console.log(this)  // DOM Object
        console.log(event.target === this) // true
    }

2. 通过attachEvent(老IE独享方法)绑定

  1. 因为兼容性极差且已被淘汰,故只做简单介绍;
  2. 老IE独享,可想而知该方法只支持捕获阶段,具体入参有二:
    • eventName(String:绑定的事件名,如onclick)
    • callback(Function:绑定的事件处理函数)
  3. attachEvent可以给同一DOM同一事件绑定多个事件处理函数,具体的执行顺序与注册顺序相反,后进先出了属于是;
    const $body = document.getElementsByTagName('body')
    
    $body.attachEvent('onclick', function() {
        console.log('body clicked')
    })

3. 通过addEventListener绑定

  1. 兼容了捕获流冒泡流的现行主流绑定方式,具体入参有三:
    • eventName(String:绑定的事件名,如click)
    • callback(Function:绑定的事件处理函数)
    • useCapture(Boolean:是否在捕获阶段执行,default:false)
  2. addEventListener可以给同一DOM同一事件绑定多个事件处理函数,具体的执行顺序与注册顺序相同(当然第三个参数权重更高,也就是true比false先执行),先进先出了属于是;
    const $body = document.getElementsByTagName('body')
    
    $body.addEventListener('click', function() {
        console.log('click1')
    }, false)
    
    $body.addEventListener('click', function() {
        console.log('click2')
    })
    
    $body.addEventListener('click', function() {
        console.log('click3')
    }, true)
    
    /*** click一下之后的输出结果 ***/
    // click3 
    // click1 
    // click2

解绑事件处理函数的几种方式

1. 解绑DOM自身属性

[DOM].onclick -> [DOM].onclick = undefined

2. 解绑addEventListener

[DOM].addEventListener('click', clickHandler, useCapture) -> [DOM].removeEventListener('click', clickHandler, useCapture)

几种绑定方式的优先级(不带attachEvent)

  • 优先级的确立基于DOM等级实现,目前共划分了DOM0DOM1DOM2DOM3四个等级(可以简单理解为http1.0 、http1.1、http2.0的关系,即优化与新增),每个级别拥有不同水平的DOM事件:
    • DOM0级事件处理: 只有最远古的通过DOM自身属性绑定
    • DOM1级事件处理: 无,因为从DOM0DOM1没做啥子事件处理的事;
    • DOM2级事件处理: 出来了addEventListener,支持click等基操;
    • DOM3级事件处理: 基于DOM2新增了很多新操作,如focus、input、scroll、keydown、mousedown……
  • 事件处理函数的优先级与DOM事件处理等级相反,因此onclick执行优先级大于addEventListener,而又因为addEventListener注册的事件处理函数执行优先级等同于注册顺序,因此最终优先级为:onclick > addEventListener[1] > addEventListener[2]

Event对象

1. Event对象咋来的

  • 无论是通过哪种方式绑定的事件处理函数FunctionA,FunctionA都会在执行之前被塞入第一个入参,这个入参就是Event对象
  • 经过尝试发现,同一事件在同一个事件流中无论是通过onclick还是通过addEventListener('click') 绑定不同的事件处理函数,最后的函数所接收到的Event对象是同一个,尝试代码如下:
    const _html = document.querySelector('body')
    const _body = document.querySelector('body')
    
    // 用于暂存先触发的处理函数的Event对象
    let _eventCache = null
    
    _html.addEventListener('click', e => {
        _eventCache = e
    }, true)
    _body.onclick = e => {
        console.log(e === _eventCache) // true
        _eventCache = e
    }
    _html.addEventListener('click', e => {
        console.log(_eventCache === e) // true
    })
    

2. Event对象的能力

  • Event对象挂载着和事件有关的所有信息,包括相关DOM、事件类型、事件触发器的数值等等,因此不同事件类型的Event对象所挂载的属性会有差异;
  • 虽然存在差异,但是所有对象都会包含如下共有属性和方法(全部是Readonly):
    • type[String] -> 触发的事件类型;
    • bubbles[Boolean] -> 当前事件是否可以冒泡;
    • cancelable[Boolean] -> 是否可以取消事件默认行为;
    • target[DOM] -> 当前触发事件流的最底层DOM
    • currentTarget[DOM] -> 绑定该事件处理函数的DOM元素(绑在window的该值为null);
    • eventPhase[Number( 0 | 1 | 2 | 3 )] -> 事件处理函数被执行时刻所处在的事件流阶段:
      • 0: PointerEvent.NONE - 无事发生阶段
      • 1: PointerEvent.CAPTURING_PHASE - 事件流处于捕获阶段
      • 2: PointerEvent.AT_TARGET - 事件流处于目标阶段(尝试后发现唯有事件流最底层DOM绑定的事件在执行时eventPhase会返回2)
      • 3: PointerEvent.BUBBLING_PHASE - 事件流处于冒泡阶段
    • preventDefault[Function] -> 取消事件默认行为(比如a标签自动跳转),前提是cancelable为true;
    • stopPropagation[Function] -> 阻止事件继续传播(捕获和冒泡),前提是bubbles为true;
    • stopImmediatePropagation[Function & DOM3] -> 拥有stopPropagation功能的同时,还能阻止当前元素接下来还未执行的事件处理函数(🌰:body给click事件按序绑定FunctionA、FunctionB、FunctionC,在FunctionB中执行stopImmediatePropagatiom后,FunctionC将不再执行);
    • trusted || isTrusted[Boolean & DOM3] -> Event对象是否可信,因为用户可以通过new PointerEvent('click')创建Event对象,所以通过该属性区分是否是事件流自动创建,true为事件流创建、false为用户创建;
转载自:https://juejin.cn/post/7169816642065530894
评论
请登录