防抖、节流、事件总线浅析

68 阅读5分钟

防抖、节流、事件总线浅析

防抖 debouch

防抖

防抖的概念 (类似踢皮球)

  • 防抖是为了解决频繁点击或者频繁触发事件导致频繁执行对应的函数

  • 具体的细节

    • 当事件触发时不会立即执行对应的函数而是等待一段时间
    • 连续触发函数,执行函数会被推迟
    • 等待一段时间没有事件触发,执行对应的函数
  • 应用场景

    • 输入框搜索事件的联想功能
    • 频发点击按钮
    • 滚动条事件(scroll)
    • 窗口缩放(resize)事件

简单版的防抖函数 具体实现

/**
 *
 * @param {*} fn 要执行的函数
 * @param {*} delay  延迟事件
 * @returns
 */
function debouch(fn, delay) {
	let timer = null
	return function (...args) {
		if (timer) clearTimeout(time)
		timer = setTimeout(() => {
			fn.apply(this, args)
			timer = null
		}, delay)
	}
}
  • 实现原理

  • 分析可得防抖函数也一定是返回一个函数给外面调用

  • 一定需要传入一个 延迟时间 和 执行函数

  • 由于防抖的核心是类似踢皮球的形式,只有皮球不动才要触发函数

  • 函数需要被延迟 ---> setTimeout() 函数

  • 皮球滚动的过程又被踢了一脚,球会继续滚动(在函数等待的时间内再次被触发,函数应该被推迟)

  • 其实就是 在下一次执行定时器函数前,先清除上一次的定时器,保证在连续触发的时间段中只存在一个定时器函数,就能保证函数会被一直被推迟

  • 如何清除上一次的定时器,利用闭包原理,在真正返回的函数的上层作用域中第一个变量,用于存储定时器,这样,下一次在执行函数时,由于闭包的特性,timer 并不会被销毁,从而每次都能拿到上一次的 timer,如果 timer 不为 null 也就是说明,在等待的过程中函数多次被触发了,需要先清除定时器,再开启一个新的的定时器。

  • 最后当没有再触发函数时,等待最后一次的 delay 时间,执行函数,完成整个节流的过程,最后在函数执行完将 timer 设置为 null,不然 timer 由于外部的有变量指向返回的函数,返回的函数中又引用了 timer 引用是不会被垃圾回收机制回收。(典型的由于闭包引起的内存泄露原理)

  • 小细节

    • 调用函数的时候应该保持 this 不变,直接调用函数 this 是执行 window(看上次的关于 this 指向的文章)
    • 函数是可以传递参数的
    • 解决 使用 fn.apply(this, args)
  • 存在的不足

    • 第一次输入的时候不会触发函数
    • 在等待的过程中是不可以取消函数
    • 函数如果有返回值无法处理
    • 边界情况暂时不考虑如:传入的 fn 就不是函数
    • 作为工具函数这些应该是要实现的
  • 完整版的 防抖函数

    /**
     *
     * @param {*} fn 要执行函数
     * @param {*} delay  延迟时间
     * @param {*} immediate  第一次是否触发 默认不触发
     * @param {*} callback  执行完的回调函数
     * @returns
     */
    function debouch(fn, delay, immediate = true, callback) {
    	let timer = null
    	let isInvoke = false // 函数是否第一次触发
    	function _debouch(...args) {
    		if (timer) clearTimeout(timer)
    		if (immediate && !isInvoke) {
    			const res = fn.apply(this, args)
    			callback && callback(res)
    			isInvoke = true
    			return
    		}
    		timer = setTimeout(() => {
    			const res = fn.apply(this, args)
    			callback && callback(res)
    			isInvoke = true
    		}, delay)
    	}
    	_debouch.cancel = function () {
    		if (timer) {
    			clearTimeout(timer)
    		}
    	}
    	return _debouch
    }
    

节流函数 throttle

节流

节流函数

