FE-Interview icon indicating copy to clipboard operation
FE-Interview copied to clipboard

第 9 题:介绍防抖节流原理、区别以及应用,并用JavaScript进行实现

Open lgwebdream opened this issue 4 years ago • 18 comments

欢迎在下方发表您的优质见解

lgwebdream avatar Jun 19 '20 12:06 lgwebdream

1)防抖

  • 原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
  • 适用场景:
    • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
    • 搜索框联想场景:防止联想发送请求,只发送最后一次输入
  • 简易版实现

function debounce(func, wait) {
    let timeout;
    return function () {
        const context = this;
        const args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}
  • 立即执行版实现
    • 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
// 有时希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。
function debounce(func, wait, immediate) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait)
      if (callNow) func.apply(context, args)
    } else {
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, wait);
    }
  }
}
  • 返回值版实现
    • func函数可能会有返回值,所以需要返回函数结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以只在 immediate 为 true 的时候返回函数的执行结果。
function debounce(func, wait, immediate) {
  let timeout, result;
  return function () {
    const context = this;
    const args = arguments;
    if (timeout) clearTimeout(timeout);
    if (immediate) {
      const callNow = !timeout;
      timeout = setTimeout(function () {
        timeout = null;
      }, wait)
      if (callNow) result = func.apply(context, args)
    }
    else {
      timeout = setTimeout(function () {
        func.apply(context, args)
      }, wait);
    }
    return result;
  }
}

2)节流

  • 原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
  • 适用场景
    • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
    • 缩放场景:监控浏览器resize
  • 使用时间戳实现
    • 使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。
function throttle(func, wait) {
  let context, args;
  let previous = 0;

  return function () {
    let now = +new Date();
    context = this;
    args = arguments;
    if (now - previous > wait) {
      func.apply(context, args);
      previous = now;
    }
  }
}
  • 使用定时器实现
    • 当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。
function throttle(func, wait) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(function () {
        timeout = null;
        func.apply(context, args)
      }, wait)
    }

  }
}

Genzhen avatar Jun 23 '20 02:06 Genzhen

/** 防抖:
 * 应用场景:当用户进行了某个行为(例如点击)之后。不希望每次行为都会触发方法,而是行为做出后,一段时间内没有再次重复行为,
 * 才给用户响应
 * 实现原理 : 每次触发事件时设置一个延时调用方法,并且取消之前的延时调用方法。(每次触发事件时都取消之前的延时调用方法)
 *  @params fun 传入的防抖函数(callback) delay 等待时间
 *  */
const debounce = (fun, delay = 500) => {
    let timer = null //设定一个定时器
    return function (...args) {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fun.apply(this, args)
        }, delay)
    }
}
/** 节流
 *  应用场景:用户进行高频事件触发(滚动),但在限制在n秒内只会执行一次。
 *  实现原理: 每次触发时间的时候,判断当前是否存在等待执行的延时函数
 * @params fun 传入的防抖函数(callback) delay 等待时间
 * */

const throttle = (fun, delay = 1000) => {
    let flag = true;
    return function (...args) {
        if (!flag) return;
        flag = false
        setTimeout(() => {
            fun.apply(this, args)
            flag = true
        }, delay)
    }
}

区别:节流不管事件触发多频繁保证在一定时间内一定会执行一次函数。防抖是只在最后一次事件触发后才会执行一次函数

Genzhen avatar Jun 23 '20 02:06 Genzhen

const debouce = (
  fn: () => void,
  wait: number = 50,
  immediate: boolean = true
) => {
  let timer: number | null, context: any, args: any;
  const later = () =>
    setTimeout(() => {
      timer = null;
      if (!immediate) {
        fn.apply(context, args);
      }
    }, wait);
  return function (...params: any[]) {
    if (!timer) {
      timer = later();
      if (immediate) {
        fn.apply(this, params);
      } else {
        context = this;
        args = params;
      }
    } else {
      clearTimeout(timer);
      timer = later();
    }
  };
};

//test

window.onmousemove = debouce(console.log, 1000, true);

123456zzz avatar Jul 13 '20 16:07 123456zzz

