前端面试100道手写题(2)—— throttle与debounce

简介: 前端面试100道手写题第二篇《throttle与debounce》,说一下为什么选这两个,其实大家都有在用

背景


前端面试100道手写题第二篇《throttle与debounce》,说一下为什么选这两个,其实大家都有在用,我们先来了解一下两个函数的作用:

  • debounce 防抖,用于减少函数触发的频率,在一个delay时间内,如果触发delay时间归零,直到delay时间到才会触发函数
  • throttle 节流,用于限制函数触发的频率,每个delay时间间隔,最多只能执行函数一次


选这两个手写的原因其实很简单,就是面试频率高,而且项目实战会经常用到,同时里面还会隐藏一些知识点和目前公共库的一些问题(见文章末尾)。


接下来就让我们开始手写撸代码吧!


手写难度:⭐️⭐️⭐️


不想看啰里吧嗦的文字,可以直接去看完整源码地址(记得给个star),地址如下:

github.com/qiubohong/h…

debounce


很多功能函数只要搞清楚他们的功能设计,基本上你就可以手写出完整的代码。 以lodash.debounce为参考,接下来我们来拆解一下完整的debounce的功能具体有哪些:

  • 构造函数 debounce(func, waitTime, maxWait, leading, trailing)
  • func (Function): 要防抖的函数。
  • [wait=0] (number): 需要防抖的毫秒。
  • [leading=false] (boolean): 指定调用在防抖开始前。
  • [trailing=true] (boolean): 指定是否在最大等待时间过期后直接调用,简单点的当超过等待时间,则会触发函数
  • [maxWait=wait] (number): 设置最大等待时间过期。
  • 取消函数 debounceReturn.cancel()debounceReturn是执行完debounce函数返回的对象
  • 状态函数 debounceReturn.pending()
  • 立即调用函数 debounceReturn.flush()

上面是lodash给出debounce的完整功能,但是如果是我们仅仅需要简易版本的throttle,应该如何实现呢?


拆解步骤一:实现一个简单版


debounce()函数最简单功能就是,希望能在wait时间段禁止重复触发某个事件,第一个简易版如下:

function debounce_easy(func, waitTime){
    // 用于存储定时器
    let timeout;
    // 存储返回结果
    let result;
    return function(){
        let context = this;
        let args = arguments;
        // 如果定时器存在,就清除定时器
        clearTimeout(timeout);
        // 重新设置定时器
        timeout = setTimeout(function(){
            // 执行函数,将当前作用域绑定的this和参数传递过去
            result = func.apply(context, args);
        }, waitTime);
    }
}
// 单元测试
const debounced = debounce_easy(function (value) {
    console.log('debounce_easy:', value)
    ++callCount;
    return value;
}, 32);
// 这里等同于快速触发4次,只有最后一次生效 输出 debounce_easy: d
const results = [debounced('a'), debounced('b'), debounced('c'), debounced('d')];
let lodashResults = [lodashDebouce('a'), lodashDebouce('b'), lodashDebouce('c'), lodashDebouce('d')];
// callCount: 0
console.log('callCount:', callCount)
setTimeout(function () {
    // callCount: 1
    console.log('callCount:', callCount)
}, 160);

拆解步骤二:leading参数希望可以先执行一次函数,再进行防抖, 具体代码如下:

// 其实就是在定时器之前判断 leading和 timeout定期器是否不为空即可 关键代码如下
// 如果leading为true,就立即执行函数
if (leading) {
    // 如果定时器不存在,就执行函数,从而避免重复执行
    if (!timeout) {
        invokeFunc();
    }
}

拆解步骤三:加上cancel等函数实现,这里能实现完基本上手写题就80分了

/**
 * 取消防抖
 */
const cancel = function () {
    clearTimeout(timeout)
    lastArgs = lastThis = timeout = undefined
}

拆解步骤四:加上参数 traling+maxWait 函数,作用在当超过maxWait等待时间后,函数会


