小程序模板網(wǎng)

這次一定徹底弄懂DOM事件機制

發(fā)布時間:2020-05-21 09:37 所屬欄目:小程序開發(fā)教程

前言

在網(wǎng)頁端、移動端H5、小程序等各個終端環(huán)境的前端開發(fā)中隨處可見事件的運用,可見事件機制的是前端這一塊的重中之重。經(jīng)過我研讀了大量博客文章以及開源框架源碼后,這次算是對DOM事件機制有了更新更全面的的認識。

下文主要通過一個例子帶你 分析DOM事件的傳遞、事件處理 ,然后在此基礎上再 深入總結(jié)對事件監(jiān)聽的優(yōu)化方案 ,最終總結(jié)下DOM事件機制 在開源框架中的實戰(zhàn)應用 。

一、從實例看事件傳遞

以下面這個普通的html文件為例

<!DOCTYPE html>
<html lang="en" onclick="handleClickHtml()">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body onclick="handleBodyClick()">
  <div id="div1" onclick="handleClick2()">
    <button id="button1" onclick="handleClick1()">handleClick1</button>
  </div>
  <script>
    function handleClick1(e) {
      console.log('click1')
    }
    function handleClick2(e) {
      console.log('click2')
    }
    function handleBodyClick(e) {
      console.log('body clicked')
    }
    function handleClickHtml(e) {
      console.log('html clicked')
    }
    document.addEventListener('click', e => {
      console.log('document clicked')
    })
  </script>
</body>
</html>
復制代碼

例子中給 document、html、body、div、button 都添加了點擊事件,其執(zhí)行結(jié)果如下:

執(zhí)行結(jié)果

事件傳遞

事件產(chǎn)生后,從window對象自上而下向目標節(jié)點傳遞,抵達目標節(jié)點后會沿著相反方向傳遞

DOM事件傳遞

事件傳遞路徑如何確定?

由鼠標、鍵盤、觸摸屏、窗口縮放、圖片加載、節(jié)點焦點變化等觸發(fā)事件,每個觸發(fā)的事件都有一個target——事件目標,根據(jù)target往html根節(jié)點遍歷就可以確定節(jié)點嵌套層級關(guān)系,從而確定此事件的傳遞路徑。

事件傳遞路徑由瀏覽器、webview來確定

事件傳遞為何要來回走兩遍?

一個事件來回走兩遍是便于事件響應時機的控制(下文的事件冒泡與事件捕獲的應用中有體現(xiàn)),同時也存在事件冒泡的瀏覽器歷史原因(與IE、Netscape有關(guān),不贅述)。

事件響應

事件在從自上而下的傳遞過程中會 判斷當前node節(jié)點是否綁定對應事件的監(jiān)聽器 ,若有則執(zhí)行響應監(jiān)聽器的事件處理程序(也就是綁定的事件要執(zhí)行的邏輯),這里的執(zhí)行就是所謂的事件響應。

事件的響應過于與事件的類型是捕獲事件、冒泡事件有關(guān),同時是否組件冒泡還會影響該事件的后續(xù)傳遞過程,具體執(zhí)行流程總結(jié)成如下圖所示:

事件響應邏輯

事件捕獲與冒泡

根據(jù)事件傳遞中的兩種傳遞路徑分別分為: 由上至下——事件捕獲階段,由下至上——事件冒泡階段 ,指的是一個事件由window(上圖并未標出window)到target之間的傳遞的過程。

可以通過組織事件冒泡讓事件不再繼續(xù)完成由下至上的過程,這會讓target的父節(jié)點綁定冒泡階段的事件監(jiān)聽器不響應事件。

可以通過指定事件在捕獲階段執(zhí)行。

:chestnut:冒泡的應用

看下面這個例子,可以通過 stopPropagation 來阻止事件繼續(xù)往上冒泡, window、document、html 上添加的點擊事件均不會生效

<!-- 省略了部分代碼 -->
<div id="div1">
  <button id="button1">button</button>