const createProxyThrottle = (fn,rate){ const lastClick = Date.now() - rate; return new Proxy(fn,{ apply(target,context,args){ if(Date.now() - lastClick >= rate){ fn(args); lastClick = Date.now(); } } }) }

yaooooooooo avatar Jul 20 '20 01:07 yaooooooooo

// 1. 防抖:不停的触发事件,只执行最后一次 // 指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次。假如我们设置了一个等待时间 3 秒的函数,在这 3 秒内如果遇到函数调用请求就重新计时 3 秒,直至新的 3 秒内没有函数调用请求,此时执行函数,不然就以此类推重新计时。

function debounce(fn, wait = 500, immediate = false) { let timer = null, result = null; return function (...args) { if (timer) { clearTimeout(timer); } if (immediate && !timer) { result = fn.apply(this, args); } timer = setTimeout(() => { fn.apply(this, args); }, wait); return result; } }

// 2. 函数节流指的是某个函数在一定时间间隔内(例如 3 秒)只执行一次,在这 3 秒内 无视后来产生的函数调用请求,也不会延长时间间隔。3 秒间隔结束后第一次遇到新的函数调用会触发执行,然后在这新的 3 秒内依旧无视后来产生的函数调用请求,以此类推。 // 函数节流非常适用于函数被频繁调用的场景,例如:window.onresize() 事件、mousemove 事件、上传进度等情况。 function throttle(fn, wait = 500, immediate = false) { let timer = null, startTime = Date.now(), result = null; return function (...args) { if(immediate) result = fn.apply(this, args); const now = Date.now(); // 超过了延时时间,马上执行 if(now - startTime > wait) { // 更新开始时间 startTime = Date.now(); result = fn.apply(this, args); }else { // 否则定时指定时间后执行 if(timer) clearTimeout(timer); timer = setTimeout(() => { // 更新开始时间 startTime = Date.now(); fn.apply(this, args); }, wait); } } }

GolderBrother avatar Jul 20 '20 14:07 GolderBrother

防抖:在n秒时间内,不停的被触发,只执行最后一次 function debounce(fn, n){ let timer = null return function (){ if(timer) { clearTimeout(timer) timer = setTimeout(fn, n) } else { timer = setTimeout(fn, n) } } }

节流:在n秒时间内,不停的被触发,只执行第一次 function throttre (fn, n){ let flag = true return function(){ if(!flag) return flag = false setTimeout(()=>{ fn() flag = true },n) } }

niunaibinggan avatar Aug 31 '20 07:08 niunaibinggan

为什么防抖的返回值版本不能直接在函数的调用中直接反返回呢? return func.apply(context, args); 这样不是在不立即执行的时候也可以返回值了嘛

tintinng avatar Oct 21 '20 03:10 tintinng

<script type="text/javascript">
    let timer;

    //防抖函数  我经常用于 防止多次点击 无论点多少下 都只有最后一次才会执行相应逻辑(当然你如果将延时器的时间设置的很短,结果可能会不一样)
    function shake(param) {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        console.log('防抖')
      }, 500)
    }

    //节流 不论你怎么点 我就按照自己的脾气来(一秒执行一下) 
    function throttle(param) {
      if (timer) return
      timer = setTimeout(() => {
        console.log('节流')
        timer = null;
      }, 1000)
    }
  </script>

chengazhen avatar Nov 04 '20 03:11 chengazhen

const Throttling = (fn, delay) =>{ let last = 0; return function(){ const now = new Date().getTime(); console.log(now-last); if(now-last<delay) return; last = now; return fn(); } }

Bilibiber avatar Dec 03 '20 23:12 Bilibiber

image

JiaLe1 avatar May 17 '21 07:05 JiaLe1

yingyingying-zhou avatar May 26 '21 06:05 yingyingying-zhou

class Debounce {
  toggle(callback, wait) {
      clearTimeout(this.timeout);
      this.timeout = setTimeout(function(){
        callback.apply(this, arguments)
      }, wait);
  }

  timeout = null;
}
class Throttle {
    toggle(callback) {
        for(let e of this.list) {
            if(new Date().getTime() - e > 5000) this.list.shift();
        }
        if(this.list.length < 3) {
            this.list.push(new Date().getTime());
            callback.call();
        }
    }
    
