面试八股:手写防抖与节流

1,001 阅读4分钟

防抖和节流是面试常被问到的问题,因为里面包含了比如闭包,this指向,函数传参,箭头函数等知识点,那么防抖和节流就是防止用户一次性点击很多下,一直发请求,比如登录页面按钮,连点很多下就会发送很多没有意义的请求,所以就可以通过防抖与节流来优化。

  • 防抖:在规定的时间内如果没有二次触发行为,则执行,否则放弃上一次的事件行为,从当前行为开始重新计时。
  • 节流就是节省流量,人为的规定去每隔一秒发送一次请求,比如用户在10秒之内点击了100次的话,也只会发送10次请求。

手写防抖

手写防抖原理就是监听点击事件,给他设置一个定时器,让它在规定的时间内不会触发第二次,如果触发了第二次,则取消前一次的点击逻辑,就可以实现防抖效果了。如下;

<body>
  <button id="btn">提交</button>

  <script>
    const btn = document.getElementById("btn");
    
    function handle() {
      console.log('向后端发请求');
      // console.log(this); // 这里的this将指向btn元素
    }
    
    btn.addEventListener("click", debounce(handle, 1000))

    function debounce(fn, wait) {     // 防抖
      let timer = null
      return function () {
        if (timer) clearTimeout(timer)   // 如果定时器没执行完就清除原来的重新开始计时
        timer = setTimeout(() => {
          fn()   // 函数handle执行  为为了不改变this指向 -> fn.call(this)
        }, wait)
      }
    }
  </script>
</body>

但是还需要注意的是这样就会修改handle里面的this指向,本来handle函数里面的this是通过隐式绑定(非独立调用)在btn这个dom对象上的,也就是指向 btn dom结构,但是如上所示,将handle()函数拿到 setTimeout 里面来调用,这时候函数为独立调用,所以默认指向全局的window对象。

为了解决这个问题,不能修改原函数的 this 指向,handle函数执行的时候可以将this指向原来的 btn,于是将fn() ---> fn.call(this),this 的指向就取决于 fn.call(this) 被调用时的上下文。在防抖函数 debounce 的场景中,this 指向触发事件的元素 btn。

但是如果函数里面有事件参数,也就是当函数被绑定在一个事件上执行时,就一定会具有一个形参,用来描述当前的事件详情。

<body>
  <button id="btn">提交</button>

  <script>
    const btn = document.getElementById("btn");

    function handle(e) {
      console.log('向后端发请求');
      console.log(e);
    }

    btn.addEventListener("click", handle)
    </script>
</body>

image.png

所以后面要保证事件参数还是原来的事件参数,还需要注意的是如果 handle 原函数里面后面还有参数时,还需要考虑到传参数的问题。通过...args接收所有的参数变成一个数组,再将这个数组结构到fn也就是handle函数的形参里面。如下;

<body>
  <button id="btn">提交</button>

  <script>
    const btn = document.getElementById("btn");


    function handle(e, a, b) {
      console.log('向后端发请求');
      console.log(e);

    }

    btn.addEventListener("click", debounce(handle, 1000))

    function debounce(fn, wait) {     // 防抖
      let timer = null
      return function (...args) {
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
          fn.call(this, ...args)
        }, wait)
      }
    }
  </script>
</body>

这样就比较完美地实现了防抖效果。

手写节流

节流就是在规定的时间内只执行一次,比如,人为的规定去每隔一秒发送一次请求,比如用户在10秒之内点击了100次的话,也只会发送10次请求

主要思路就是点击一次后,记录下来此时的时间节点,点第二次的时候看是否与前一次间隔有没有1s,如果有1s就触发,否则不触发。

获取时间可以使用Date.now()image.png

代码如下:

<body>
  <button id="btn">提交</button>

  <script>
    let btn = document.getElementById('btn');

    function handle() {
      console.log('向后端发送请求');
    }

    btn.addEventListener('click', throttle(handle, 1000));

    function throttle(fn, wait) {
      let preTime = null
      return function () {
        let nowTime = Date.now()
        if (nowTime - preTime >= wait) {
          fn()
          preTime = nowTime
        }
      }
    }
  </script>
</body>

然后考虑原函数的this指向,原函数的事件参数,完整代码如下。

<body>
  <button id="btn">提交</button>

  <script>
    let btn = document.getElementById('btn');

    function handle(e, a, b) {
      console.log('向后端发送请求');
      console.log(e);
      console.log(this);
    }

    btn.addEventListener('click', throttle(handle, 1000));

    function throttle(fn, wait) {
      let preTime = null
      return function (...args) {
        let nowTime = Date.now()
        if (nowTime - preTime >= wait) {
          fn.call(this, ...args)
          preTime = nowTime
        }
      }
    }
  </script>
</body>

自定义一个防抖指令

全局定义指令分为:

  • 定义指令
  • 注册指令 (vue 认可这个指令) 定义install函数,调用install函数,通过 app.directive(name, options) directive 会触发 options 中的钩子函数。
  • 调用指令 入口文件中Vue.use()调用,会调用 install 函数

举个例子:写一个节流的自定义指令

  1. 定义全局自定义指令:
// command/debounce.js 
export default {
  // el:该指令绑定在哪个元素上
  // binding:指令的相关信息,包含该指令绑定的值
  mounted(el, binding) {
    let timer = null;

    el.addEventListener('click', () => {
      if (timer) clearTimeout(timer)  // 如果还存在定时器,则清除它重新计时

      timer = setTimeout(() => {
        binding.value();      // 当计时器执行完后,再执行绑定的事件
        timer = null;
      }, 2000)
    })
  }
};

image.png

  1. 注册全局自定义指令:
// command/index.js  
// 引入指令代码
import debounce from "./debounce.js";

const directive = {    // 汇总指令
  debounce,
}

export default {   
  install(app) {     // app === vue 实例 已安装的方式插到app中
    // 注册指令
    Object.keys(directive).forEach(key => {  // 找出 key
      app.directive(key, directive[key])   // 注册指令  两个参数 (名字,指令代码)
    })
  }
}
  1. 引入指令并调用全局自定义指令
// mian.js
import { createApp } from 'vue'
import App from './App.vue'
import Directives from '@/command/index.js'

const app = createApp(App)
app.use(Directives)     // 全局注册   // 注册指令调用了 install 函数
  1. 使用自定义指令
<template>
  <button v-debounce="handle"></button>
</template>

<script setup>
const handle=> (){
  console.log('xxxxxx');
}
</script>

好了,关于防抖与节流就介绍到这里了。