36

前端进击的巨人(八):浅谈函数防抖与节流

 5 years ago
source link: https://segmentfault.com/a/1190000018383955?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

NbuyqiQ.png!web

本篇课题,或许早已是烂大街的解读文章。不过春招系列面试下来,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新。

JavaScript的执行过程,是基于栈来进行的。复杂的程序代码被封装到函数中,程序执行时,函数不断被推入执行栈中。所以 "执行栈" 也称 "函数执行栈"

函数中封装的代码块,一般都有相对复杂的逻辑处理(计算/判断),例如函数中可能会涉及到 DOM 的渲染更新,复杂的计算与验证, Ajax 数据请求等等。

前端页面的操作权,大部分都是属于浏览断的客户爸爸们(单身三十年的手速,惹不起惹不起!!!)。如果函数被频繁调用,造成的性能开销绝对不只一点点。

DOM

既要提升用户体验,又要减少后端服务开销,可见我们大前端的使命不只一页PPT。说好前因,接着就是后果了。既然有优化的需求,必然就要有相应的解决方案。隆重请出主角: “防抖”“节流”

防抖(debounce)

在事件被触发 n 秒后再执行回调函数,如果在这 n 秒内又被触发,则重新计时延迟时间。

生活化理解:英雄的技能条,技能条读完才能使用技能(R大招60s)

防抖的实现方式分两种 “立即执行”“非立即执行” ,区别在于第一次触发时,是否立即执行回调函数。

非立即执行

”非立即执行防抖“ 指事件触发后,回调函数不会立即执行,会在延迟时间 n 秒后执行,如果 n 秒内被调用多次,则重新计时延迟时间

// e.g. 防抖 - 非立即执行
function debounce(func, delay) {
  var timeout;
  return function() {
    var context = this;
    var args = arguments;
    // && 短路运算 == if(timeout) else {...} 
    timeout && clearTimeout(timeout); 
    timeout = setTimeout(function(){
      func.apply(context, args);
    }, delay);
  }
}

// 调用
var printUserName = debounce(function(){ 
  console.log(this.value);
}, 800);
document.getElementById('username')
  .addEventListener('keyup', printUserName);

立即执行

“立即执行防抖” 指事件触发后,回调函数会立即执行,之后要想触发执行回调函数,需等待 n 秒延迟

// e.g. 防抖 - 立即执行
function debounce(func, delay) {
    var timeout;
    return function() {
        var context = this;
        var args = arguments;
        callNow = !timeout;
        timeout = setTimeout(function() {
            timeout = null;
        }, delay);
        callNow && func.apply(context, args);
    }
}

函数防抖原理:通过维护一个定时器,其延迟计时以最后一次触发为计时起点,到达延迟时间后才会触发函数执行。

节流(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效(间隔执行)

生活化理解:

  1. FPS射击游戏子弹射速(即使按住鼠标左键,射出子弹的速度也是限定的)
  2. 水龙头的滴水(水滴攒到一定重量才会下落)

函数节流实现的方式有 “时间戳”“定时器” 两种。

时间戳

// e.g. 节流 - 时间戳
function throttle(func, delay) {
  var lastTime = 0;
  return function() {
    var context = this;
    var args = arguments;
    var nowTime = +new Date();
    if (nowTime > lastTime + delay) {
      func.apply(context, args)
      lastTime = nowTime;
    }
  }
}

“时间戳”的方式,函数在时间段开始时执行。

缺点:假定函数间隔1s执行,如果最后一次停止触发,卡在4.2s,则不会再执行。

定时器

// e.g. 节流 - 定时器
function throttle(func, delay) {
  var timeout;
  return function() {
    var context = this;
    var args = arguments;
    if (!timeout) {
      setTimeout(function(){
        func.apply(context, args);
        timeout = null;
      }, delay)
    }
  }
}

“定时器”的方式,函数在时间段结束时执行。可理解为函数并不会立即执行,而是等待延迟计时完成才执行。 (由于定时器延时,最后一次触发后,可能会再执行一次回调函数)

时间戳 + 定时器(互补优化)

// e.g. 节流 - 时间戳 + 定时器
function throttle(func, delay) {
  let lastTime, timeout;
  return function() {
    let context = this;
    let args = arguments;
    let nowTime = +new Date();
    if (lastTime && nowTime < lastTime + delay) {
      timeout && clearTimeout(timeout);
      timeout = setTimeout(function(){
        lastTime = nowTime;
        func.apply(context, args);
      }, delay);
    } else {
      lastTime = nowTime;
      func.apply(context, args);
    }
  }
}

合并优化的原理:“时间戳”方式让函数在时间段开始时执行(第一次触发立即执行),“定时器”方式让函数在最后一次事件触发后(如4.2s)也能触发。

函数节流原理:一定时间内只触发一次,间隔执行。通过判断是否到达指定触发时间,间隔时间固定。

“防抖” 与 “节流” 的异同

相同:都是防止某一时间段内,函数被频繁调用执行,通过时间频率控制,减少回调函数执行次数,来实现相关性能优化。

区别:“防抖”是某一时间内只执行一次,最后一次触发后过段时间执行,而“节流”则是间隔时间执行,间隔时间固定。

“防抖” 与 “节流” 的应用场景

防抖

  1. 文本输入搜索联想
  2. 文本输入验证(包括 Ajax 后端验证)

节流

scroll
resize
mousemove

应用场景还有很多,具体场景需具体分析。只要涉及高频的函数调用,都可参考函数防抖节流的优化方案。

鼓起勇气写在结尾:以上代码都不是 “完美” 的 “防抖 / 节流” 实现代码!!!仅就实现方式和基本原理,浅谈分解一二。

实际代码开发中,一般会引入 lodash 相对 “靠谱” 的第三方库,帮我们去实现防抖节流的工具函数。有兴趣的伙伴们可阅读 lodash 相关源码,加深印象理解可再读以下参考文章。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK