目录 §


1. 点击事件 §

有三个div,.爷爷>.爸爸>.儿子

  • 给三个div分别添加事件监听,fnYe/fnBa/fnEr

    1
    2
    3
    4
    5
    6
    7
    
    <div class="爷爷">
    <div class="爸爸">
        <div class="儿子">
        文字
        </div>
    </div>
    </div>
  • 点击文字,同时算点击了儿子、爸爸、爷爷

  • 点击文字,事件的调用顺序

  • W3C制定浏览器标准需同时支持两种调用顺序

  • 首先按爷爷->爸爸->儿子顺序查找函数监听,从外向内,找监听函数,叫事件捕获

  • 然后按儿子->爸爸->爷爷顺序查找函数监听,从内向外,找监听函数,叫事件冒泡

  • 有监听函数就调用,并提供事件信息,没有就跳过

  • 可选择间监听的函数放在捕获阶段还是冒泡阶段调用


2.addEventListener §

2.1 事件绑定API

  • element.addEventListener('click', fn, boolean) 冒泡

如果bool不传或为falsy

  • 就让fn走冒泡,即当浏览器在冒泡阶段发现节点有fn监听函数,就会调用fn,并提供事件信息
  • 即监听函数默认走冒泡

如果bool为true

  • 就让fn走捕获, 即当浏览器在捕获阶段发现节点有fn监听函数,就会调用fn,并提供信息

你可以选择吧fn放在哪边

  • 如果bool为true,fn就放在左边,右边空跑
  • 如果bool为空或falsy,fn就放在右边,左边空跑

代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<body>
  <div class="lv1 x">
    <div class="lv2 x">
      <div class="lv3 x">
        <div class="lv4 x">
          <div class="lv5 x">
            <div class="lv6 x">
              <div class="lv7 x"></div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script src="main.js"></script>
</body>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
* {
  box-sizing: border-box;
}


div[class^="lv"]:not([class=".lv7"]) {
  border: 1px solid;
  border-radius: 50%;
  display: inline-flex;
  padding: 10px;
}

.lv1 {
  background-color: purple;
}

.lv2 {
  background-color: blue;
}

.lv3 {
  background-color: cyan;
}

.lv4 {
  background-color: green;
}

.lv5 {
  background-color: yellow;
}

.lv6 {
  background-color: orange;
}

.lv7 {
  width: 50px;
  height: 50px;
  border: 1px solid;
  border-radius: 50%;
  background-color: red;
  display: inline-flex;
}

