節(jié)流與防抖

防抖

防抖, 僅僅從字面去理解,就是防止抖動(dòng),關(guān)鍵點(diǎn)是等待,等待300ms,如果沒(méi)有新的action,就執(zhí)行。
這里舉一個(gè)更形象的例子,也是使用此場(chǎng)景最多的例子。

有一個(gè)搜索輸入框,為了提升用戶體驗(yàn),希望在用戶輸入后可以立即展現(xiàn)搜索結(jié)果,而不是每次輸入完后還要點(diǎn)擊搜索按鈕。最基本的實(shí)現(xiàn)方式應(yīng)該很容易想到,那就是綁定 input 元素的鍵盤事件,每次輸入的時(shí)候,觸發(fā)input,向后臺(tái)發(fā)起請(qǐng)求。

類似于以下偽代碼:

const inputEle = document.querySelector('input')
async function search (e) {
    await getUser({name: e.targe.value})
}
inputEle.addEventListener('input', search)

但是這個(gè)時(shí)候,后端提出了一個(gè)問(wèn)題,不希望用戶每輸入一個(gè)字符,就發(fā)起一次后端請(qǐng)求,這樣會(huì)造成資源的浪費(fèi)。例如每當(dāng)用戶輸入一個(gè)字符,都會(huì)觸發(fā)搜索,而實(shí)際上,只有最后一次搜索結(jié)果是用戶想要的,前面進(jìn)行了 2 次無(wú)效查詢,浪費(fèi)了網(wǎng)絡(luò)帶寬和服務(wù)器資源。

  • 1: 'l'
  • 2: 'li'
  • 3: 'liu'

對(duì)于這類連續(xù)觸發(fā)的事件,需要添加一個(gè)“防抖”功能,為函數(shù)的執(zhí)行設(shè)置一個(gè)合理的時(shí)間間隔,避免事件在時(shí)間間隔內(nèi)頻繁觸發(fā),同時(shí)又保證用戶輸入后能即時(shí)看到搜索結(jié)果。

接下來(lái),我們分析下如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單版的的防抖

function debounce (fn, wait = 0) {
  let timeout = null
  return function () {
    // 如果已經(jīng)存在定時(shí)器,說(shuō)明是在wait間隔之內(nèi)觸發(fā)的,需要重新計(jì)時(shí)
    if (timeout) {
      clearTimeout(timeout)
      timeout = null
    }
    let self = this
    let args = Array.prototype.slice(arguments)
    timeout = setTimeout(() => {
      fn.call(self, ...args)
    }, wait)
  }
}

如果通過(guò)箭頭函數(shù),也可以這么寫

function debounce = (fn, wait = 0) => {
    let timeout = null
    return (...args) => {
        if (timeout) {
            clearTimeout(timeout)
            timeout = null
        }
        timeout = setTimeout(async () => {
            // 箭頭函數(shù)里的this,指向定義時(shí)的上一層,沒(méi)有自己的this
            await fn.apply(this, args)
        }, wait)
    }
}

通過(guò)這段代碼,我們做測(cè)試和調(diào)試

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>截流與防抖</title></title>
    <script src="./debounce.js"></script>
</head>
<body>
    <div>
        <input type="text"> 請(qǐng)輸入
    </div>
    <script>
        const inputEle = document.querySelector('input')
        async function search (e) {
            console.log(this)
            console.log('input==>', e.target.value)
        }
        inputEle.addEventListener('input', debounce(search, 200))
    </script>
</body>
</html>

接下來(lái),我們實(shí)現(xiàn)一個(gè)豪華版的防抖

const debounce = (func, await = 0) => {
  let timeout = null
  let args
  function dobounced(...arg) {
    args = arg
    if (timeout) {
      clearTimeout(timeouts)
      timeout = null
    }
    return new Promise((resolve, reject) => {
      timeout = setTimeout(async() => {
        try {
          const res = await func.apply(this, args)
          resolve(res)
        } catch (e) {
          reject(e)
        }
      }, wait)
    })
  }
  // 允許取消
  function cancel() {
    clearTimeout(timeout)
    timeout = null
  }
  // 允許立即執(zhí)行
  function flush() {
    cancel()
    return func.apply(this, args)
  }
  debounced.cancel = cancel
  debounced.flush = flush
  return debounced
}

節(jié)流

我們知道有個(gè)詞語(yǔ),叫做開源節(jié)流。這里的節(jié)流是省著消費(fèi)。而我們JS里的節(jié)流,也用相同的意義,減少?zèng)]有必要的dom操作,因?yàn)閐om操作是昂貴的。節(jié)流的關(guān)鍵詞是丟棄, 500ms內(nèi)只執(zhí)行一次,其余的action 被丟棄。打一個(gè)生活中的比喻就是:我腦海里無(wú)時(shí)無(wú)刻都有購(gòu)物的想法,但是要省錢,就必須節(jié)制購(gòu)物,給自己定下規(guī)則,3個(gè)月內(nèi)只能買一次物品。當(dāng)然現(xiàn)實(shí)生活中,小伙伴們不會(huì)對(duì)自己這么嚴(yán)苛啊,這里的例子只是方便大家理解。

我們舉一個(gè)項(xiàng)目上的應(yīng)用,加深對(duì)節(jié)流的理解。

