面试官:工作两年半了,你真的懂节流吗?

前言

日前我参加了字节跳动的面试。面试官看着我的简历,沉思了一下,说:"手写个节流函数给我瞅瞅,写得好的话,直接给你offer!"

我心里"咯噔"一声,原来一个小小的节流函数,就能直接决定我能不能拿到offer!看来不能掉以轻心,一定要写出一个最优雅和专业的节流函数给面试官瞅瞅!

什么是节流

在面试官提出的问题中,"手写节流函数" 早在2017年就在很多前端博客里出现,节流函数的应用在前端开发中是至关重要的,它可以确保我们的代码在处理频繁的事件时不会导致性能问题:JS执行和网页的重新渲染是互斥的。

节流函数的核心思想是:控制函数的执行频率,确保函数在一定时间间隔内只被执行一次。这意味着无论用户多频繁地触发某个事件,函数都只会在规定的时间内执行一次。这对于处理滚动事件、输入事件等非常有用。网上的答案也是层出不穷,有ES5版本的,有ES6版的,有用定时器做的,也有用时间戳做的,面试前复习过一遍,脑海里依稀记得大概长下面这个样子:

js 复制代码
// 时间戳版本
function throttle(fun, interval) {
  let lastTimestap = 0; //初始时间
  return function (...args) {
    let currentTimestap = new Date(); //当前时间
    if (currentTimestap - lastTimestap > interval) {
      fun.apply(this, args);
      lastTimestap = currentTimestap;
    }
  };
}

// 定时器版本
function throttle(fn, interval) {
  let lock = false;
  return function (...args) {
    if (!lock) {
      lock = true;
      setTimeout(() => {
        lock = false;
        fn.apply(this, args);
      }, interval);
    }
  };
}

然而,就在我充满自信地准备默写时间戳版本的时候,面试官放了一张图并加了三个节流函数的额外限制条件:

  1. 可以控制首次的回调函数是否立即调用
  2. 距离本周期结束最近的一个回调函数,会被延迟到本周期结束时执行
  3. 可以控制最后的回调是否延迟执行

尊嘟假嘟?第一次还能配置不立即执行?那我是不是得写定时器版本的节流?最后一次还能配置是否延迟执行?我措手不及,被颠覆了认知,陷入了窘境。我能感受到面试官的一丝幽默,他拿着答案看着我做不出来,笑了起来。这一刻,我明白了前端开发的真正精髓,不仅在于掌握基本概念,还在于能否现场解决难题。

实现节流

实现后续回调可延迟执行

既然是首次回调立即执行,那么肯定不能换定时器版本的节流,必须得在时间戳版本的节流上加额外的分支,来处理在interval内触发多次回调的情况(在interval节点上触发的情况已经处理了,也就是立即执行),其实延迟执行的本质就是使用定时器,延迟到下一个interval时间点上执行就实现了,延迟多久呢?看图可得 remaining = interval - (currentTimestap - lastTimestap),多次触发的时候记得清空上一轮的定时器,因为定时器只能存在一个

js 复制代码
function throttle(fn, interval) {
  let lastTimestap = 0,
    timer = null;
  return function (...args) {
    let currentTimestap = new Date();
    // 清空上一轮的计时器
    clearTimeout(timer);
    if (currentTimestap - lastTimestap >= interval) {
      fn.apply(this, args);
      lastTimestap = currentTimestap;
    } else {
      // 使用计时器把最近的回调延时触发
      const remaining = interval - (currentTimestap - lastTimestap);
      timer = setTimeout(() => {
        fn.apply(this, args);
        lastTimestap = new Date();
      }, remaining);
    }
  };
}

配置后续回调可延迟执行

添加一个options参数{ trailing: true },用来判断处不处理在interval内触发多次回调的情况就行了