.x {
  background-color: transparent;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const $ = document.querySelector.bind(document)
const [lv1, lv2, lv3, lv4, lv5, lv6, lv7] = [$('.lv1'), $('.lv2'), $('.lv3'), $('.lv4'), $('.lv5'), $('.lv6'), $('.lv7')]
let n = 1
console.log(lv1)
lv1.addEventListener('click', (e) => {
  console.log(e.currentTarget)
  setTimeout(() => {
    console.log(e.currentTarget)
    e.currentTarget.classList.remove('x');
  }, n * 1000)
})

事件结束后,e 还在?

  • 答案并不是「在」,也不是「不在」
  • e 只是没了 currentTarget: null ,其他属性都还「健在」
  • 动作结束,事件结束,e 会被浏览器悄悄改变
  • 不推荐在事件结束后访问 e
  • 把属性复制到自己的变量里再使用,比如const tempEvent = e.currentTarget 就是把 currentTarget 的引用复制到 tempEvent

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    const $ = document.querySelector.bind(document)
    const [lv1, lv2, lv3, lv4, lv5, lv6, lv7] = [$('.lv1'), $('.lv2'), $('.lv3'), $('.lv4'), $('.lv5'), $('.lv6'), $('.lv7')]
    let n = 1
    console.log(lv1)
    lv1.addEventListener('click', (e) => {
    const tempEvent = e.currentTarget
    setTimeout(() => {
    tempEvent.classList.remove('x');
    }, n * 1000)
    n += 1
    })
    

循环监听事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const $ = document.querySelector.bind(document)
let lvn = [lv1, lv2, lv3, lv4, lv5, lv6, lv7] = [$('.lv1'), $('.lv2'), $('.lv3'), $('.lv4'), $('.lv5'), $('.lv6'), $('.lv7')]

const removeX = (e) => {
  const tempEvent = e.currentTarget
  setTimeout(() => {
    tempEvent.classList.remove('x');
  }, n * 100)
  n += 1
}

const addX = (e) => {
  const tempEvent = e.currentTarget
  setTimeout(() => {
    tempEvent.classList.add('x');
  }, n * 150)
  n += 1
}

let n = 1
for (let i = 0; i < lvn.length; i++) {
  const lvnEle = lvn[i];
  lvnEle.addEventListener('click', removeX, true)
  lvnEle.addEventListener('click', addX, false)
  // console.log(n, i)
}

事件捕获和冒泡效果链接



6.2 小结二

targetcurrentTarget

  • 一个是用户点击的,一个是开发者监听的

取消冒泡

  • e.stopPropagation()

事件的特性

  • Bubbles
  • Cancelable
  • scrollCancelablefalse

如何禁用滚动

  • 取消特定元素的wheeltouchstart的默认动作

7. 自定义事件 §

浏览器自带事件

自定义事件

  • const userEvent = new CustomEvent("newName", {detail:{}, bubbles, cancelable})
  • userElement.dispatchEvent(event)
  • userElement.addEventListener(event)
  • 示例代码


8. 事件委托(事件代理) §

  • 让父/祖元素监听原本在子元素上的监听事件;将子元素监听事件委托父/祖元素监听

场景一

  • 要给100个按钮添加点击事件,不需要添加100个监听器
  • 监听这100元素的祖先元素,等冒泡的时候判断是否传递到当前元素
  • 即判断用户操作的target是否是这100个按钮中的一个

场景二

  • 要监听目前不存在的元素的点击事件
  • 监听祖先元素,等点击的时候看是否是想要监听的元素

事件委托的优点

  • 节省监听数(内存)
  • 可以监听动态元素

示例代码链接



9. 封装事件委托 §

要求

  • 写出这样一个函数on('click', '#testDiv', 'li', fn)
  • 当用户点击#testDiv里的li元素时,调用fn函数
  • 事件委托

方案一

  • 判断target是否匹配li

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    /*
    <div id="div1">
    <span>span1</span>
    </div>
    <script src="EventDelegation.js"></script>
    */
    
    /* // 未封装
    div1.addEventListener('click', (e) => {
    const tempButton = e.target // by user
    if (tempButton.tagName.toLowerCase() === "button") {
    console.log('button被点击了')
    console.log('button内容是' + tempButton.textContent)
    console.log('button id是' + tempButton.dataset.id)
    }
    });
    */
    
    for (let i = 0; i < 10; i += 1) {
    setTimeout(() => {
    const button = document.createElement('button')
    button.textContent = 'click ' + i
    button.setAttribute("data-id", i)
    div1.appendChild(button)
    }, i * 1000)
    }
    
    on('click', '#div1', 'button', (e) => {
    const tempElement = e.target // by user
    console.log('button被点击了')
    console.log('button内容是' + tempElement.textContent)
    console.log('button id是' + tempElement.dataset.id)
    })
    
    function on(eventType, element, selector, fn) {
    if (!(element instanceof Element)) {
    element = document.querySelector(element)
    }
    element.addEventListener(eventType, (e) => {
    const tempElement = e.target // by user
    if (tempElement.matches(selector) === true) {
      fn(e)
    }
    })
    }
    
  • 这个答案有缺陷

  • 如果将操作的元素内部再放一层子元素,比如包裹 span 元素

  • 操作的元素变为span 元素

  • 点击的时候,tempElementspan,而selectorbutton,不匹配,就不执行fn

方案二

  • 递归判断target/target的父元素/target的祖元素是否匹配li

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    function on(eventType, element, selector, fn) {
    if (!(element instanceof Element)) {
    element = document.querySelector(element)
    }
    element.addEventListener(eventType, e => {
    let el = e.target
    while (!el.matches(selector)) {
      // 范围是 element 之内的元素
      if (element === el) {
        el = null
        break
      }
      el = el.parentNode
    }
    el && fn.call(el, e, el)
    })
    return element
    }
    

jQuery的实现

  • $('#xxx').on('click', 'li', fn)

JS 支持事件吗

  • DOM 事件不属于JS的功能,而是浏览器提供的 DOM 功能
  • JS 只是调用了 DOM 提供的addEventListener这个API
  • 所以 JS 不支持,提供事件机制的时浏览器

追问:手写JS事件系统


面试题:请简述 DOM 事件模型或 DOM 事件机制(你可以写一篇博客来回答这个问题)

提示:请描述什么是捕获什么是冒泡,并说说是先捕获还是先冒泡,再搜搜看网上的文章怎么说

  • 浏览器的事件模型,就是通过监听函数(listener) 事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数
  • DOM 事件机制 事件发生会分成三个阶段 捕获 目标 冒泡阶段,在各层元素之间传播,使得同一个事件会在多个节点上触发
  • 事件捕获是从window(-> Document -> html -> …)开始由外向内查找监听函数
  • 事件冒泡是从当前操作的节点(目标节点)开始由内向外直至 window 查找监听函数
  • 按照先捕获,后冒泡的顺序执行

捕获:当用户点击按钮,浏览器会从 window 从上向下遍历至用户点击的按钮,逐个触发事件处理函数。

冒泡:浏览器从用户点击的按钮从下往上遍历至 window,逐个触发事件处理函数。

W3C 事件模型/事件机制:对每个事件先捕获再冒泡

面试题:请简述事件委托(可写博客回答)

提示:请想办法把「监听祖先元素」说得高大上一点

  • 事件委托就是把一个元素响应事件(click、keydown……)的函数委托到另一个元素
  • 一般是把元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素
  • 当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件
  • 然后在外层元素上去执行函数,即便一开始当前元素不存在于页面中,也能实现事件委托
  • 以达到减少内存消耗和动态绑定事件的目的

事件委托就是把事件监听放在祖先元素(如父元素、爷爷元素)上。

好处是:1 节约监听数量 2 可以监听动态生成的元素。

代码示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 错误版(但是可能能过)
 ul.addEventListener('click', function(e){
     if(e.target.tagName.toLowerCase() === 'li'){
         fn() // 执行某个函数
     }
 })
// bug 在于,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对。
// 高级版
 function delegate(element, eventType, selector, fn) {
     element.addEventListener(eventType, e => {
       let el = e.target
       while (!el.matches(selector)) {
         if (element === el) {
           el = null
           break
         }
         el = el.parentNode
       }
       el && fn.call(el, e, el)
     })
     return element
   }
// 思路是点击 span 后,递归遍历 span 的祖先元素看其中有没有 ul 里面的 li。

·未完待续·

参考文章

相关文章