重点在于判断是否过了等待时间,所以需要记录每次执行的时间,当超过的时候判断是否有传参数traling+maxWait


实现步骤如下流程所示:

56b0e7aabbd549c78984d1301e7b242.png

参数解释:

  • lastArgs,  // 上一次调用时的参数
  • lastThis,  // 上一次调用时的this
  • result,  // 上一次调用的返回值
  • lastCallTime,  // 上一次调用的时间
  • lastInvokeTime = 0,  // 上一次执行的时间
  • leading = false,  // 是否立即执行
  • maxing = false,  // 是否有最大等待时间
  • trailing = true; // 是否在最后一次调用后执行

throttle


throttle节流函数定义:就是无论频率多快,每过一段时间就执行一次。


在实现逻辑上其实是可以看做debounce的一种升级版,只需要保证debounce函数在超时后执行一次函数即可


只要针对debouonce函数设置一下参数即可,代码如下:

function throttle(func, wait, leading = true){
    return debounce(func, wait, {
        leading,
        trailing: true,
        'maxWait': wait // 超时时间和控制时间一致就可以了
    })
}

当然我们也可以实现一个快速简单版,代码如下:

function throttle_eazy(func, wait) {
    let timer = null;
    let lastInvokeTime = 0;
    return function () {
        const context = this;
        const args = arguments;
        function invokeFunc() {
            func.apply(context, args);
            lastInvokeTime = Date.now();
            timer = null;
        }
        wait = +wait || 0;
        // 计算剩余时间
        let remainTime = wait;
        // 如果上次执行时间大于0,说明已经执行过了,计算剩余时间
        if(lastInvokeTime > 0){
            remainTime = wait - (Date.now() - lastInvokeTime);
        }
        // 如果剩余时间小于等于0,说明可以执行了,重置上次执行时间
        if (remainTime <= 0) {
            invokeFunc();
            return;
        }
        // 如果已经开始计时,说明已经有定时器了,直接返回
        if (timer) {
            return;
        }
        // 否则,开始计时
        timer = setTimeout(() => {
            invokeFunc();
        }, remainTime);
    }
}
// 单元测试
; (function () {
    let count = 0;
    const throttled = throttle(() => {
        count++;
        console.log('hello', count);
    }, 200);
    for (let i = 1; i <= 20; i++) {
        setTimeout(() => {
            console.log('触发i~', i * 100)
            throttled();
        }, 100 * i);
    }
    setTimeout(() => {
        // 正确输出10
        console.log(count);
    }, 2200);
})()

额外知识点


TDD开发模式


一般写这些工具函数,都需要提前想好单元测试怎么写,这就是涉及一种开发模式测试驱动开发(TDD),主要遵循以下两个原则:

  • 仅在自动测试失败时才编写新代码。
  • 消除重复设计(去除不必要的依赖关系),优化设计结构(逐渐使代码一般化)。


TDD的研发流程如下:

be16b3d47a401cc91e67ae3e7f6cb1f.png

lodash的缺陷


如果你正在使用lodash,你应该关注一下,因为lodash的github最后一次更新2021年4月24号,到目前为止已经有两年的时间没有更新,已经堆积很多issues,从上面解读源码的时候就发现一个lodash.throttle的一个bug,如下:

const changeInput = throttle((value: string)=>{
  console.log(value);
}, 1000, {
  leading: false,
  trailing: false,
})
// 上述防抖函数将不会按照我们所设想的每隔1秒触发,而是会出现各种异常情况
// 如果触发频率够高可能会执行,如果触发频率低于1秒则不会执行
// 因为leading和trailing都设置为false
// lodash源码没有针对这一情况进行处理,解决方案是 throttle不支持trailing参数设置即可,默认为true

同时,lodash还有其他一些缺陷:

  • lodash是支持tree shaking,但是这么写import {throttle} from 'lodash' 会将整个lodash包都引入, 必须这么写import throttle from 'lodash/throttle'才能做到按需加载
  • 进入 npm 上的 lodash 包,它被列为 v4.17.21,并且已经 2 年多没有发布了:www.npmjs.com/package/lod…