js 复制代码
function throttle(fn, interval, options = { trailing: true }) {
  let lastTimestap = 0,
    timer = null;
  return function (...args) {
    let currentTimestap = new Date();
    clearTimeout(timer);
    if (currentTimestap - lastTimestap >= interval) {
      fn.apply(this, args);
      lastTimestap = currentTimestap;
    } else if (options.trailing) { // 根据options.trailing来判断走不走这个分支逻辑就行了
      const remaining = interval - (currentTimestap - lastTimestap);
      timer = setTimeout(() => {
        fn.apply(this, args);
        lastTimestap = new Date();
      }, remaining);
    }
  };
}

配置首次回调可立即执行

为了做到在任何周期内,次次都进入到延迟执行的逻辑中,那就得让currentTimestap - lastTimestap >= interval不成立,比如在初始化的时候让lastTimestap = currentTimestap,直接进入定时器的逻辑(lastTimestap初始值为0)

js 复制代码
function throttle(fn, interval, options = { leading: true, trailing: true }) {
  let lastTimestap = 0,
    timer = null;
  return function (...args) {
    let currentTimestap = new Date();
    clearTimeout(timer);
    // 让lastTimestap初始值变为currentTimestap进入定时器逻辑
    if (!options.leading && !lastTimestap) lastTimestap = currentTimestap;
    // options.leading处理leading和trailing同时为false的情况
    if (options.leading && currentTimestap - lastTimestap >= interval) {
      fn.apply(this, args);
      lastTimestap = currentTimestap;
    } else if (options.trailing) {
      const remaining = interval - (currentTimestap - lastTimestap);
      timer = setTimeout(() => {
        fn.apply(this, args);
        // 定时器结束后重置lastTimestap为0
        lastTimestap = options.leading ? new Date() : 0;
      }, remaining);
    }
  };
}

测试

js 复制代码
const sleep = (time) =>
  new Promise((resolve) => {
    setTimeout(() => {
      console.log(`${time}ms 后`);
      resolve();
    }, time);
  });

const throttleLog1 = throttle((val) => console.log(val), 1000, {
  leading: true,
  trailing: true,
});

const throttleLog2 = throttle((val) => console.log(val), 1000, {
  leading: false,
  trailing: true,
});

const throttleLog3 = throttle((val) => console.log(val), 1000, {
  leading: true,
  trailing: false,
});

const throttleLog4 = throttle((val) => console.log(val), 1000, {
  leading: false,
  trailing: false,
});

// 周期: 0           1            2            3

// 回调: 1         34            2        5

throttleLog1(1);
await sleep(900);
throttleLog1(3);
throttleLog1(4);
await sleep(900);
throttleLog1(2);
await sleep(500);
throttleLog1(5);
// 1
// 900ms 后
// 4
// 900ms 后
// 2
// 500ms 后
// 5

throttleLog2(1);
await sleep(900);
throttleLog2(3);
throttleLog2(4);
await sleep(900);
throttleLog2(2);
await sleep(500);
throttleLog2(5);
// 900ms 后
// 4
// 900ms 后
// 500ms 后
// 5

throttleLog3(1);
await sleep(900);
throttleLog3(3);
throttleLog3(4);
await sleep(900);
throttleLog3(2);
await sleep(500);
throttleLog3(5);
// 1
// 900ms 后
// 900ms 后
// 2
// 500ms 后

throttleLog4(1);
await sleep(900);
throttleLog4(3);
throttleLog4(4);
await sleep(900);
throttleLog4(2);
await sleep(500);
throttleLog4(5);
// 900ms 后
// 900ms 后
// 500ms 后

最后

面试官看到我能如此流畅地实现节流函数,眼前一亮,大声说道:"你这代码写得真是太牛了,快把offer拿走吧!"我终于长出了一口气,靠着扎实的编程技巧,一举拿下了心仪的offer!我开心地站起来准备接过offer,却听到一阵刺耳的闹钟声......原来刚才都是在做梦!不过梦里的奇思妙想还是让我收获颇丰呢!

相关推荐
豆豆41 分钟前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
twins35202 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky2 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~2 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n03 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。3 小时前
案例-任务清单
前端·javascript·css
zqx_74 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己4 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称5 小时前
Pikachu-csrf-CSRF(get)
前端·csrf