节流的概念

  • 节流是为了解决频发触发事件的另一种形式
  • 具体的细节
    • 事件触发会立即执行函数
    • 事件频发触发会按照一定频率执行触发函数
    • 不管在等待时间范围内触发多少次,都按照自己的频率触发函数
  • 应用场景
    • 射击游戏中 子弹的发射
    • 鼠标移动事件
    • 滚动事件

简单版的节流函数 具体实现

/**
 *
 * @param {*} fn  要执行的函数
 * @param {*} intervals  间隔时间
 * @returns
 */
function throttle(fn, intervals) {
	let startTime = 0
	return function (...args) {
		let nowTime = Date.now()
		if (intervals - (nowTime - startTime) <= 0) {
			fn.apply(this, args)
			startTime = nowTime
		}
	}
}
  • 实现的原理

    • 分析可得节流函数也一定是返回一个函数给外面调用
    • 按照一定频率的执行函数 ----> 一定需要传入一个 间隔时间 和执行函数
    • 计算时间差,获取当前系统时间,记录上一次函数开始执行的时间
    • 如果 间隔时间 - (系统时间 - 上一次函数开始执行的时间)<=0 代表 时间已经超过了 间隔时间 函数应该要执行了
    • 执行完函数 ,上一次函数执行的时间应该是 更新为 nowTime 因为 nowTime 是 在执行函数时就获取的系统时间戳 刚好就是函数开始执行的时间
    • 同样的 利用闭包的原理 将 startTime 保留 ,这样下一次执行函数用到的 startTime 才是准确的。具体立即看图就可以理解了
  • 小细节

    • 调用函数的时候应该保持 this 不变,直接调用函数 this 是执行 window(看上次的关于 this 指向的文章)
    • 函数是可以传递参数的
    • 解决 使用 fn.apply(this, args)
  • 函数存在的不足

    • 第一次函数必定是触发的 intervals - (nowTime - startTime) <= 0 nowTime 是一个非常大的大的数 表达式必定是小于 0 的。
    • 最后一次是没有触发的
    • 在等待的过程中是不可以取消函数
    • 函数如果有返回值无法处理
    • 边界情况暂时不考虑如:传入的 fn 就不是函数
  • 完整版的 节流函数

/**
 *  节流函数
 * @param {*} executorFn 要执行的函数
 * @param {*} intervals  每次间隔的时间
 * @param {*} options (leading/tailing)   leading 第一次 要不要触发 默认 要   tailing 最后一次要不要触发 默认 不触发
 * @param {*} callback   回调函数
 * @returns
 */
function throttle(
	executorFn,
	intervals = 3000,
	options = { leading: true, tailing: true },
	callback
) {
	let startTime = 0
	const { leading, tailing } = options
	let timer = null
	function _throttle(...args) {
		const nowTime = Date.now()
		// 实现第一次不会触发函数
		//  利用 startTime 判断是否是第一次执行函数
		//  直接将 startTime 改为 nowTime 下面的 if 就不会执行 达到第一次不会触发的效果
		if (!startTime && !leading) startTime = nowTime
		// 计算时间差
		const radomTime = intervals - (nowTime - startTime)
		if (radomTime <= 0) {
			// 有执行函数 都不应该添加定时器 如果有就清除掉
			if (timer) {
				clearTimeout(timer)
				timer = null
			}
			// 执行函数
			const res = executorFn.apply(this, args)
			startTime = nowTime
			callback && callback(res)
			//  有执行函数 都不应该添加定时器 直接return 就不会执行下面的代码了
			return
		}
		// 最后一次触发
		// 拿到 剩余时间 就是 radomTime 自己画图即可理解
		// 设置定时器
		// 设置 timer 保证只添加一次定时器
		if (tailing && !timer) {
			timer = setTimeout(() => {
				// 重置 防止 下次执行 无法添加定时器
				timer = null
				// startTime 的重置和 选择第一次要不要执行有关
				// 第一次要执行 startTime 就为重置为 Date.now()
				// 第一次不要执行 startTime 就为重置为 0
				startTime = !leading ? 0 : Date.now()
				const res = executorFn.apply(this, args)
				callback && callback(res)
			}, radomTime)
		}
	}
	// 取消函数
	_throttle.cancel = function () {
		if (timer) {
			clearTimeout(timer)

			timer = null
			startTime = 0
		}
	}
	return _throttle
}

