原文:juejin.im/user/4107431174759256
导读
同事写了一个拖拽左边菜单栏改变菜单栏宽度从而得到更好的交互体验效果。
But !美中不足的是拖拽的时候如果手速过快,会导致卡顿效果,看起来十分难受。经过不断调试,最终是使用了setTimeout解决了该问题。那么问题来了!为什么setTimeout能解决动画卡顿问题呢?
一图胜千文,通过两张图的前后对比,我们来看看实际效果!(这是没有加上setTimeout之前,有明显卡顿???)
(这是加上setTimeout之后,拖拽效果明显顺畅了不少???)
![6d5d3cf2fdd613f85ea90a0cf8f9f316.gif](https://i-blog.csdnimg.cn/blog_migrate/4a3f095cc0c0e5d7568a2258aa187e46.gif)
假设提问:会不会是内容过多导致的呢??
为了证实我这个猜想,故将内容切换到比较少模块。
果然,拖拽效果瞬间流畅了许多???
![d451e00f5c2b3a090be21b93c00619b6.gif](https://i-blog.csdnimg.cn/blog_migrate/1ba77ef01396b4ee7fe887ed38dbb643.gif)
这个时候基本可以确认是内容过多导致的卡顿,那如何保证不改变内容大小的情况下达到拖拽顺滑的效果呢?提出解决方案:能否通过减少回流次数来节省性能,从而使得拖拽顺滑呢??
为此,故将右边内容固定宽度,拖拽菜单的时候,由于右边内容盒子宽度已经固定,所以不会造成元素盒子尺寸发生变化,故不会触发回流。果然,拖拽效果再次流畅了许多???
什么是回流与重绘?
回流
当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
重绘:
当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式。这个过程叫做重绘。
由此可知:重绘不一定导致回流,回流一定会导致重绘!
通过控制内容宽度,减少回流操作确实解决了卡顿问题,但该操作导致页面无法自适应了,无法适配各种尺寸的屏幕。那么还有什么办法是可以既不改变内容大小,也不改变内容盒子尺寸从而解决菜单拖拽卡顿问题呢?
能否通过打乱模块声明周期,改变任务执行顺序来使得拖拽顺滑呢??
如标题所示,当我们将改变元素尺寸的代码放置到setTimeout中。利用setTimeout实现一种伪多线程的效果,从而解决了拖拽卡顿问题!
浏览器渲染过程(简述)
当我们在浏览器输入网址到页面呈现,在浏览器渲染这一层面,大体上需要经历三个步骤(下载 ? 解析 ? 布局):
- 根据页面URL向服务器发送一个请求,服务器响应页面的HTML。浏览器首先去下载html,js,css,图片,字体等一系列资源。
- 浏览器将html解析成为dom tree(节点树),将css解析成为rule tree(规则树),二者结合成为render tree(渲染树)
- 渲染树构建完毕之后,浏览器的渲染引擎将遍历渲染树把每个节点绘制出来,从而呈现出我们所看到的页面效果。
❝浏览器刷新的频率大概是60次/秒, 也就是说刷新一次大概时间为16ms。如果浏览器对每一帧的渲染工作超过了这个时间, 页面的渲染就会出现卡顿的现象。
❞
浏览器进程与线程(简述)
我们都知道JavaScript是单线程的,但是浏览器是支持多进程啊。所以,配合浏览器的合理分配,各个进程各司其职,玩儿的不亦乐乎。
什么是进程?
进程是CPU进行资源分配的基本单位
什么是线程?
线程是CPU调度的最小单位,是建立在进程的基础上运行的单位,共享进程的内存空间。
浏览器包含了那些进程?
- 浏览器进程(负责浏览器级别的操作,如:标签页的创建和销毁、资源的管理与下载等)
- 第三方插件进程(负责第三方插件的使用)
- GPU进程(负责3D绘制和硬件加速)
- 浏览器渲染进程(浏览器内核负责HTML,CSS,JS等文件的解析和执行)
浏览器渲染进程(浏览器内核详解)
浏览器渲染进程主要包含了5个线程:
- GUI渲染线程
- JS引擎线程
- 计时器线程
- 定时器触发线程
- 异步Http请求线程
GUI渲染线程与JS引擎线程是相互排斥的,因为二者不能够同时进行。
试想一下,渲染线程这边正在渲染一个div的背景色,JS引擎线程那边可劲儿的通过js改变你这个div元素的背景色以及尺寸啥的。那么这时候到底以那一边为标准。非得乱套不可。
所以,当执行其中任何一个线程的时候,另一个线程操作则被挂起,等待其他任务执行完毕之后,再回来执行这边的任务。
了解了这些前置知识后,我们还需要了解一下宏任务微任务,以及事件循环、任务队列等知识。这里我不做过多讲述
关于setTimeout
setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。
setTimeout会将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这就说明setTimeout指定的代码,必须等到本次执行的所有代码都执行完,才会执行。
setTimeout扫盲:
1.setTimeout中的this关键字将指向全局环境,而不是定义时所在的那个对象。
2.setTimeout是一个异步任务,并且是一个宏任务。所以是最后执行的任务。
3.setTimeout执行回调间隔时间长度
setTimeout( () =>{
console.log('hello');
}, 100)
❝注意:这里的时间间隔长度一定是大于100ms的,具体大于多少取决于在此之前同步任务执行的js需要占用多少时间。
❞
4.setTimeout(func,0)与setTimeout(func)区别
setTimeout(func,0):
表示让func回调函数挂起,等到所有同步任务以及任务队列里面的任务执行完毕之后,立马执行func函数。事实上,0毫秒实际上达不到的。setTimeout推迟执行的时间,最少是4毫秒。
setTimeout(func):
如果没有传递具体延迟时间,浏览器将为定时器自动分配时间,具体分配多少因浏览器而异。
5.setTimeout的延迟时间最大值是多少呢?
答案是:2147483647毫秒(24.8天),超出这个时间,将作为溢出处理,即等同于setTimeout(f,0)的效果。
6.setTimeout将返回一个表示计数器编号的整数值,将该整数传入clearTimeout就可以取消对应的定时器。
--------- 华丽的分割线 --------------
说了这么多,这跟拖拽卡顿有啥关系啊???? 哈哈,你别慌,咱们一步一步来,接下来我就给你介绍一波setTimeout的实际用途!???
用途以一:调整事件的发生顺序
setTimeout( () =>{
console.log(2);
},0)
console.log(1);
我们都知道js在解析代码的时候是逐行执行的,只有当遇到异步任务的时候,才会将异步任务挂起放到同步任务之后执行。所以我们正好利用这个特性,来打乱事件执行顺序,以满足特定的需求场景。
用途二:分割耗时任务
我们都知道javascript是单线程的,其特点就是容易出现阻塞。如果某一段程序处理时间很长,很容易导致整个页面卡住。干不了其他事儿,这时候便可以使用setTimeout来将这些任务分割从而解决此类问题。用我们拖拽改变元素宽度举个栗子?
// ...其它代码
document.onmousemove = function (e) {
// ...其它代码
setTimeout( () =>{
document.getElementById("app").style.width = xxx + 'px'
},0)
}
如上代码,我们在拖拽元素的过程中,频繁的触发回流操作,而回流操作本身就是非常昂贵的。
这个时候就很容易出现阻塞,导致网页卡顿。这时我们加上setTimeout
,巧妙的将这部分操作进行一个分割处理,将任务放到浏览器最早可得的空闲时段执行,那些计算量大、耗时长的任务,分别放到setTimeout(f,0)
里面执行(分片塞入队列),这样即使在复杂程序没有处理完时,我们操作页面,也是能得到即时响应的。
用途三:事件防抖与节流
好啦!关于一个拖拽卡顿问题引发出对setTimeOut
的探索就简单的介绍到这里了,由此可见,一个小小的问题,背后也需要强大的理论知识支撑。
所以,掌握好js
的一些底层原理知识,才是我们提升个人能力,进入下一个阶段的关键步骤。这里安利一句我特别喜欢的名句:
80%
的问题可以用20%
的知识解决,而剩下的那20%
的问题则需要你用80%
的知识积累去解决了。
1.如果看到这里,说明你喜欢这篇文章,请 转发
、点赞、
同时 在看
标星(置顶)
2.关注公众号前端人,回复资料包
领取我整理的前端进阶资料包
3.回复加群
,加入前端进阶群,和小伙伴一起学习讨论!