JavaScript高级-防抖、节流、深拷贝、事件总线

79 阅读4分钟

一、防抖

1. 认识防抖和节流函数

  • 防抖和节流的概念其实最早并不是出现在软件工程中,防抖是出现在电子元件中,节流出现在流体流动中
  • 而JavaScript是事件驱动的,大量的操作会触发事件,加入到事件队列中处理
  • 而对于某些频繁的事件处理会造成性能的损耗,我们就可以通过防抖和节流来限制事件频繁的发生

2. 认识防抖debounce函数

42_防抖函数.jpg

  • 当事件触发时,响应的函数并不会立即触发,而是会等待一定的时间
  • 当事件密集触发时,函数的触发会被频繁的推迟
  • 只有等待了一段时间也没有事件触发,才会真正的执行响应函数

3. 防抖的应用场景

  • 输入框中频繁的输入内容,搜索或者提交信息
  • 频繁的点击按钮,触发某个事件
  • 监听浏览器滚动事件,完成某些特定操作
  • 用户缩放浏览器的resize事件

4. 防抖函数实现

<input type="text">
<button>取消</button>
  • 基本实现
function hydebounce(fn, delay) {
  // 1. 用于记录上一次事件触发的timer
  let timer = null

  // 2. 触发事件时执行的函数
  const _debounce = () => {
    // 2.1 如果有再次触发(更多次触发)事件,那么取消上一次的事件
    if (timer) clearTimeout(timer)

    // 2.2 延迟去执行对应的fn函数(传入的回调函数)
    timer = setTimeout(() => {
      fn()
      timer = null // 执行函数之后,将timer重新置null
    }, delay);
  }
  // 返回一个新的函数
  return _debounce
}

// 1. 获取input元素
const inputEl = document.querySelector("input")
// 2. underscore防抖处理代码
// let counter = 1
// inputEl.oninput = _.debounce(function() {
//    console.log(`发送网络请求${counter++}`, this.value)
// }, 1000)
// 3. 自己实现的防抖
let counter = 1
inputEl.oninput = hydebounce(function() {
   console.log(`发送网络请求${counter++}`, this.value)
}, 3000)
  • 独立封装实现

function hydebounce(fn, delay, immediate = true) {
  // 1. 用于记录上一次事件触发的timer
  let timer = null
  let isInvoke = false

  // 2. 触发事件时执行的函数
  const _debounce = function(...args) {
    return new Promise((resolve, reject) => {
      // console.log("_debounce的this:", this);
      try {
        // 2.1 如果有再次出发(更多次触发)事件,那么取消上一次的事件
        if (timer) clearTimeout(timer)

        // 第一次操作是不需要延迟的
        let res = undefined
        if (immediate && !isInvoke) {
          res = fn.apply(this, args)
          resolve(res)
          isInvoke = true
          return
        }

        // 2.2 延迟去执行对应的fn函数(传入的回调函数)
        timer = setTimeout(() => {
          res = fn.apply(this, args)
          resolve(res)
          timer = null // 执行函数之后,将timer重新置null
          isInvoke = false
        }, delay);
      } catch (error) {
        reject(error)
      }
    })
  }

  // 3. 给_debounce绑定一个取消的函数
  _debounce.cancel = function() {
    if (timer) clearTimeout(timer)
    timer = null 
    isInvoke = false
  }
  // 返回一个新的函数
  return _debounce
}

二、节流

1. 认识节流throttle函数

42_节流函数.jpg

  • 当事件触发时,会执行这个事件的响应函数
  • 如果这个事件被频繁触发,那么节流函数会按照一定的频率来执行函数
  • 不管在这个中间有多少次触发这个事件,执行函数的频率总是固定的

2. 节流函数的应用场景

  • 监听页面的滚动事件
  • 鼠标移动事件
  • 用户频繁点击按钮操作
  • 游戏中的一些设计

3. 节流函数的实现

<button>按钮</button>
<input type="text">
  • 基本实现
function hythrottle(fn, interval) {
  let startTime = 0
  const _throttle = function() {
    const nowTime = new Date().getTime()
    const waitTime = interval - (nowTime - startTime)
    if (waitTime <= 0) {
      fn()
      startTime = nowTime
    }
  }

  return _throttle
}


// 1. 获取input元素
const inputEl = document.querySelector("input")

// 2. underscore节流处理代码
// let counter = 1
// inputEl.oninput = _.throttle(function() {
//   console.log(`发送网络请求${counter++}:`, this.value);
// }, 1000)