当然作为一个工具库lodash确实可以让我们少写很多代码,但是已经很长时间没有维护的问题还是需要关注的。

参考资料


目录
相关文章
|
1月前
|
存储 前端开发 JavaScript
前端面试题23-34
通过对 Promise 和 ECMAScript6 的深入理解,可以更好地应对现代 JavaScript 开发中的复杂异步操作和新特性,提升代码质量和开发效率。
21 2
|
2月前
|
缓存 JavaScript 前端开发
2024 前端高频面试题之 Vue 篇
2024 前端高频面试题之 Vue 篇
56 8
|
2月前
|
前端开发 JavaScript Java
2024高频前端面试题合集(一)
JavaScript Bridge 是一种在 JavaScript 和其他语言(如 Java、Objective-C 等)间建立通信的技术,常用于混合应用开发,允许调用原生功能、获取数据、事件通知及优化性能。SSR(服务器端渲染)的单机 QPS 取决于服务器性能、应用复杂度、网络条件等因素。Egg.js 是基于 Node.js 的企业级框架,通过目录结构约定、启动流程、插件机制和核心组件来初始化应用。前端错误捕获可通过 try-catch、window.onerror、Promise.catch 和 unhandledrejection 事件等方式实现。
|
3天前
|
缓存 前端开发 JavaScript
高级前端常见的面试题?
【7月更文挑战第11天】 **高级前端面试聚焦候选人的技术深度、项目实战、问题解决及技术趋势洞察。涉及React/Vue生命周期、Redux/Vuex状态管理、Webpack优化、HTTP/HTTPS安全、性能提升策略、PWA、GraphQL、WebAssembly、安全性议题及项目管理。通过回答,展现候选人技术广度与应对复杂场景的能力。**
13 1
|
4天前
|
移动开发 前端开发 JavaScript
前端常见的面试题都有那些?
【7月更文挑战第10天】 前端面试涵盖HTML5新特性、CSS盒模型、JS事件传播、Vue的双向绑定、React生命周期、性能优化策略、浏览器解析流程及安全知识等。例如,HTML5新增video/audio元素、CSS选择器优先级计算、闭包功能、async/await处理异步、Vue通过Object.defineProperty实现数据绑定、React组件生命周期的关键阶段、前端优化如CDN和资源压缩,以及浏览器如何构建渲染树。面试还可能涉及XSS/CSRF防护和框架选择考量。准备面试需全面理解基础概念并结合实践经验。
8 0
|
28天前
|
存储 缓存 监控
2024春招小红书前端面试题分享
2024春招小红书前端面试题分享
53 3
|
1月前
|
前端开发 JavaScript 虚拟化
前端面试题12-22
ES6(ECMAScript 2015)是 JavaScript 的重要版本,引入了许多新特性和语法,提升了语言的功能和可用性。ES6 的主要特性包括箭头函数、类、模板字符串、解构赋值、默认参数、Promise、模块化、Generator 函数、async 函数、Proxy 和 Reflect 等。这些特性不仅简化了代码的编写和维护,还为开发者提供了更多的编程范式和工具。了解和掌握 ES6 的特性是现代 JavaScript 开发的必备技能。
12 1
|
1月前
|
JSON 前端开发 JavaScript
前端面试题01-11
Map是ES6引入的一种新的键值对集合数据结构,类似于对象,但键的范围不限于字符串,还可以是任何类型的值。Map保持键值对的插入顺序,提供更灵活的键值对操作方法,如`set()`、`get()`、`delete()`、`has()`等。
16 1
|
13天前
|
JavaScript 前端开发
JS进阶篇(前端面试题整合)(三)
JS进阶篇(前端面试题整合)(三)
13 0
|
18天前
|
Web App开发 存储 前端开发
技术心得记录:前端面试题汇总
技术心得记录:前端面试题汇总