Skip to content

性能

在 JS 中,添加到页面上的事件处理程序数量会直接关系到页面的整体运行性能。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。当然,还是有方法优化这种情况的。

事件委托

事件委托:利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。用于处理事件处理程序过多的情况。

以下面代码为例:

html
<ul id="links">
  <!-- 包含三个被点击后会执行操作的列表项 -->
  <li id="go">Go</li>
  <li id="do">Do</li>
  <li id="hi">Hi</li>
</ul>

按照以往做法,需要为每个列表项添加事件处理程序。

js
const item1 = document.getElementById('go')
const item2 = document.getElementById('do')
const item3 = document.getElementById('hi')

item1.addEventListener('click', () => {
  console.log('go')
})

item2.addEventListener('click', () => {
  console.log('do')
})

item3.addEventListener('click', () => {
  console.log('hi')
})

如果在一个复杂的 Web 应用中,对所有可点击的元素都采用这种方式,那么就会有数不清的代码用于添加事件处理程序。这个时候我们就可以使用事件委托,只需在 DOM 树中尽量高的一层节点添加一个事件处理程序。

js
const list = document.getElementById('links')

list.addEventListener('click', (event) => {
  switch (event.target.id) {
    case 'go':
      console.log('go')
      break
    case 'do':
      console.log('do')
      break
    case 'hi':
      console.log('hi')
      break
  }
})

与前面代码相比,这段代码占用的内存更少,性能消耗更少。

如果可以的话,可以考虑为 document 添加一个事件处理程序,用以处理页面上某种特定类型的事件。这样做有以下优点。

  • document 很快就可以访问,而且可以在页面生命周期的任何时间点上为它添加事件处理程序(无需等待 DOMContentLoadedload 事件)。只要可点击的元素渲染完成,就可以立即具备相应功能;
  • 在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的 DOM 引用更少,所花时间也更少;
  • 整个页面占用内存更少,能够提升整体性能。

最适合采用事件委托的事件:clickmousedownmouseupkeydownkeyupkeypress。虽然 mouseovermouseout 也冒泡,但要适当处理它们并不容易,而且经常需要计算元素位置。

移除事件处理程序

除了事件委托外还有一种方案,那就是移除不需要的事件处理程序。

有两种情况会产生不需要的事件处理程序:

  • 从文档中移除带有事件处理程序的元素时。这通常是通过 DOM 操作,例如使用 removeChild()replaceChild(),当然更多地是发生在使用 innerHTML 替换页面某一部分的时候。如果带有事件处理程序的元素被 innerHTML 删除了,那么这些事件处理程序极有可能无法被垃圾回收。

示例:

html
<div id="my-div">
  <input id="my-btn" type="button" value="Click" />
</div>

<script>
  const btn = document.getElementById('my-btn')
  btn.addEventListener('click', function () {
    /**
     * 事件处理程序仍然与按钮保持引用关系。有的浏览器(尤其是 IE)
     * 有可能将按钮和事件处理程序的引用保存在内存中,
     * 导致按钮和事件处理程序无法被垃圾回收。
     */
    document.getElementById('my-div').innerHTML = 'Button clicked'
  })
</script>

如果你要移除某个元素,那么最好也手动移除添加在其上的所有事件处理程序。

html
<div id="my-div">
  <input id="my-btn" type="button" value="Click" />
</div>

<script>
  const btn = document.getElementById('my-btn')

  function ShowBtnClicked() {
    btn.removeEventListener('click', ShowBtnClicked)
    document.getElementById('my-div').innerHTML = 'Button clicked'
  }

  btn.addEventListener('click', ShowBtnClicked)
</script>

INFO

在事件处理程序中删除目标元素会阻止事件冒泡。因为目标元素在文档中是事件冒泡的前提。

TIP

这个问题也可以使用事件委托解决。如果你已经知道目标元素会被 innerHTML 替换掉,那么可以把事件处理程序添加到其祖先元素上。这样可以避免内存泄漏。

  • 卸载页面时。IE 8 及更早版本是这种情况问题最多的浏览器,尽管其它浏览器或多或少也有类似的问题。如果在页面卸载之前没有清理干净事件处理程序,那么它们就会滞留在内存中。每次加载完页面再卸载页面时(在两个页面之间来回切换或者点击刷新按钮),内存中滞留的对象数目会增加,因为事件处理程序占用的内存没有被释放。

一般来说,最好的做法是在页面卸载之前,先通过 onunload 事件移除所有事件处理程序。在此,事件委托再次表现出它的优势 —— 需要跟踪的事件处理程序越少,移除它们就越容易。

WARNING

需要注意,使用 onunload 事件会导致页面不会被缓存在 bfcache 中。

References