浏览器事件详解

浏览器事件的过程

浏览器事件模型中的过程主要分为三个阶段:捕获阶段、目标阶段、冒泡阶段。具体可以看图:

第三个参数

注意addEventListener的第三个参数, 如果为true,就是代表在捕获阶段执行。如果为false,就是在冒泡阶段进行空口这么说可能不好理解,咱们来看一下代码。

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
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div id="parent" class="flex-center">
      parent
      <p id="child" class="flex-center">
        child
        <span id="son" class="flex-center">
          son
          <a href="https://www.baidu.com" id="a-baidu">click me!</a>
        </span>
      </p>
    </div>
  </body>
  <script type="text/javascript" src="index.js"></script>
  <style>
    #parent {
      background-color: bisque;
      width: 700px;
      height: 700px;
    }
    #child {
      background-color: chocolate;
      width: 500px;
      height: 500px;
    }
    #son {
      background-color: crimson;
      width: 300px;
      height: 300px;
    }
    .flex-center {
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 20px;
    }
  </style>
</html>
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const parent = document.getElementById('parent')
const child = document.getElementById('child')
const son = document.getElementById('son')

window.addEventListener(
  'click',
  function (e) {
    // e.target.nodeName 指当前点击的元素, e.currentTarget.nodeName绑定监听事件的元素
    console.log('window 捕获', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

parent.addEventListener(
  'click',
  function (e) {
    console.log('parent 捕获', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

child.addEventListener(
  'click',
  function (e) {
    console.log('child 捕获', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

son.addEventListener(
  'click',
  function (e) {
    console.log('son 捕获', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

window.addEventListener(
  'click',
  function (e) {
    console.log('window 冒泡', e.target.nodeName, e.currentTarget.nodeName)
  },
  false
)

parent.addEventListener(
  'click',
  function (e) {
    console.log('parent 冒泡', e.target.nodeName, e.currentTarget.nodeName)
  },
  false
)

child.addEventListener(
  'click',
  function (e) {
    console.log('child 冒泡', e.target.nodeName, e.currentTarget.nodeName)
  },
  false
)

son.addEventListener(
  'click',
  function (e) {
    console.log('son 冒泡', e.target.nodeName, e.currentTarget.nodeName)
  },
  false
)
1
2
3
4
5
6
7
8
9
// 输出   我们可以看到,事件流是从window到son捕获,再从son到window冒泡的一个过程
// window 捕获 SPAN undefined
// parent 捕获 SPAN DIV
// child 捕获 SPAN P
// son 捕获 SPAN SPAN
// son 冒泡 SPAN SPAN
// child 冒泡 SPAN P
// parent 冒泡 SPAN DIV
// window 冒泡 SPAN undefined

阻止事件的传播

e.stopPropagation

e.stopPropagation大家经常听到的可能是阻止冒泡,实际上这个方法不只能阻止冒泡,还能阻止捕获阶段的传播。我们在parent中加入stopPropagation,看下代码:

1
2
3
4
5
6
7
8
parent.addEventListener(
  'click',
  function (e) {
    e.stopPropagation()
    console.log('parent 捕获', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)
1
2
3
// 输出结果
// window 捕获 SPAN undefined
// parent 捕获 SPAN DIV

我们可以看到,stopPropagation阻止了捕获事件的传播,并且也阻止了冒泡事件的传播。但是大家思考下,要是在parent上再次绑定一个捕获事件,stopPropagation能不能阻止呢?我们改写下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
parent.addEventListener(
  'click',
  function (e) {
    e.stopPropagation()
    console.log('parent 捕获1', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

parent.addEventListener(
  'click',
  function (e) {
    console.log('parent 捕获2', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

// 输出
// window 捕获 SPAN undefined
// parent 捕获1 SPAN DIV
// parent 捕获2 SPAN DIV

我们可以看到,parent上绑定的另一个捕获事件并没有被阻止掉,那么如果有这样的需求该怎么办呢?那就要用到stopImmediatePropagation

e.stopImmediatePropagation

如果有多个相同类型事件的事件监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了stopImmediatePropagation 方法,则当前元素剩下的监听函数将不会被执行。

我们把代码改写下:

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
parent.addEventListener(
  'click',
  function (e) {
    console.log('parent 捕获0', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

parent.addEventListener(
  'click',
  function (e) {
    e.stopImmediatePropagation()
    console.log('parent 捕获1', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

parent.addEventListener(
  'click',
  function (e) {
    console.log('parent 捕获2', e.target.nodeName, e.currentTarget.nodeName)
  },
  true
)

// 输出结果
// window 捕获 SPAN undefined
// parent 捕获0 SPAN DIV
// parent 捕获1 SPAN DIV

我们可以看到,parent同级的捕获2成功被阻止了,但是捕获0没有被阻止掉,原因是事件的执行是按照顺序执行的,从0->1->2

阻止默认行为 e.preventDefault

preventDefault可以阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其他页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,因为有的时候我们并不希望发生这些事情,所以需要阻止默认行为。我们给html中的a标签加上事件,并阻止默认行为,同时删除parent的阻止事件传播

1
2
3
4
5
const baidu = document.getElementById('a-baidu')

baidu.addEventListener('click', function (e) {
  e.preventDefault()
})

我们可以看到,点击a标签之后没有发生跳转,a标签的默认事件成功被阻止了。

兼容性

attachEvent——兼容:IE7、IE8; 不支持第三个参数来控制在哪个阶段发生,默认是绑定在冒泡阶段 addEventListener——兼容:firefox、chrome、IE、safari、opera

事件委托

我们来手打一个事件委托,给出一个ul+li,输出点击的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
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <ul id="ul">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
    </ul>
  </body>
  <style>
    #ul {
      background-color: gray;
      width: 700px;
      position: relative;
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    li {
      margin-bottom: 20px;
      width: 80%;
      height: 100px;
      display: flex;
      justify-content: center;
      align-items: center;
      background-color: lightgoldenrodyellow;
      font-size: 20px;
      font-weight: bold;
    }
  </style>
</html>

先来一个在li上绑定事件:

1
2
3
4
5
6
const liList = document.querySelectorAll('li')
for (let i = 0; i < liList.length; i++) {
  liList[i].addEventListener('click', (e) => {
    console.log(e.target.innerHTML, i)
  })
}

再来一个在ul上绑定事件:

1
2
3
4
5
6
7
8
9
10
const ul = document.getElementById('ul')
ul.addEventListener('click', (e) => {
  const { target } = e
  if (e.target.tagName.toLowerCase() === 'li') {
    const liList = document.querySelectorAll('li')
    const index = [...liList].indexOf(target)
    // const index = Array.prototype.indexOf.call(liList, target)
    console.log(target.innerHTML, index)
  }
})

封装⼀个多浏览器兼容的绑定事件函数

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
class BomEvent {
  constructor(element) {
    this.element = element
  }

  addEvent(type, handler) {
    if (this.element.addEventListener) {
      // 存在addEventListener
      this.element.addEventListener(type, handler, false)
    } else if (this.element.attachEvent) {
      this.element.attachEvent('on' + type, function () {
        handler.call(this.element)
      })
    } else {
      this.element['on' + type] = handler
    }
  }

  removeEvent(type, handler) {
    if (this.element.removeEventListener) {
      this.element.removeEventListener(type, handler, false)
    } else if (this.element.detachEvent) {
      this.element.detachEvent('on' + type, handler)
    } else {
      this.element['on' + type] = null
    }
  }
}

// 阻止事件 (主要是事件冒泡,因为IE不支持事件捕获)
function stopPropagation(event) {
  if (event.stopPropagation) {
    event.stopPropagation() // 标准w3c
  } else {
    event.cancelBubble = true // IE
  }
}

// 取消事件的默认行为
function preventDefault(event) {
  if (event.preventDefault) {
    event.preventDefault() // 标准w3c
  } else {
    event.returnValue = false // IE
  }
}