    list = [];
}

PassionPenguin avatar Jun 08 '21 06:06 PassionPenguin

function debounce(func, wait, immediate) {
    var timeout, result
    var d = function(){
        var context = this
        var args = arguments
        if(timeout)clearTimeout(timeout)
        if(immediate){
            var call = !timeout
            timeout = setTimeout(function(){
                timeout = null
            }, wait)
            if(call) result = func.apply(context, args)
        }else{
            timeout = setTimeout(function(){
                result = func.apply(context, args)
                timeout = null
            }, wait)
        }
        return result
    }
    d.cancel = function(){
        if(timeout) clearTimeout(timeout)
        timeout = null
    }
    return d
}
function throttle(func, wait, options) {
    options = options || {}
    var timeout, result
    var previous = 0
    function t(){
        var context = this
        var args = arguments
        var now = +new Date()
        previous = options.leading === false && !previous ? now : previous
        var r = wait - (now - previous)
        if(r > wait || r <= 0){
            if(timeout){
                clearTimeout(timeout)
                timeout = null
            }
            previous = now
            result = func.apply(context, args)
        }else if(!timeout && options.trailing !== false){
            timeout = setTimeout(function(){
                previous = options.leading === false ? 0 : +new Date()
                timeout = null
                result = func.apply(context, args)
            }, r)
        }
        return result
    }
    t.cancel = function(){
        clearTimeout(timeout)
        timeout = null
        previous = 0
    }
    return t
}

Luoyuda avatar Jun 09 '21 07:06 Luoyuda

// 防抖
const debounce = (func, wait) => {
  let timer = null;
  return function(){
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    setTimeout(() => {
      func.apply(context, args);
    }, wait)
  }
}
// test
const test = debounce(() => { console.log(777); }, 5000);
test('a');

// 节流
const throttle = (func, wait) => {
  var flag = true;
  return function () {
    if (!flag) return;
    flag = false;
    const context = this;
    const args = arguments;
    setTimeout(() => {
      flag = true;
      func.apply(context, args);
    }, wait);
  }
}
// test
const test = throttle(() => { console.log('111') }, 3000)
setInterval(() => {
  test();
}, 1000);

zizxzy avatar Nov 01 '21 10:11 zizxzy

//防抖 function debounce(fn,n){ let timer =null; return function(){ clearTimeout(timer); timer= setTimeout(fn,n); } } let con=debounce(()=>console.log('1'),2000);

chihaoduodongxi avatar Feb 18 '22 09:02 chihaoduodongxi

//节流 function throttle(fn, n) { let flag = true; return function () { if (flag) { fn(); flag = false; setTimeout(() => (flag = true), n); } }; }

chihaoduodongxi avatar Feb 18 '22 09:02 chihaoduodongxi

/**
 * 防抖节流都是把高频事件导致的高频回调降到低频的处理
 * 
 * 防抖在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
 * 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
 * 搜索框联想场景:防止联想发送请求,只发送最后一次输入
 * 
 * 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
 * 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
 * 缩放场景:监控浏览器resize
 */

function debounce(fn, timeout) {
  let timer;
  return function () {
    const self = this;
    const args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(self, args);
    }, timeout);
  };
}

function throttle(fn, timeout) {
  let timer;
  return function () {
    const self = this;
    const args = arguments;
    if (timer) {
      return;
    }
    timer = setTimeout(() => {
      fn.apply(self, args);
      timer = null;
    }, timeout);
  };
}

// 以下是测试代码
// const handleInput = debounce((e) => {
//   console.log(e);
// }, 1000);
let count = 0;
const handleInput = throttle((e) => {
  count++;
  console.log(count);
}, 1000);

document.addEventListener("input", handleInput);

Kisthanny avatar Mar 20 '24 07:03 Kisthanny