</div>
<script>
  var div1 = document.getElementById('div1')
  var button1 = document.getElementById('button1')
  div1.addEventListener('click', (e) => {
    console.log(e.currentTarget)
  }, false)
  button1.addEventListener('click', (e) => {
    console.log(e.currentTarget)
    e.stopPropagation() // 關(guān)鍵代碼:組織了click事件繼續(xù)往上冒泡
  }, false)
復制代碼

// 以下是監(jiān)聽html、document的點擊事件 function handleClickHtml(e) { console.log('html clicked') } document.addEventListener('click', e => { console.log('document clicked') }) </script> 復制代碼

點擊button的打印結(jié)果如下:

執(zhí)行結(jié)果

addEventListener需要用attachEvent進行兼容性處理,第三個參數(shù)默認值為false表示在事件冒泡階段響應事件

:chestnut:捕獲的應用

還是上面這個例子,只更改div1部分的事件監(jiān)聽,的第三個參數(shù)為true來開啟事件捕獲

<!-- 省略了部分代碼 -->
<div id="div1">
  <button id="button1">button</button>
</div>
<script>
  var div1 = document.getElementById('div1')
  var button1 = document.getElementById('button1')
  div1.addEventListener('click', (e) => {
    console.log(e.currentTarget)
  }, true) // true:表示div1在事件的捕獲階段響應事件
  button1.addEventListener('click', (e) => {
    console.log(e.currentTarget)
    e.stopPropagation() // 組織事件冒泡
  }, false)
復制代碼

// 以下是監(jiān)聽html、document的點擊事件 function handleClickHtml(e) { console.log('html clicked') } document.addEventListener('click', e => { console.log('document clicked') }) </script> 復制代碼

點擊button的打印結(jié)果如下:

執(zhí)行結(jié)果

依次執(zhí)行了div1和button1綁定的點擊事件,注意這里兩者的 執(zhí)行順序 很關(guān)鍵。事件在捕獲階段遇到div1節(jié)點時執(zhí)行打印,然后在button處(處于目標階段)執(zhí)行打印,然后事件被阻止冒泡不再往上傳遞。

二、事件性能優(yōu)化

大批量事件監(jiān)聽性能問題

考慮以下場景:

有個一個長消息列表實時接受新的消息,滾動到底部時加載更多消息,點擊消息可進入回話窗口頁面,消息左滑消息此條消息

問題:

如何給此消息列表添加左滑事件?

抽象來看這個問題就是如何高效的給大批量(甚至是無限量)節(jié)點添加事件,這里暫時不考慮vue、react等框架

性能問題原因

每注冊一個事件監(jiān)聽監(jiān)聽都需要使用一定內(nèi)存,同時在dom節(jié)點多了之后事件經(jīng)過的捕獲、冒泡階段要處理事件傳遞也就慢了

事件委托

父節(jié)點嵌套的子節(jié)點的觸發(fā)的事件會通過事件冒泡到達父節(jié)點,事件處理程序不直接綁定到子節(jié)點,統(tǒng)一由公共父節(jié)點進行事件監(jiān)聽。

e.target表示觸發(fā)事件的元素,通過e.target可以判斷具體響應那個元素的事件 e.currentTarget表示事件綁定的元素,在事件委托情況下指向同一個元素

事件委托

采用事件委托改造本文例子

<!-- 省略了部分代碼 -->
<div id="div1">
  <button id="button1">button</button>
</div>
<script>
  document.addEventListener('click', e => {
    // e.target:觸發(fā)事件的元素
    // e.currentTarget:事件監(jiān)聽器綁定的元素
    // console.log(e.target, e.currentTarget)
    if (e.target.id === 'button1') {
      return console.log('button1 clicked')
    }
    if (e.target.id === 'div1') {
      return console.log('div1 clicked')
    }
    if (e.target.tagName === 'HTML') {
      return console.log('html clicked')
    }
    if (e.target.tagName === 'BODY') {
      return cosnole.log('body clicked')
    }
  })
</script>
復制代碼