// 3. 自己实现的节流函数
let counter = 1
inputEl.oninput = hythrottle(function() {
  console.log(`发送网络请求${counter++}:`, this.value);
}, 1000)
  • 独立封装实现
function hythrottle(fn, interval, { leading = true, trailing = false } = {}) {
  let startTime = 0
  let timer = null

  const _throttle = function(...args) {
    return new Promise((resolve, reject) => {
      try {
         // 1.获取当前时间
        const nowTime = new Date().getTime()

        // 对立即执行进行控制
        if (!leading && startTime === 0) {
          startTime = nowTime
        }

        // 2.计算需要等待的时间执行函数
        const waitTime = interval - (nowTime - startTime)
        if (waitTime <= 0) {
          // console.log("执行操作fn")
          if (timer) clearTimeout(timer)
          const res = fn.apply(this, args)
          resolve(res)
          startTime = nowTime
          timer = null
          return
        } 

        // 3.判断是否需要执行尾部
        if (trailing && !timer) {
          timer = setTimeout(() => {
            // console.log("执行timer")
            const res = fn.apply(this, args)
            resolve(res)
            startTime = new Date().getTime()
            timer = null
          }, waitTime);
        }
      } catch (error) {
        reject(error)
      }
    })
  }

  _throttle.cancel = function() {
    if (timer) clearTimeout(timer)
    startTime = 0
    timer = null
  }

  return _throttle
}

三、深拷贝

  • JSON方法
const info = {
  name: "why",
  age: 18,
  friend: {
    name: "kobe"
  },
  running: function() {}
}
const obj = JSON.parse(JSON.stringify(info))
  • 自己实现
function deepCopy(originValue, map = new WeakMap) {

  // 0. 如果值是Symbol类型,创建一个新的Symbol
  if (typeof originValue === "Symbol") {
    return Symbol(originValue.description)
  }

  // 1. 如果是原始类型,直接返回
  if (isObject(originValue)) {
    return originValue
  }

  // 2. 如果是set类型
  if (originValue instanceof Set) {
    const newSet = new Set()
    for (const setItem of originValue) {
      newSet.add(deepCopy(setItem, map))
    }
    return newSet
  }

  // 3. 如果是函数类型,不要进行深拷贝
  if (typeof originValue === "function") {
    return originValue
  }

  // 4. 如果是对象类型,才需要创建对象
  if (map.get(originValue)) {
    return map.get(originValue)
  }
  const newObj = Array.isArray(originValue) ? [] : {}
  map.set(originValue, newObj)
  for (const key in originValue) {
    newObj[key] = deepCopy(originValue[key], map)
  }

  // 单独遍历Symbol
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  for (const symbolKey of symbolKeys) {
    newObj[Symbol(symbolKey.description)] = deepCopy(originValue[symbolKey], map)
  }

  return newObj
}


const set = new Set(["abc", "cba", "nba"])
const s1 = Symbol()
const s2 = Symbol()
const info = {
  name: "why",
  age: 18,
  friend: {
    name: "kobe",
    address: {
      name: "洛杉矶",
      detail: "斯坦基普中心"
    }
  },
  // 1. 特殊类型:Set
  set: set,
  // 2. 特殊类型:function
  running: function() {
    console.log("running~");
  },
  // 3. 值的特殊类型:Symbol
  symbolKey: Symbol(),
  // 4. key的特殊类型:Symbol
  [s1]: "aaa",
  [s2]: "bbb"
}
info.self = info

const newObj = deepCopy(info)

四、事件总线

  • 代码实现
// 类EventBus -> 事件总线对象
class HYEventBus {
  constructor() {
    this.eventMap = {}
  }

  on(eventName, eventFn) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) {
      eventFns = []
      this.eventMap[eventName] = eventFns
    }
    eventFns.push(eventFn)
  }

  off(eventName, eventFn) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return
    for (let i = 0; i < eventFns.length; i++) {
      const fn = eventFns[i]
      if (fn === eventFn) {
        eventFns.splice(i, 1)
        break
      }
    }

    if (eventFns.length === 0) {
      delete this.eventMap[eventName]
    }
  }

  emit(eventName, ...args) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return 
    eventFns.forEach(fn => {
      fn(...args)
    })
  }
}

// 使用过程
const eventBus = new HYEventBus()

// aside.vue组件中监听事件
eventBus.on("navclick", () => {
  console.log("navclick listener 01");
})

// nav.bue
const navBtnEl = document.querySelector(".nav-btn")
navBtnEl.onclick = function() {
  eventBus.emit("navclick")
}