防抖(debounce)和节流(throttle)是两种常用的性能优化手段,尤其在处理高频触发的事件(如滚动、窗口大小调整、键盘输入等)时非常有用。

  • 防抖(Debounce) 如果一个函数持续被触发,那么在这个函数执行一次之后,只有在一定的时间间隔内没有再次被触发,才会执行第二次。 也就是说,防抖会确保函数在一定时间内只执行一次,即使在这段时间内被触发了很多次。

  • 节流(Throttle) 在单位时间,无论触发多少次,函数都只会执行一次。 也就是说,节流会限制函数的执行频率,确保在单位时间内只执行一次。

  • 区别

    1. 防抖注重的是一定时间间隔内只执行一次,而节流注重的是单位时间内只执行一次。
    2. 防抖是在事件触发后 n 秒内函数只能执行一次,如果在这 n 秒内又触发了这个事件,则重新计算执行时间。 节流是不管事件触发多么频繁,都会保证在 n 秒内只执行一次。
    3. 如果需要确保用户停止操作一段时间后才执行,可以选择防抖。
    4. 如果需要确保在一定时间内至少执行一次操作,可以选择节流。
  • 应用

    • 防抖的主要思想是:在一定时间内,事件处理函数只执行一次,如果在这个时间段内又触发了该事件,则重新计算执行时间。这适用于短时间内的大量触发场景,例如用户连续输入搜索关键字时,浏览器不会立即执行搜索请求,而是等待用户停止输入一段时间后,再执行搜索操作。这样可以避免在用户连续操作时发送过多的无意义请求,提高页面性能和用户体验。

    • 而节流的主要思想是:在一段时间内最多触发一次事件,无论这个事件被触发了多少次。每次事件触发时,如果之前的事件还在等待执行,就取消之前的等待,并重新设置等待时间。节流适用于持续的触发场景,确保在一定时间间隔内至少执行一次操作,如监听滚动事件时,每隔一段时间更新页面位置等。

    • 总结来说,防抖是将多次执行变为最后一次执行,而节流是将多次执行变成每隔一段时间执行。两者都可以有效减少事件触发的频率,但在不同的使用场景下,选择哪种方法取决于具体需求。如果需要确保用户停止操作一段时间后才执行,可以选择防抖;如果需要确保在一定时间内至少执行一次操作,可以选择节流。

防抖实现

