[JS效能優化]函式去抖(debounce)與函式節流(throttle)

CNO發表於2019-03-04

前言

這是個老生常談的話題了,之所以還搬出來講講,原因之一是之前根本就沒在意,近期面臨的一些問題需要用到這兩個小技巧;原因之二,這兩個技巧帶來的優化不小;原因之三,順便複習一下閉包。

開發中你可能會遇到下面的情況:

  • 監聽Window物件的resizescroll事件
  • 拖拽時監聽mousemove
  • 文字輸入時,對輸入字串進行處理,比如要把markdwon轉換成html
  • 監聽檔案變化,重啟服務

第一種和第三種情況,事件短時間內被頻繁出發,如果在事件中有大量的計算,頻繁操作DOM,資源載入等重行為,可能會導致UI卡頓,嚴重點甚至讓瀏覽器掛掉。對於第四種情況,有的開發者儲存編輯好的檔案喜歡按多次Ctrl+S,若是快速的重啟服務還能Hold住,但是要是重啟一個應用,就可能多次不必要的重啟。

針對上面這一系列的需求,於是有了debouncethrottle兩種解決辦法。

函式節流

函式按照一個週期執行,例如給window繫結一個resize事件之後,只要視窗改變大小改變就列印1,如果不採用函式節流,當我們將視窗調節的時候發現控制檯一直列印1,但是使用了函式節流後我們會發現調節的過程中,每隔一段時間才列印1

一個函式節流的簡單實現:

/**
*
* @param func    {Function}   實際要執行的函式
* @param wait    {Number}     執行間隔,單位是毫秒(ms),預設100ms
*
* @return        {Function}   返回一個“節流”函式
*/

function throttle(func, wait = 100) {
   // 利用閉包儲存定時器和上次執行時間
   let timer = null;
   let previous; // 上次執行時間
   return function() {
       // 儲存函式呼叫時的上下文和引數,傳遞給 fn
       const context = this;
       const args = arguments;
       const now = +new Date();
       if (previous && now < previous + wait) { // 週期之中
           clearTimeout(timer);
   	    timer = setTimeout(function() {
   	        previous = now;
   	        func.apply(context, args);
   	    }, wait);
       } else {
           previous = now;
           func.apply(context, args);
       }
   };
}
複製程式碼

使用的方法也很簡單:

const btn = document.getElementById(`btn`);

function demo() {
   console.log(`click`);
}
btn.addEventListener(`click`, throttle(demo, 1000));
複製程式碼

看看React中怎麼使用的,下面監聽視窗的resize和輸入框的onChange事件:

import React, { Component } from `react`;
import { throttle } from `../../utils/utils`;

export default class Demo extends Component {
 constructor() {
   super();
   this.change = throttle((e) => {
       console.log(e.target.value);
       console.log(`throttle`);
   }, 100);
 }
 
 componentDidMount() {
   window.addEventListener(`resize`, throttle(this.onWindowResize, 60));
 }
 
  componentWillUnmount() {
   window.removeEventListener(`resize`, throttle(this.onWindowResize, 60));
 }
 
 onWindowResize = () => {
 	console.log(`resize`);
 }

 handleChange = (e) => {
   e.persist();
   this.change(e);
 }

 render() {
   return (
       <input
         onChange={this.handleChange}
       />
   );
 }
}
複製程式碼

函式去抖

當事件觸發之後,必須等待某一個時間(N)之後,回撥函式才會執行,假若再等待的時間內,事件又觸發了則重新再等待時間N,直到事件N內事件不被觸發,那麼最後一次觸發過了事件N後,執行函式。

還是視窗resize,如果一直改變視窗大小,則不會列印1,只有停止改變視窗大小並等待一段時間後,才會列印1。

函式去抖簡單實現:

/**
 * @param     func     {Function}   實際要執行的函式
 * @param     delay    {Number}     延遲時間,單位是毫秒(ms)
 * @return    {Function}
 */

function debounce(fn, delay = 1000) {
  let timer;

  // 返回一個函式,這個函式會在一個時間區間結束後的 delay 毫秒時執行 func 函式
  return function () { 

    // 儲存函式呼叫時的上下文和引數,傳遞給func
    var context = this
    var args = arguments

    // 函式被呼叫,清除定時器
    clearTimeout(timer)

    // 當返回的函式被最後一次呼叫後(也就是使用者停止了某個連續的操作),
    // 再過 delay 毫秒就執行 func
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
  }
}
複製程式碼

應用場景,監聽檔案變化,重啟應用:

const debounce = require(`./debounce`);

watcher.on(`change`, debounce(() => {
    const child = spawn(`npm`, [`run`, `dev:electron`], {
      cwd,
      detached: true,
      stdio: `inherit`
    })
    child.unref();
    electron.app.quit();
  }, delay));
複製程式碼

相關文章