依次點擊 html、div1、button1 會依次打印結(jié)果:

執(zhí)行結(jié)果

虛擬滾動

上面事件委托可以解決大批量節(jié)點事件監(jiān)聽的性能問題,但是DOM節(jié)點量過大導致界面滾動不流暢的問題沒解決。

虛擬滾動是指在列表滾動事件中動態(tài)計算出當前窗口中能夠顯示的列表節(jié)點,其前后窗口不顯示的節(jié)點各用一個div進行占位處理,并調(diào)整占位div的樣式確模擬的列表樣式與原始列表無異處。

虛擬滾動已經(jīng)是一個完美解決長列表渲染性能的方案,針對react、vue均有對應庫實現(xiàn),此處不再贅述

React事件代理

事件委托也叫事件代理,在react中的合成事件則是完全采用了事件代理

基于react16的 ant-design-pro 項目的事件綁定截圖如下:

React事件代理

直接通過onClick綁定的事件通過react處理最終會統(tǒng)一綁定到document上,正是使用的事件委托。

三、實戰(zhàn)中的應用

小程序中阻止事件冒泡阻止彈窗滾動穿透

最新版微信小程序中綁定事件有兩種類型: bind:methodName、catch:methodName、mut-bind:methodName、mut-catch:methodName ,其中區(qū)別最大的是前兩者。

methodName表示移動端事件名,例如:tap、touchmove等

bind:methodName 采用冒泡綁定事件, catch:methodName **采用冒泡綁定事件并阻止冒泡

vant-weapp中防止?jié)L動穿透

實現(xiàn)思路: 彈框采用一層遮罩層overlay遮蓋,遮罩層寬高設為窗口寬高同大小,采用 catch:touchmove 綁定該層的touchmove事件為空函數(shù)防止其冒泡 。

給overlay組件外層綁定touchmove為空函數(shù)并阻止冒泡:

// overlay/index.wxml
<van-transition
  show="{{ show }}"
  custom-class="van-overlay"
  custom-style="z-index: {{ zIndex }}; {{ customStyle }}"
  duration="{{ duration }}"
  bind:tap="onClick"
  catch:touchmove="noop"
  /*
   * 通過catch:touchmove來阻止冒泡至overlay層的事件繼續(xù)往上冒泡,
   * 實現(xiàn)內(nèi)層嵌套的內(nèi)容可以響應touchmove事件而外層(頁面)滾動事件不響應),
   * 以此來解決滾動事件穿透
  */
>
  <slot></slot>
</van-transition>
復制代碼

overlay組件外層fixed定位固定寬高防止?jié)L動:

// overlay/index.less
.van-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  .theme(background-color, '@overlay-background-color');
}
復制代碼

移動端H5框架中事件冒泡阻止彈窗滾動穿透

vant中防止?jié)L動穿透

實現(xiàn)思路:與vant-weapp小程序框架中相似, 彈框采用一層遮罩層overlay遮蓋,遮罩層寬高設為窗口寬高同大小,采用 @touchmove 綁定該層的touchmove事件,在事件中用event.stopPropagation阻止事件冒泡 。

給overlay組件外層綁定touchmove事件阻止冒泡:

return (
  <transition name="van-fade">
    <div
      vShow={props.show}
      style={style}
      class={[bem(), props.className]}
      onTouchmove={preventTouchMove}
      {...inherit(ctx, true)}
    />
  </transition>
);
復制代碼

const preventTouchMove(e: Event): void = () =&gt; { event.stopPropagation(); } <span>復制代碼</span>

overlay組件外層fixed定位固定寬高防止?jié)L動:

.van-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  .theme(background-color, '@overlay-background-color');
}
復制代碼


易優(yōu)小程序(企業(yè)版)+靈活api+前后代碼開源 碼云倉庫:starfork
本文地址:http://22321a.com/wxmini/doc/course/25241.html 復制鏈接 如需定制請聯(lián)系易優(yōu)客服咨詢:800182392 點擊咨詢
QQ在線咨詢