事件总线 的简单实现

  • 核心方法

    • on 注册事件
    • off 注销事件
    • emit 触发事件
  • 存储事件结构

    • 同一个 事件名 可以拥有多个触发事件 触发的事件存储可以采用数组
    • 可以根据事件名称找到 对应的事件数组 采用对象 或者 Map 都行
    this.eventFns = {
    
       eventName1: [{callback1,this }, {callback2,this} ]
       eventName2: [{callback1,this }, {callback2,this} ]
    
    }
    
    class MYEventBus {
    	constructor() {
    		this.eventFns = {} // 存储事件
    	}
    	on() {}
    	off() {}
    	emit() {}
    }
    
    • on 方法
      • 注册事件 肯定要有传递一个事件名称 和 对用的触发事件 严谨一点吧 this 也传进来
    
      on(eventNam, callback, thisArgs) {
    		let handlers = this.eventFns[eventNam] // 找到对应的事件数组
    		if (!handlers) { //不存在就新建一个数组
    			handlers = []
    			this.eventFns[eventNam] = handlers
    		}
    		handlers.push({   // 把事件和事件的this  放到对应的数组中  形成的结构  eventName: [{callback,thisArgs },..... ]
    			callback,
    			thisArgs,
    		})
    	}
    
    • emit 方法
      • 接受一个 事件名称 和 一些参数
    
    	emit(eventNam, ...args) {
    		const handlers = this.eventFns[eventNam] // 找到对应的事件数组
    		if (!handlers) return // 没有 说明 还有没事件  根本无法触发事件
    		handlers.forEach((handler) => { // 存在事件 对数组循环 依次触发事件
    			handler.callback.apply(handler.thisArgs, args)
    		})
    	}
    
    • off 方法
      • 接受一个 事件名称 和 要注销的事件名称
    
    	off(eventNam, callback) {
    		const handlers = this.eventFns[eventNam] // 找事件
    		if (!handlers) return // 没有 说明 还有没事件  无法删除一个不存在的事件
    		this.eventFns[eventNam] = handlers.filter(  // 有对应事件存在 一个一个删除找到的事件 或者直接把存在的事件过滤掉 对 事件数组重新赋值
    			(handler) => handler.callback !== callback
    		)
    	}
    

  • 完成代码

    class MYEventBus {
    	constructor() {
    		this.eventFns = {}
    	}
    	on(eventNam, callback, thisArgs) {
    		let handlers = this.eventFns[eventNam]
    		if (!handlers) {
    			handlers = []
    			this.eventFns[eventNam] = handlers
    		}
    		handlers.push({
    			callback,
    			thisArgs,
    		})
    	}
    	emit(eventNam, ...args) {
    		const handlers = this.eventFns[eventNam]
    		if (!handlers) return
    		handlers.forEach((handler) => {
    			handler.callback.apply(handler.thisArgs, args)
    		})
    	}
    	off(eventNam, callback) {
    		const handlers = this.eventFns[eventNam]
    		if (!handlers) return
    		this.eventFns[eventNam] = handlers.filter(
    			(handler) => handler.callback !== callback
    		)
    	}
    }
    
    const evt = new MYEventBus()
    function fn(vale) {
    	console.log('on fn1', vale, this)
    }
    function fn2() {
    	console.log('on fn2')
    }
    evt.on('abc', fn, 'this')
    evt.on('abc', fn2)
    evt.emit('abc', 111)
    
    evt.off('abc', fn2)
    
    evt.emit('abc', 111)