Skip to content

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

@lgwebdream

Description

@lgwebdream
Owner

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

Activity

Genzhen

Genzhen commented on Jun 23, 2020

@Genzhen
Collaborator

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

Genzhen commented on Jun 23, 2020

@Genzhen
Collaborator
/** 防抖:
 * 应用场景:当用户进行了某个行为(例如点击)之后。不希望每次行为都会触发方法,而是行为做出后,一段时间内没有再次重复行为,
 * 才给用户响应
 * 实现原理 : 每次触发事件时设置一个延时调用方法,并且取消之前的延时调用方法。(每次触发事件时都取消之前的延时调用方法)
 *  @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)
    }
}

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

123456zzz

123456zzz commented on Jul 13, 2020

@123456zzz
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);
yaooooooooo

yaooooooooo commented on Jul 20, 2020

@yaooooooooo

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();
}
}
})
}

GolderBrother

GolderBrother commented on Jul 20, 2020

@GolderBrother

// 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);
}
}
}

niunaibinggan

niunaibinggan commented on Aug 31, 2020

@niunaibinggan

防抖:在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)
}
}

tintinng

tintinng commented on Oct 21, 2020

@tintinng

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

chengazhen

chengazhen commented on Nov 4, 2020

@chengazhen
<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>
Bilibiber

Bilibiber commented on Dec 3, 2020

@Bilibiber

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();
}
}

JiaLe1

JiaLe1 commented on May 17, 2021

@JiaLe1

image

yingyingying-zhou

yingyingying-zhou commented on May 26, 2021

@yingyingying-zhou

PassionPenguin

PassionPenguin commented on Jun 8, 2021

@PassionPenguin
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 = [];
}
Luoyuda

Luoyuda commented on Jun 9, 2021

@Luoyuda
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
}
zizxzy

zizxzy commented on Nov 1, 2021

@zizxzy
// 防抖
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);
chihaoduodongxi

chihaoduodongxi commented on Feb 18, 2022

@chihaoduodongxi

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

chihaoduodongxi

chihaoduodongxi commented on Feb 18, 2022

@chihaoduodongxi

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

Kisthanny

Kisthanny commented on Mar 20, 2024

@Kisthanny
/**
 * 防抖节流都是把高频事件导致的高频回调降到低频的处理
 * 
 * 防抖在事件被触发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);
TANGYC-CS

TANGYC-CS commented on Apr 9, 2024

@TANGYC-CS

防抖(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);

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @lgwebdream@Luoyuda@Genzhen@JiaLe1@123456zzz

        Issue actions

          第 9 题:介绍防抖节流原理、区别以及应用,并用JavaScript进行实现 · Issue #15 · lgwebdream/FE-Interview