目录 §

  • 1. 点击事件 §
  • 2. addEventListener §
  • 3. target V.S. currentTarget §
  • 4. 取消冒泡 §
  • 5. 不可取消冒泡的事件 §
  • 6. 阻止滚动 §
  • 7. 自定义事件 §
  • 8. 事件委托(事件代理) §
  • 9. 封装事件委托 §
  • 10. $3 §
  • 11. $3 §

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)
}

事件捕获和冒泡效果链接




2.2 小结

注意点

  • 子节点被点击了,也算点击父节点
  • 先调用父节点的函数还是先调用儿子的函数

捕获与冒泡

  • 捕获先调用父节点的监听函数
  • 冒泡先调用子节点的监听函数

W3C 事件模型

  • 先捕获(先爸爸->儿子),再冒泡(先儿子->爸爸)
  • 冒泡可以被阻止,而捕获不可被阻止
  • 注意 e 对象被传给所有监听函数
  • 事件结束后,e 对象就不存在了

3.target V.S. currentTarget §

区别

  • e.target用户操作的元素
  • e.currentTarget程序员监听的函数
  • thise.currentTarget,不推荐使用,易混淆
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// <div id="target">target</div>
console.log(target.onclick) // null
// 简单赋值 改写 (箭头函数不支持内部 this)
target.onclick = function(e){
    console.log('hi')
    console.log(this)
    console.log(e) // 包含了点击事件的所有信息
}

// target.onclick.call(target, event)
// this <- target
// event <- e
// this 是浏览器在用户点击时,用 .call() 按顺序传进来的

举例

  • div > span{文字},用户点击文字
  • e.target就是span
  • e.currentTarget就是div

特例:同一个节点上的监听事件

  • 背景
    • 只有一个div被监听(不考虑父子同时被监听),同级
    • fn分别在捕获阶段和冒泡阶段监听click事件
    • 用户点击的元素就是开发者监听的
    • e.target === e.currentTarget
  • 代码
    • div.addEventListener('click', f1)
    • div.addEventListener('click',f2, true)
    • 问,f1 先执行还是 f2 先执行
    • 如果把位置调换后,哪个先执行
    • 哪个先监听就哪个先执行

4. 取消冒泡 §

捕获不可取消,但冒泡可以

  • e.stopPropagation()可以中断冒泡,浏览器不再向上传递
  • 一般用于封装某些独立的组件

5. 不可取消冒泡的事件 §

  • MDN 英文版搜 scroll event,看到BubblesCancelable
  • Bubbles是该事件是否会冒泡
  • Cancelable是开发者是否可以取消冒泡

6. 阻止滚动 §

6.1 阻止页面滚动的步骤

scroll事件不可取消冒泡和默认事件

  • 阻止scroll默认事件无效,因先有滚轮滚动才有滚动事件
  • 要阻止滚动,可阻止wheeltouchstart的默认动作
1
2
3
4
5
6
7
x.addEventListener('wheel', (e) => {
  e.preventDefault()
});

x.addEventListener('touchstart', (e) => {
  e.preventDefault()
});
  • 注意需要找准滚动所在的元素,能覆盖整个滚动区域的元素,示例代码
  • 但是滚动条依然开可以用鼠标拖动,可以用CSS让滚动条 width: 0
1
2
3
::-webkit-scrollbar {
  width: 0 !important;
}

CSS实现隐藏滚动条

  • 滚动条是在 Document上的
  • 使用overflow:hidden可以直接取消滚动条
  • 但此时 JS 依然可以修改scrollTop

在有iframe的场景下可能失效



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。


参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名