例子來(lái)源于拉勾教育-前端高手進(jìn)階, 有興趣可以到應(yīng)用里學(xué)習(xí)
一個(gè)左右兩列布局的查看文章頁(yè)面,左側(cè)為文章大綱結(jié)構(gòu),右側(cè)為文章內(nèi)容?,F(xiàn)在需要添加一個(gè)功能,就是當(dāng)用戶滾動(dòng)閱讀右側(cè)文章內(nèi)容時(shí),左側(cè)大綱相對(duì)應(yīng)部分高亮顯示,提示用戶當(dāng)前閱讀位置。這個(gè)功能的實(shí)現(xiàn)思路比較簡(jiǎn)單,滾動(dòng)前先記錄大綱中各個(gè)章節(jié)的垂直距離,然后監(jiān)聽 scroll 事件的滾動(dòng)距離,根據(jù)距離的比較來(lái)判斷需要高亮的章節(jié)。偽代碼如下:

// 監(jiān)聽scroll事件
wrap.addEventListener('scroll', e => {
  let highlightId = ''
  // 遍歷大綱章節(jié)位置,與滾動(dòng)距離比較,得到當(dāng)前高亮章節(jié)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')
  }
})

功能是實(shí)現(xiàn)了,但這并不是最優(yōu)方法,因?yàn)闈L動(dòng)事件的觸發(fā)頻率是很高的,持續(xù)調(diào)用判斷函數(shù)很可能會(huì)影響渲染性能。實(shí)際上也不需要過(guò)于頻繁地調(diào)用,因?yàn)楫?dāng)鼠標(biāo)滾動(dòng) 1 像素的時(shí)候,很有可能當(dāng)前章節(jié)的閱讀并沒(méi)有發(fā)生變化。所以我們可以設(shè)置在指定一段時(shí)間內(nèi)只調(diào)用一次函數(shù),從而降低函數(shù)調(diào)用頻率,這種方式我們稱之為“節(jié)流”。

方法一:
通過(guò)一個(gè)標(biāo)識(shí)位,來(lái)實(shí)現(xiàn)第一個(gè)截流,可以滿足一般場(chǎng)景

 function throttle (fn, interval = 0) {
    let isExecute = true
    return function (...args) {
      if (isExecute) {
        fn.apply(this, args)
        isExecute = false
        setTimeout(() => {
          isExecute = true
        }, interval) 
      }
    }
  }

方法二:
通過(guò)時(shí)間來(lái)控制

第一次就會(huì)觸發(fā), 因?yàn)榈谝淮蝜ast ===0 ,導(dǎo)致 now 是一定大于 delay的,所以第一次必須觸發(fā)

function throttle(fn, interval) {
    let last = 0
    return function () {
        let now = Date.now()
        let context = this
        let args = arguments
        
        if (now - last > interval) {
            last = Date.now()
            fn.apply(context, args)
        }
    }
}

方法三,思想類似與方法一

第一次也是延遲執(zhí)行,但是用戶最后一次操作,也會(huì)延遲執(zhí)行

function throttle(fn, interval = 0) {
  let timer = null
  return function () {
      let args = arguments
      if (!timer) {
        timer = setTimeout(() => {
              fn.apply(this, args)
              clearTimeout(timer)
              timer = null
          }, interval)
      }
  }
}

方法四:更精確的時(shí)間控制防抖

function throttle5(fn, interval) {
  let timer = null
  let startTime = Date.now()
  return function () {
      let curTime = Date.now()
      let remainning = interval - (curTime - startTime)
      let context = this
      let args = arguments
      clearTimeout(timer)
      // 操作已過(guò)剩余時(shí)間,立即執(zhí)行
      if (remainning <=0) {
          // 重新計(jì)時(shí)
          startTime = Date.now()
          fn.apply(context, args)
      } else {
          timer = setTimeout(() => {
              fn.apply(context, args)
          }, remainning)
      }
  }
}

可以通過(guò)下面這點(diǎn)代碼來(lái)驗(yàn)證調(diào)試throttle

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>截流與防抖</title></title>
    <script src="./debounce.js"></script>
    <script src="./throttle.js"></script>
    <style>
        .test {
            width: 200px;
            height: 200px;
            overflow: auto;
            background-color: burlywood;
        }
    </style>
</head>
<body>
    <div class="test">
        一個(gè)左右兩列布局的查看文章頁(yè)面,左側(cè)為文章大綱結(jié)構(gòu),右側(cè)為文章內(nèi)容?,F(xiàn)在需要添加一個(gè)功能,就是當(dāng)用戶滾動(dòng)閱讀右側(cè)文章內(nèi)容時(shí),左側(cè)大綱相對(duì)應(yīng)部分高亮顯示,提示用戶當(dāng)前閱讀位置。這個(gè)功能的實(shí)現(xiàn)思路比較簡(jiǎn)單,滾動(dòng)前先記錄大綱中各個(gè)章節(jié)的垂直距離,然后監(jiān)聽 scroll 事件的滾動(dòng)距離,根據(jù)距離的比較來(lái)判斷需要高亮的章節(jié)。 
    </div>
    <script>
        const ele = document.querySelector('.test')
        function scrollFn (e) {
            console.log('this==>', this)
            console.log('top', e.target.scrollTop)
        }
        ele.addEventListener('scroll', throttle5(scrollFn, 500))
    </script>
</body>
</html>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容