龙空技术网

从节流和防抖说起(一)

前端咖咖 167

前言:

现在朋友们对“css3向下箭头”大约比较关切,小伙伴们都需要知道一些“css3向下箭头”的相关资讯。那么小编同时在网络上网罗了一些对于“css3向下箭头””的相关内容,希望你们能喜欢,朋友们快快来学习一下吧!

在你职业生涯中,一定有人问过你节流和防抖。在这里我冒昧猜测,绝大多数人可能只是简短的说了下概念,讲个七八句就结束了。其实,如果细说可以由此展开几个知识点,同时,也向想要了解你技术的人展示你掌握知识的扎实程度。

进入正题,阅读本文你将会对防抖、节流,函数apply、call和bind,以及事件循环相关概念等有更深入的了解,对你以往的知识也能起到融会贯通的作用,希望本文不失众望,Let's GO。

我们新建一个测试文件

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>节流与防抖</title></head><body><button id="buy">buy</button><button id="buy1">buy1</button></body><script type="application/javascript">function debounce(fn, delay = 500) {    let timer = null;    return () => {        if (timer) {        clearTimeout(timer);				}        timer = setTimeout(() => {            fn();            clearTimeout(timer); // 及时清除            timer = null; // 垃圾回收变量        }, delay)		}}function buy(param1) {		console.log('buy ... ', param1)}function buy1() {		console.log('buy 1 ... ')}document.getElementById('buy').addEventListener('click', debounce(buy))const throttle = (f, delay) => {    let timer = 0;    return () => {        if (timer) return        timer = setTimeout(() => {            f();            clearTimeout(timer)            timer = 0        }, delay)    }}document.getElementById('buy1').addEventListener('click', throttle(buy1))</script></html>
关于节流和防抖

核心是闭包和延时器的应用。更深层次会涉及到变量作用域,宏任务,以及内存泄露的问题。所以展开说,每一行代码都有深意,意味着我们在实际开发中如何规范的开发。<br>

为什么要深入,因为我们要走的更远更规范,试想一下,公司的一个产品交给你,像某猫级别的,庞大的代码体系中存在着代码不规范,内存泄露等问题,日积月累,整个产品还能存活吗?凭借产品生存的公司,以及为公司勤勤恳恳奉献的员工都将面临风险,这将是整个产品、公司的悲哀,所以为了自己能承担更重要的事情,我们必须要搞清楚更深层次的原因。

故不积跬步,无以至千里;不积小流,无以成江海。

手写一个防抖

function debounce (fn, delay = 500) {  let timer = null;  return () => {    if (timer) {      clearTimeout(timer);    }    timer = setTimeout(() => {      fn();    }, delay)  }}

我们运行上面的代码,触发事件,再触发几次,然后再刷新,再触发事件,然后周而复始,重复操作,得到下图的性能图

你可能对上图还不是很敏感,看不出到底有没有内存泄露

接下来我们写一段内存泄漏的代码,我们看看泄露的情况又是什么样子呢,相隔时间去手动刷新页面

function Tack() {  let index = 0  return () => {    index++    return index  }}let t1 = new Tack()console.log(t1())

在上面代码再加一个置空的代码

function Tack() {  let index = 0  return () => {    index++    return index  }}let t1 = new Tack()console.log(t1())t1 = null

通过上面的对比,不难发现,发生内存泄露的代码,图表呈现递增趋势,不是一个起伏的状态。所以最开始的防抖的代码并没有导致内存泄露。

上面的代码是完整的吗?我们看下,再想想。

你能想出哪些问题,或者说是,基于这个代码你还能说出哪些问题呢?我在这里罗列一下,看看你和我想的是否一样呢?

接下来我们将讲到哪些内容?

是否有内存泄露嫌疑,如何分析内存泄露?如果fn有参数,如何处理setTimeout的返回值有0、1的可能吗?这个关系到if判断的严谨性和对setTimeout的熟悉层度如果把箭头函数换成function,如何写?timer什么时候才会被垃圾回收清除?关系到闭包的变量的生命周期关于宏任务的执行时机,关系到微任务,事件循环,消息队列,延迟队列,事件触发的两种机制事件捕获和事件冒泡的话题闭包的写法和结构特点有没有让你想到es6的class,闭包不能滥用,使用不当会导致内存泄露,但是class的写法非常相似于闭包,那es6使用class有内存泄露风险吗?和function的区别是什么?一、是否有内存泄露嫌疑,如何分析内存泄露?

经过上面的分析,我们检测浏览器运行代码的过程,发现堆栈和事件占用的空间是起伏态势,说明是可释放,也就是是被垃圾回收的。(PS:但是实际情况是我们并没有主动销毁防抖函数debounce的引用,是如何做到垃圾回收的?)

接下来我们继续对防抖函数深入处理优化,看看他的性能是否有所改变。

function debounce (fn, delay = 500) {  let timer = null;  return () => {    if (timer) {      clearTimeout(timer);    }    timer = setTimeout(() => {      fn();      clearTimeout(timer); // 及时清除      timer = null; // 垃圾回收变量    }, delay)  }}

在这个单例中,我们看不到他的影响,在现代浏览器中,基本没什么太大的影响,但是当实际应用中,庞大的代码中还是需要注意垃圾回收引用的问题,这样完善的做法一定是正确的。

二、如果fn有参数,如何处理

function debounce(fn, delay = 500) {  let timer = null;  return function () {    if (timer) {      clearTimeout(timer);    }    timer = setTimeout(() => {      fn.apply(this, ...arguments);      clearTimeout(timer);      timer = null;    }, delay)  }}

VUE中的用法

<input v-model="value" @change="(e) => changeHandle(item)"/>
changeHandle: debounce((row) => {	console.log('row', row)})

关于arguments,他是隐形参数之一,另一个是this,拿来即用。

此处还有可讲之处,关于arguments的使用,此时此刻,你是否发现引发出的知识点有很多,这也是为什么防抖和节流会是互联网公司必问的问题,因为太丰富了,里面奥妙无群,无群延伸,几乎要覆盖js进阶的所有关键知识点了。

这里我们要重点说下apply和this的用法。

认识this

this的指向对象到底是谁,这个问题我们要视运行环境而看,标准就是当前的执行环境的对象

let obj = {	name: 'jack',	get() {		return this.name;	}}// 再看下面这种情况function getName() {	return this.name}let obj = {	name: 'jack',	get: getName}

第一段代码this指向的是obj对象;第二个this指向的是window。

this的指向问题是由具体运行环境决定,那么是否可以指定this指向呢?

这样就出现了apply和call函数了。

认识apply、call和bind

三者作用就是绑定变量作用域,不同处在于参数类型和返回值类型

函数

入参

返回值

call

this, arg1, arg2, ...

-

apply

this, [arg1, arg2, ...]

-

bind

this, arg1, arg2, ...

function

call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别就来了:

call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,'成都', ... ,'string' )。

apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,['成都', ..., 'string' ])。

bind 除了返回是函数以外,它 的参数和 call 一样。

可以移步阅读概念:

[apply、call和bind的概念]()

箭头函数和function以及立即执行函数区别

想要讲清楚这几个概念,首先需要了解清楚变量作用域,js中存在变量提升的问题,其实关于变量提升在从ES6开始之后,在实际工作中淡化了,使用`let`后不用再忧虑变量提升了,而且研发规范中明确禁止使用`var`,要使用`let`替代之。

变量提升是js的特点,但这个特点在构建大型应用并不是优点,容易导致变量泛滥污染,所以出现了let来限制变量提升。

但是我们还是有必要了解清楚他的前世,有利于我们深入的理解。

var lis = [0,1,2,3,4,5,6]for(var i = 0; i < lis.length; i++) {	setTimeout(() => {		console.log('[i] = ', lis[i])	})}
let lis1 = [0,1,2,3,4,5,6]for(let i = 0; i < lis1.length; i++) {	setTimeout(() => {		console.log('[i] = ', lis1[i])	})}

看上下两段代码的区别,他们输出结果一样吗?

按照我们直观的感受,上面的代码都应该输出:0、1、2、3、4、5、6

事实如此吗?

这里我们要重点说下在变量提升存在的情况下,for循环i的问题,你可能之前并没有注意到。

上面的事实有助于你了解为什么变量提升+异步+循环的情况下,最终所得值让你匪夷所思。

请看下面的:

这里面问题显而易见

最终值都是`undefined`,为什么不是0、1、2、3、4、5、6为什么是`undefined`,哪儿来的?

前一张截图,很明显,最终的i值为7,而数组lis中并没有下标7的值存在所以是`undefined`

同时,这种异步,宏任务被放到宏任务队列中异步执行,在最后程序执行的时候,i值变成了7,运行过程中并没有缓冲i值。

做个改造,请看

当变量不再提升时,这个时候会看到,循环中即使调用的是异步宏任务,那也会打印出预期的值。

我们换一种写法:

var lis = [0, 1, 2, 3, 4, 5, 6]for (var i = 0; i < lis.length; i++) {	(function (i) {		setTimeout(() => {			console.log('[i] = ', lis[i])		})	})(i)}

文章所及内容有点多,小憩一下吧

其实此处我会好奇一个问题,js在执行的时候到底是怎么处理变量i的?

那就需要了解清楚作用域,作用域分为全局作用域和局部作用域,ES6带来了块级作用域,局部作用域包括函数作用域和块级作用域

如果运行在浏览器中,那么全局作用域是浏览器的window对象,在console中做测试

> var a = 123undefined> window.a123> a = 456456> window.a456

如果运行在node环境中,那么全局作用域就是global对象

PS D:\workplace\ipoint-base\ipoint> nodeWelcome to Node.js v14.18.0.Type ".help" for more information.  > var a = 123undefined> a123> global.a123> a = 456456> global.a456>

所以我们可以看出ES6之前,对于var声明的变量,对于一个庞大应用的话,是不是犹如发丝一样的多,都挂载到了window对象下。

思路漫漫,请耐心的思考。我们之所以挖掘作用域,是想探究`for`循环中执行的宏任务为什么`i`记录到0到6的下标。

我们接着看局部作用域,函数作用域和块级作用域。

这里主要说明一下for循环涉及到几个作用域呢?

外部作用域 后称outerEnv()作用域 后称loopEnv{}内部作用域 后称innerEnv

重点说明:一定要牢记这三种作用域的存在,在没有覆盖值或者变量提升的情况下,互不影响。如果在innerEnv中声明`let i = '213'`是不会对loopEnv造成影响的,但是如果直接使用`i = '12'`则会覆盖loopEnv中的i,循环终止,但不报错

使用var声明,变量会被提升到global或window中

使用let声明,每一次loop都会产生一个innerEnv,所以现在可以理解,为什么可以记录到准确的下标了吧。

到此为止,我们对立即执行函数,箭头函数和匿名function的本质区别有了更深的认知,其实就是作用域的区别,同时对于for循环也有了更深的认知,循环过程中,不同的写法导致作用域的区别。

三、setTimeout的返回值有0、1的可能吗?这个关系到if判断的严谨性和对setTimeout的熟悉层度

不会返回0,对于一个空白页,当执行setTimeout的时候,会返回>=1的number,所以可以简写`timer && clearTimeout(timer)`

实际上完全可以不做非空判断,请看下面,所以可以直接调用也无妨

> clearTimeout(undefined)undefined> clearTimeout(null)undefined

未完待续...

标签: #css3向下箭头