前端學習筆記之 3個使用場景助你用好 DOM 事件

防抖

input的輸入自動關鍵字匹配提示,通過監聽每一次輸入來出發 AJAX 請求,獲取匹配數據,但是每一次的輸入並不一定都要發起AJAX請求,此時,我們就需要通過使用 setTimeout() 函數來讓函數延遲執行,達到添加防抖功能。

  • 普通寫法
// 代碼1
const ipt = document.querySelector('input')
let timeout = null
ipt.addEventListener('input', e => {
  if(timeout) {
    clearTimeout(timeout)
    timeout = null
  }
  timeout = setTimeout(() => {
    search(e.target.value).then(resp => {
      // ...    
    }, e => {
      // ...
    })  
  }, 500)  
})
  • 優化寫法
    爲了達到複用,可以抽取成公共函數。
// 代碼2
const debounce = (func, wait = 0) => {
  let timeout = null
  let args
  function debounced(...arg) {
    args = arg
    if(timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    // 以Promise的形式返回函數執行結果
    return new Promise((res, rej) => {
      timeout = setTimeout(async () => {
        try {
          const result = await func.apply(this, args)
          res(result)
        } catch(e) {
          rej(e)
        }
      }, wait)
    })
  }
  // 允許取消
  function cancel() {
    clearTimeout(timeout)
    timeout = null
  }
  // 允許立即執行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  debounced.cancel = cancel
  debounced.flush = flush
  return debounced
}

代碼說明:首先將原函數作爲參數傳入 debounce() 函數中,同時指定延遲等待時間,返回一個新的函數,這個函數包含 cancel 屬性,用來取消原函數執行。flush 屬性用來立即調用原函數,同時將原函數的執行結果以 Promise 的形式返回。

節流

場景

實現文章閱讀大綱節點高亮顯示。具體就是,當用戶滾動閱讀右側文章內容時,左側大綱相對應部分高亮顯示,提示用戶當前閱讀位置。

現在來考慮另外一個場景,一個左右兩列布局的查看文章頁面,左側爲文章大綱結構,右側爲文章內容。現在需要添加一個功能,就是當用戶滾動閱讀右側文章內容時,左側大綱相對應部分高亮顯示,提示用戶當前閱讀位置。

這個功能的實現思路比較簡單,滾動前先記錄大綱中各個章節的垂直距離,然後監聽 scroll 事件的滾動距離,根據距離的比較來判斷需要高亮的章節。

思路

  1. 滾動前先記錄大綱中各個章節的垂直距離;
  2. 監聽 scroll 事件的滾動距離,根據距離的比較來判斷需要高亮的章節。
//爲代碼
// 監聽scroll事件
wrap.addEventListener('scroll', e => {
  let highlightId = ''
  // 遍歷大綱章節位置,與滾動距離比較,得到當前高亮章節id
  for (let id in offsetMap) {
    if (e.target.scrollTop <= offsetMap[id].offsetTop) {
      highlightId = id
      break
    }
  }
  const lastDom = document.querySelector('.highlight')
  const currentElem = document.querySelector(`a[href="#${highlightId}"]`)
  // 修改高亮樣式
  if (lastDom && lastDom.id !== highlightId) {
    lastDom.classList.remove('highlight')
    currentElem.classList.add('highlight')
  } else {
    currentElem.classList.add('highlight')
  }  
})

以上代碼可以實現大綱高亮的功能,但是由於滾動時間出發頻率很高,持續調用會影響渲染性能,因此,我們可以指定一段時間內只調用一次函數,從而降低函數調用頻率,這種方式我們稱之爲“節流”。
節流函數的兩種執行方式:

  • 在調用函數時執行最先一次調用;
  • 在調用函數時執行最近一次調用。
    通過節流方式實現如下:
//僞代碼
const throttle = (func, wait = 0, execFirstCall) => {
  let timeout = null
  let args
  let firstCallTimestamp


  function throttled(...arg) {
    if (!firstCallTimestamp) firstCallTimestamp = new Date().getTime()
    if (!execFirstCall || !args) {
      console.log('set args:', arg)
      args = arg
    }
    if (timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    // 以Promise的形式返回函數執行結果
    return new Promise(async(res, rej) => {
      if (new Date().getTime() - firstCallTimestamp >= wait) {
        try {
          const result = await func.apply(this, args)
          res(result)
        } catch (e) {
          rej(e)
        } finally {
          cancel()
        }
      } else {
        timeout = setTimeout(async () => {
          try {
            const result = await func.apply(this, args)
            res(result)
          } catch (e) {
            rej(e)
          } finally {
            cancel()
          }
        }, firstCallTimestamp + wait - new Date().getTime())
      }
    })
  }
  // 允許取消
  function cancel() {
    clearTimeout(timeout)
    args = null
    timeout = null
    firstCallTimestamp = null
  }
  // 允許立即執行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  throttled.cancel = cancel
  throttled.flush = flush
  return throttle
}

tips:
節流與防抖都是通過延遲執行,減少調用次數,來優化頻繁調用函數時的性能。不同的是,對於一段時間內的頻繁調用,防抖是延遲執行後一次調用,節流是延遲定時多次調用。

代理

以下實例是通過事件代理來優化列表中控件的點擊事件監聽。

初始實例

通過點擊每個項目的時候調用 getInfo() 函數,當點擊“編輯”時,調用一個 edit() 函數,當點擊“刪除”時,調用一個 del() 函數。

<ul class="list">
  <li class="item" id="item1">項目1<span class="edit">編輯</span><span class="delete">刪除</span></li>
  <li class="item" id="item2">項目2<span class="edit">編輯</span><span class="delete" >刪除</span></li>
  <li class="item" id="item3">項目3<span class="edit">編輯</span><span class="delete">刪除</span></li>
  ...
</ul>

問題所在

功能的實現並不難,但如果數據量一旦增大,事件綁定佔用的內存以及執行時間將會成線性增加,而其實這些事件監聽函數邏輯一致,只是參數不同而已。此時我們可以以事件代理或事件委託來進行優化。

首先,看看事件觸發流程的三個階段:

  • 捕獲,事件對象 Window 傳播到目標的父對象,圖 1 的紅色過程;
  • 目標,事件對象到達事件對象的事件目標,圖 1 的藍色過程;
  • 冒泡,事件對象從目標的父節點開始傳播到 Window,圖 1 的綠色過程。
    事件觸發流程圖
    監聽事件執行順序實例:
<body>
  <button>click</button>
</body>
<script>
document.querySelector('button').addEventListener('click', function () {
  console.log('bubble')
})
document.querySelector('button').addEventListener('click', function () {
  console.log('capture')
}, true)
// 執行結果
// buble
// capture
</script>

結合上述對DOM事件的觸發流程,我們得到事件代理的原理就是利用 DOM 事件的觸發流程來對一類事件進行統一處理。以下是事件代理實例代碼:

const ul = document.querySelector('.list')
ul.addEventListener('click', e => {
  const t = e.target || e.srcElement
  if (t.classList.contains('item')) {
    getInfo(t.id)
  } else {
    id = t.parentElement.id
    if (t.classList.contains('edit')) {
      edit(id)
    } else if (t.classList.contains('delete')) {
      del(id)
    }
  }
})

補充

區別以下三種事件監聽方式:

// 方式1
<input type="text" onclick="click()"/>
// 方式2
document.querySelector('input').onClick = function(e) {
  // ...
}
// 方式3
document.querySelector('input').addEventListener('click', function(e) {
  //...
})

方式 1 和方式 2 同屬於 DOM0 標準,通過這種方式進行事件監會覆蓋之前的事件監聽函數。

方式 3 屬於 DOM2 標準,推薦使用這種方式。同一元素上的事件監聽函數互不影響,而且可以獨立取消,調用順序和監聽順序一致。

筆記內容來自拉勾教育朱德龍講師講解的 前端高手進階 第三講

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章