function debounce(func, wait) {
  // 定义一个变量timeout,用来存储setTimeout返回的定时器的ID。
  // 初始值为undefined,表示没有定时器在运行。
  let timeout;

  // 返回一个函数,这个函数将会代替原始函数func被调用。
  return function () {
    // 使用context保存当前上下文this,以便在setTimeout中正确调用func。
    const context = this;

    // 使用arguments对象保存传入的所有参数,以便在setTimeout中传递给func。
    const args = arguments;

    // 如果timeout存在(即定时器正在运行),则清除它。
    // 这样可以确保不会执行旧的定时器回调,只保留最新的。
    if (timeout) clearTimeout(timeout);

    // 设置一个新的定时器,等待wait毫秒后执行func函数。
    // 注意:这里使用了闭包,所以func、context和args的值会在定时器触发时保持不变。
    timeout = setTimeout(function () {
      // 当定时器触发时,使用apply方法来调用func,并传入之前保存的context和args。
      // 这样就可以确保func在正确的上下文中使用正确的参数被调用。
      func.apply(context, args);
    }, wait);

    // 返回的这个函数没有返回值,所以调用它会返回undefined
    // 但重要的是,它设置了定时器来调用func,并处理了可能得定时器重叠
  };
}
// 用箭头函数和剩余参数的简化版
function debounce(func, wait) {
  let timeout;
  return function (...args) {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

这个防抖函数的工作原理是:

  1. 当返回的函数被调用时,它首先检查是否已经有定时器在运行(即timeout是否存在)。
  2. 如果存在定时器,它会立即清除该定时器,这样可以防止在wait毫秒内多次调用后执行func
  3. 然后,它设置一个新的定时器,等待wait毫秒后执行func
  4. 如果在wait毫秒内再次调用这个返回的函数,它会再次清除定时器并设置一个新的,确保func不会在这段时间内执行。
  5. 只有当wait毫秒的等待期内没有再次调用这个返回的函数时,func才会最终被执行。

这种机制非常适用于处理如输入框实时搜索、窗口大小调整等需要延迟执行的任务,因为这样可以防止函数在短时间内被频繁调用,从而提高性能。

如何使用防抖

要使用上面的防抖函数,需要首先定义一个需要防抖处理的函数,然后将其传递给debounce函数来获取一个新的防抖函数。 之后,你可以像调用普通函数一样调用这个新的防抖函数。具体使用示例:

/**
 * 实时搜索函数
 */
function search(query) {
  // 这里可以发送AJAX请求到服务器进行搜索
  console.log("Searching for:", query);
  // 假设这里模拟一个搜索请求延迟
  return new Promise((resolve) =>
    setTimeout(() => {
      console.log("Search results for:", query);
      resolve("Search results");
    }, 1000)
  );
}

/**
 * 使用`debounce`函数创建一个防抖版本的抖索函数`debouncedSearch`
 */
const debouncedSearch = debounce(search, 500);

/**
 * 输入框的实时搜索时间处理函数
 */
function handleSearchInput(event) {
  const query = event.target.value;
  // 使用防抖函数包装搜索函数
  debouncedSearch(query);
}

// 获取输入框元素并绑定事件监听器
const searchInput = document.querySelector("#search-input");
searchInput.addEventListener("input", handleSearchInput);

在上面的代码中,首先定义了一个防抖函数debounce,它接受一个需要防抖的函数func和一个等待时间wait作为参数。 防抖函数返回一个新的函数,这个新函数会在每次调用时取消之前的等待定时器,并重新设置一个新的等待定时器。只有在等待时间过后,才会执行原函数func

接下来,定义了一个实时搜索函数search,它接受一个查询参数query,并打印出搜索内容。在实际应用中,这个函数可能会发送搜索请求到服务器。

然后,使用debounce函数创建了一个防抖版本的搜索函数debouncedSearch,并设置防抖间隔为 500 毫秒。这意味着在用户停止输入 500 毫秒后,才会执行搜索操作。

最后,假设有一个搜索框,通过getElementById获取其 DOM 元素,并监听它的input事件。在事件处理函数中,使用防抖版本的搜索函数debouncedSearch来处理用户输入的查询内容。这样,即使用户在输入框中快速输入,也不会立即触发搜索操作,而是会等待用户停止输入一段时间后再执行搜索。

这种方式可以有效减少不必要的搜索请求,提高性能和用户体验。

节流实现

function throttle(func, limit) {
  // 定义一个变量,用于标记当前是否处于节流状态
  let inThrottle;

  // 返回一个新函数,作为节流函数的代理
  return function (...args) {
    const context = this;
    // 检查是否处于节流状态
    if (!inThrottle) {
      // 调用原始函数,并传入正确的上下文和参数
      func.apply(context, args);

      // 将节流状态设置为true,表示现在正在处于节流中
      inThrottle = true;

      // 设置一个定时器,在limit毫秒后将节流状态重置为false
      setTimeout(() => (inThrottle = false), limit);
    }
    // 如果处于节流状态,则不执行任何操作,直接返回
  };
}

throttle函数接受一个需要被节流的函数func和一个时间间隔limit作为参数。 它返回一个内部函数,这个内部函数在每次调用时都会检查是否处于节流状态。 如果不处于节流状态,它会调用func函数,并将节流状态设置为true,然后设置一个定时器在limit毫秒后将节流状态重置为false。 如果处于节流状态,则不会执行func函数。 通过这种方式,可以确保func函数在指定的时间间隔内只被调用一次。

如何使用节流

function upadateScrollPosition() {
  console.log("Scroll position updated:", window.scrollY);
}

const throttledUpdateScrollPosition = throttle(upadateScrollPosition, 300);

window.addEventListener("scroll", throttledUpdateScrollPosition);

滚动事件监听: 当页面滚动时,可能希望执行某些操作,比如懒加载图片、更新导航栏状态等。 但是,如果每次滚动都触发这些操作,会导致性能问题。 使用节流函数可以确保在一定时间间隔内只执行一次操作。

TANGYC-CS avatar Apr 09 '24 03:04 TANGYC-CS