这是我第16篇简书。
整体思路:
性能优化是什么?
从用户角度来看,什么才是好的页面?一个是页面加载的很快,另一个是页面使用起来很流畅。
那么对应到前端开发的角度,性能优化可以对应分为两个方向:页面加载时间跟页面运行效率。
从浏览器打开到页面渲染完成,花费了多少时间?
是的,这个问题有点熟悉,面试官比较常问的是从浏览器打开到页面渲染完成,发生了什么事情。这个问题网上很多回答,我也不就重复的细说了。主要的过程是:
浏览器解析->查询缓存->dns查询->建立链接->服务器处理请求->服务器发送响应->客户端收到页面->解析HTML->构建渲染树->开始显示内容(白屏时间)->首屏内容加载完成(首屏时间)->用户可交互(DOMContentLoaded)->加载完成(load)
很显然,如果我们要进行加载时间的优化,我们需要从这里的每一个步骤都去思考,去总结,而避免东凑一点,西凑一点。
具体细节:
一、web性能优化
1、减少HTTP请求(这个优化是最明显的)
(1)图片地图
,允许在一个图片上关联多个URL,目标URL取决于用户单击的图片上的位置。
(2)雪碧图
(也叫CSS精灵, 是一种CSS图像合成技术,一般用于图标,将多张图片合并到一张图片,配合background-position来使用)
(3)合并JS和CSS文件
(4)减少http请求头
(5)配置多个域名和CDN加速
把网站内容分散到多个、处于不同地域位置的服务器上可以加快下载速度。
(6)使用缓存(HTTP缓存、浏览器缓存、应用缓存)
恰当的缓存设置可以大大的减少 HTTP请求,例如,很少变化的图片资源可以直接通过 HTTP Header中的Expires设置一个很长的过期头 ;变化不频繁而又可能会变的资源可以使用 Last-Modifed来做请求验证。尽可能的让资源能够在缓存中待得更久。
(7)优化cookie
2、避免坏请求
有时候页面中的html或css会向服务器请求一个不存在的资源,比如图片或者html文件,这会造成浏览器与服务器之间过多的往返请求。
3、避免使用document.write
在js中,可以使用document.write。在网页上显示内容或者调用外部资源,而通过此方法,浏览器采取一些多余的步骤(下载资源,读取资源)。运行js来了解需要做什么,调用其他资源时,需要重新在执行一次这个过程。由于浏览器之前不知道要显示什么,所以会降低页面加载的速度。
要知道,任何能够被document.write调用的资源,都可以通过html调用。这样速度会更快
document.write('<scriptsrc="another.js"></script>');
改为
<scriptsrc="another.js"></script>
4、尽量减少dns查询次数
当浏览器和服务器建立链接时,它需要进行dns解析,将域名解析为ip地址,然而,一旦客户端需要执行dns lookup时,等待时间将会取决于域名服务器的有效响应速度。
虽然所有的isp的dns服务器都能缓存域名和ip地址映射表。但如果缓存的dns记录过期了而需要更新,则可能需要遍历多个dns节点,有时候需要通过全球范围内来找到可信任的域名服务器,一旦域名服务器工作繁忙,请求解析时,就需要排队则进一步延时等待时间。
所有减少dns查询次数很重要,页面加载就尽量避免额外耗时,为了减少dns查询次数,最好的解决方法就是在页面中减少不同的域名请求的机会、
可通过request checker工具来检测页面中存在多少请求后,进行优化。
5、尽量减少重定向
有时候为了特定需求,需要在网页中使用重定向。重定向的意思是,用户的原始请求(如请求A)被重定向到其他的请求(如请求B);
网页中使用重定向会造成网站性能和速度下降,因为浏览器访问网址是一连串的过程,如果访问到一半,而跳转到新的地址,就会重复发起一连串的过程,这将浪费很多时间。所有我们尽量避免重定向。Google建议:
A、 不要链接到一个包含重定向的页面
B、 不要请求包含重定向的资源
6、优化样式表和脚步顺序
Style标签和样式表调用代码应该放置在js代码的前面,这样可以使页面的加载速度加快。
7、避免js阻塞渲染
浏览器在遇到一个引入外部js文件的<script>标签时,会停下所有工作下载并解析执行它。在这个过程中,页面渲染和用户交互完全被阻塞了。这是页面加载就会停止。
8、启用压缩/Gzip
使用gzip对html和css文件进行压缩,通常可以大约节省50%到70%,这样加载页面只需要更少的带宽和更少的时间。
使用gzip对html和css文件进行压缩,通常可以大约节省50%到70%,这样加载页面只需要更少的带宽和更少的时间。
Google建议删除干扰页面第一屏内容加载 的js,第一屏指的是用户在屏幕中最初看到的页面,无论桌面浏览器,还是手机
二、html
1、减少DOM元素数量
页面中存在大量DOM元素,会导致js遍历DOM的效率变慢。尤其要尽量少用iframe,它是耗能最大的dom元素,而且会阻塞onload事件(除了获取葵花码 扫码登录这些需要用到iframe,其他地方基本不使用了)
2、使用css+div代替table布局,去掉格式化控制标签如:strong,b,i等,使用css控制。
下面这个说给不怎么懂前端的技术总监听[滑稽]:
说前5点就行了
为什么不建议用table进行布局?
1)table比其它html标记占更多的字节。(造成下载时间延迟,占用服务器更多流量资源)
2)table会阻挡浏览器渲染引擎的渲染顺序。(会延迟页面的生成速度,让用户等待更久的时间)
3)table里显示图片时需要你把单个、有逻辑性的图片切成多个图。(增加设计的复杂度,增加页面加载时间,增加http会话数)
4)在某些浏览器中,table里的文字的拷贝会出现问题。(会让用户不悦)
5)table会影响其内部的某些布局属性的生效(比如<td>里的元素的height:100%) (限制页面设计的自由性)
6)一旦学了CSS的知识,你会发现使用table做页面布局会变得更麻烦。(先花时间学一些CSS知识,会省去你以后大量的时间)
7)‘table对’对于页面布局来说,从语义上看是不正确的。(它描述的是表现,而不是内容)
8)table代码可读性差。(不但无法利用CSS,而且会不知所云,尤其在进行页面改版或内容抽取的时候)
9)table一旦设计完成就变成死的,很难通过CSS让它展现新的面貌
3、减少不必要的嵌套,尽量扁平化
因为当浏览器编译器遇到一个标签时就开始寻找它的结束标签,直到它匹配上才能显示它的内容,所以当嵌套很多标签时会影响页面加载速度(当然这个影响微乎其微)。
三、CSS
是时候展现真正的技术了,什么叫专业前端。
1、内联首屏关键CSS
性能优化中有一个重要的指标——首次有效绘制(First Meaningful Paint,简称FMP)即指页面的首要内容出现在屏幕上的时间。这一指标影响用户看到页面前所需等待的时间,FMP能减少这一时间。
这点对于电商网站特别重要,做后台管理的可以忽视。
将CSS直接内联到HTML文档中能使CSS更快速地下载。而使用外部CSS文件时,需要在HTML文档下载完成后才知道所要引用的CSS文件,然后才下载它们。所以说,内联CSS能够使浏览器开始页面渲染的时间提前,因为在HTML下载完成之后就能渲染了。所以不能一味的没有想法的通过link标签引用外部CSS文件。
但是,又不能内联所有的CSS,这种方式并不适用于内联较大的CSS文件。因为初始拥塞窗口存在限制(TCP相关概念,通常是 14.6kB,压缩后大小),如果内联CSS后的文件超出了这一限制,系统就需要在服务器和浏览器之间进行更多次的往返,这样并不能提前页面渲染时间。因此,我们应当只将渲染首屏内容所需的关键CSS内联到HTML中。
如何确定首屏关键CSS呢?显然,我们不需要手动确定哪些内容是首屏关键CSS。推荐Github上一个开源项目Critical CSS,可以将属于首屏的关键样式提取出来。
不过内联CSS有一个缺点,内联之后的CSS不会进行缓存,每次都会重新下载。所以上面的说到的初始拥塞窗口将内联后的文件大小控制在了14.6kb以内,每次重新下载14.6kb可以接受。
2、异步加载css
CSS会阻塞渲染,在CSS文件请求、下载、解析完成之前,浏览器将不会渲染任何已处理的内容。有时,这种阻塞是必须的,因为我们需要在css加载后再开始渲染页面。那么将首屏关键CSS内联后,剩余的CSS内容的阻塞渲染就不是必需的了,可以使用外部CSS,并且异步加载。
如何实现CSS的异步加载呢?
(前3点看看就好,重点用第4点)
1)使用JavaScript动态创建样式表link元素,并插入到DOM中。
// 创建link标签
const myCSS = document.createElement( "link" );
myCSS.rel = "stylesheet";
myCSS.href = "mystyles.css";
// 插入到header的最后位置
document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling );
2)将link元素的media属性设置为用户浏览器不匹配的媒体类型(或媒体查询),如media="print",甚至可以是完全不存在的类型media="noexist"。对浏览器来说,如果样式表不适用于当前媒体类型,其优先级会被放低,会在不阻塞页面渲染的情况下再进行下载。
<link rel="stylesheet" href="mystyles.css" media="noexist" onload="this.media='all'">
当然,这么做只是为了实现CSS的异步加载,别忘了在文件加载完成之后,将media的值设为screen或all,从而让浏览器开始解析CSS。
3)我们还可以通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel改回去。
<link rel="alternate stylesheet" href="mystyles.css" onload="this.rel='stylesheet'">
4)然而,上述的三种方法已过时。现在,rel="preload"这一Web标准指出了如何异步加载资源,包括CSS类资源。
<link rel="preload" href="mystyles.css" as="style" onload="this.rel='stylesheet'">
注意,as
是必须的。忽略as
属性,或者错误的as
属性会使preload
等同于XHR
请求,浏览器不知道加载的是什么内容,因此此类资源加载优先级会非常低。
看起来,rel="preload"
的用法和上面两种没什么区别,都是通过更改某些属性,使得浏览器异步加载CSS文件但不解析,直到加载完成并将修改还原,然后开始解析。
但是它们之间其实有一个很重要的不同点,那就是使用preload,比使用不匹配的media
方法能够更早地开始加载CSS。
3、文件压缩(效果显著)
文件的大小会直接影响浏览器的加载速度,这一点在网络较差时表现地尤为明显。相信大家都早已习惯对CSS进行压缩,现在的构建工具,如webpack、gulp/grunt、rollup等也都支持CSS压缩功能。压缩后的文件大小明显减小,可以大大降低了浏览器的加载时间。
4、去除无用CSS
虽然文件压缩能够降低文件大小。但CSS文件压缩通常只会去除无用的空格。而去除无用CSS会进一步减小CSS文件的大小。(文件压缩是个前端都知道,而去除无用css相信很多人没用到过,还有前面异步加载的黑科技,面试就是各种知识细节堆积,加起来就能给人一种靠谱专业的冲击感)
一般情况下,会存在这两种无用的CSS代码:一种是不同元素或者其他情况下的重复代码,一种是整个页面内没有生效的CSS代码。对于前者,在编写的代码时候,我们应该尽可能地提取公共类,减少重复。对于后者,在不同开发者进行代码维护的过程中,总会产生不再使用的CSS的代码,当然一个人编写时也有可能出现这一问题。而这些无用的CSS代码不仅会增加浏览器的下载量,还会增加浏览器的解析时间,这对性能来说是很大的消耗。所以我们需要找到并去除这些无用代码。
当然,如果手动删除这些无用CSS是很低效的。我们可以借助Uncss库来进行。Uncss可以用来移除样式表中的无用CSS,并且支持多文件和JavaScript注入的CSS。
5、有选择地使用选择器
因为CSS选择器的匹配是从右向左进行的,所以尽量保持以下原则:
- 保持简单,不要使用嵌套过多过于复杂的选择器。
- 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。 (子元素选择器它不香吗)
- 不要使用类选择器和ID选择器修饰元素标签,这样多此一举,还会降低效率。
- 不要为了追求速度而放弃代码的可读性与可维护性,没有绝对的优化。
一般开发团队都会定好css编写规范,如果你的团队还没定,那么你一定要普及大家用同一规范(为什么呢,难道看别人代码时不难受吗?),或者引用借鉴一些:如BEM、OOCSS、SUIT、SMACSS、ITCSS等作为CSS编写规范。使用统一的方法论能够帮助大家形成统一的风格,减少命名冲突,也能避免上述的问题。
6、减少使用昂贵的属性
在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价。当页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写CSS时,我们应该尽量减少使用昂贵属性,如box-shadow、border-radius、filter、透明度、:nth-child等。
当然,并不是让大家不要使用这些属性(比如border-radius确实不得不用),因为这些应该都是我们经常使用的属性。只是当有两种方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,如果每次都做这样的决策,网站的性能会在不知不觉中得到一定的提升。
7、优化重排与重绘(划重点)
在网站的使用过程中,某些操作会导致样式的改变,这时浏览器需要检测这些改变并重新渲染,其中有些操作所耗费的性能更多。我们都知道,当FPS为60时,用户使用网站时才会感到流畅。这也就是说,我们需要在16.67ms内完成每次渲染相关的所有操作,所以我们要尽量减少耗费更多的操作。
(1)减少重排
重排会导致浏览器重新计算整个文档,重新构建渲染树,这一过程会降低浏览器的渲染速度。如下所示,有很多操作会触发重排,我们应该避免频繁触发这些操作:
- 改变
font-size
和font-family
- 改变元素的内外边距
- 通过JS改变CSS类
- 通过JS获取DOM元素的位置相关属性(如width/height/left等)
- CSS伪类激活
- 滚动滚动条或者改变窗口大小
此外,我们还可以通过CSS Trigger查询哪些属性会触发重排与重绘。
值得一提的是,某些CSS属性具有更好的重排性能。如使用Flex
时,比使用inline-block
和float
时重排更快,所以在布局时可以优先考虑Flex
。
(2)避免不必要的重绘
当元素的外观(如color,background,visibility等属性)发生改变时,会触发重绘。在网站的使用过程中,重绘是无法避免的。不过,浏览器对此做了优化,它会将多次的重排、重绘操作合并为一次执行。不过我们仍需要避免不必要的重绘,如页面滚动时触发的hover事件,可以在滚动的时候禁用hover事件,这样页面在滚动时会更加流畅。
此外,我们编写的CSS中动画相关的代码越来越多,我们已经习惯于使用动画来提升用户体验。我们在编写动画时,也应当参考上述内容,减少重绘重排的触发。除此之外我们还可以通过硬件加速、will-change来提升动画性能。
最后需要注意的是,用户的设备可能并没有想象中的那么好,至少不会有我们的开发机器那么好。我们可以借助Chrome的开发者工具进行CPU降速,然后再进行相关的测试,降速方法如下图所示。如果需要在移动端访问的,最好将速度限制更低,因为移动端的性能往往更差。
8、不要使用@import引入CSS
最后提一下,不要使用@import引入CSS,相信大家也很少使用。
不建议使用@import主要有以下两点原因:
首先,使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。
其次,多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载。
所以不要使用这一方法,使用link标签就行了。
9、硬件加速与will-change
硬件加速意味着浏览器会帮你把一部分对渲染页面来说较繁重的任务交给GPU,而不是一股脑都交给CPU去处理,当这部分CSS操作得到硬件加速时,可以使页面渲染速度更快。
CSS animation,transform,transition这些属性并不会自动被硬件加速,而是由浏览器的渲染引擎去执行的,但是一些浏览器通过某些属性提供硬件加速,从而提升页面的渲染性能。例如CSS中的opacity
属性是少数几个可以被浏览器认定为可被硬件加速的属性之一,因此它也是所有的CSS属性中性能最好属性的之一。
其他常见的硬件加速操作——CSS 3D变换Hack方法:translateZ() 或 translate3d()。使用translateZ()或translate3d()这种hack方式来让浏览器对animation或transform行为使用硬件加速。但是这种hack的方法去创建图层并不是万能的,图层创建可以加快页面加载速度,但会带来其他的成本:它会占用系统RAM和GPU上的内存(限于移动设备),并且很多时候都会带来不良影响(特别是在移动设备上),所以这种方法要合理的去使用,你必须要清楚的知道使用这种方式是不是真的可以提高页面性能,不能使这个操作反而成了影响页面性能的瓶颈。
CSS新属性:will-change
比hack方法更为有效:
will-change
属性允许你提前通知浏览器你可能会对某个元素做什么类型的操作,以便于浏览器在需要的时候采取适当的优化方案。因此,避免了可能对页面的响应性产生负面影响的非必要成本,使元素可以更快地呈现。使用will-change可以提示浏览器将会发生的转化:
不要这样直接写在默认状态中,因为will-change会一直挂着
.will-change {
will-change: transform;
transition: transform 0.3s;
}
.will-change:hover {
transform: scale(1.5);
}
可以让父元素hover的时候,声明will-change,这样,移出的时候就会自动remove,触发的范围基本上是有效元素范围。
.will-change-parent:hover .will-change {
will-change: transform;
}
.will-change {
transition: transform 0.3s;
}
.will-change:hover {
transform: scale(1.5);
}
注意事项:
- 不要使用will-change声明对太多属性或元素的更改
-
给浏览器足够的时间来工作
will-change属性顾名思义只告知浏览器即发生的变化,所以这个阶段什么改变也没发生,我们使用will-change是以便于浏览器对我们声明的更改进行某些优化,浏览器需要时间去处理优化这些更改,为了在这些变化在真正发生时可以立即生效,所以在元素更改之前立即设置will-change几乎没有任何效果,并且可能比不设置更糟糕。你应该提前一点时间预测某种改变会发生,然后设置will-change的值,浏览器提供足够的时间来优化该更改,见上面的例子。 - 变更生效后移除掉will-change
浏览器对即将发生的更改进行的优化通常成本很高,所以使用完后要记得移除will-change来释放资源。 - 有选择性的在CSS中直接使用will-change
四、js
(如果说css优化是锦上添花,那么css是花,js优化才是锦)
1、减少全局变量的查找。
因为全局变量在作用域链的最顶端,频繁查找很耗性能。所以在一个函数中尽量将全局对象存储为局部变量来查找,因为访问局部变量的数要更快一些。
function(){
alert(window.location.href+window.location.host);
}
修改为:
funciton(){
var location=window.location;
alert(location.href+location.host);
}
2、慎用 with
with会创建自已的作用域,因此会增加其中执行代码的作用域的长度。(我好像从来没用过with,也没见别人代码里用过)
with(obj){ p = 1};
代码块的行为实际上是修改了代码块中的 执行环境,将obj放在了其作用域链的最前端,在 with代码块中访问非局部变量是都是先从 obj上开始查找,如果没有再依次按作用域链向上查找,因此使用 with相当于增加了作用域链长度。而每次查找作用域链都是要消耗时间的,过长的作用域链会导致查找性能下降。
因此,除非你能肯定在 with代码中只访问 obj中的属性,否则慎用 with,可以使用局部变量缓存需要访问的属性。
with(a,b,c,d){
property1=1;
property2=2;
}
修改为:
var obj=a.b.c.d;
obj.property1=1;
obj.property2=2;
3、慎用 定时器
(1)如果针对的是不断运行的代码,不应该使用setTimeout,而应该使用setInterval。因为setTimeout每一次都会初始化一个定时器。而setInterval只会在开始的时候初始化一个定时器。
(2)不要给setTimeout或setInterval传递字符串参数。
4、避免使用 eval和 Function
每次 eval 或 Function 构造函数作用于字符串表示的源代码时,脚本引擎都需要将源代码转换成可执行代码。这是很消耗资源的操作 —— 通常比简单的函数调用慢 100倍以上。
eval 函数效率特别低,由于事先无法知晓传给 eval 的字符串中的内容,eval在其上下文中解释要处理的代码,也就是说编译器无法优化上下文,因此只能有浏览器在运行时解释代码。这对性能影响很大。
Function 构造函数比 eval略好,因为使用此代码不会影响周围代码 ;但其速度仍很慢。
此外,使用 eval和 Function也不利于Javascript 压缩工具执行压缩。
5、优化循环
循环是编程中最常见的结构,优化循环是性能优化过程中很重要的一部分。一个循环的基本优化步骤如下:
1)减值迭代——大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加有效。
2)简化终止条件——由于每次循环过程都会计算终止条件,故必须保证它尽可能快,即避免属性查找或其它操作。最好是将循环控制量保存到局部变量中,也就是说对数组或列表对象遍历的时候,提前将length保存到局部变量中,避免循环的每一步重复取值。
3)简化循环体——循环体是执行最多的,故要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。
4)使用后测试循环—— 在js中,我们常用的循环中:
for(in)的效率极差,因为他需要查询散列键,只要可以,就应该尽量少用。
for和while循环,while优于for,可能for结构问题,需要经常的跳转。do..while更好。
最常用的for和while循环都是前测试循环,而后测试循环如do-while循环可以避免最初终止条件的计算,因些计算更快。
for(var i = 0; i < values.length; i++) {
process(values[i]);
}
优化1:简化终止条件
for(var i = 0, len = values.length; i < len; i++) {
process(values[i]);
}
优化2:使用后测试循环(注意:使用后测试循环需要确保要处理的值至少有一个)
var i values.length - 1;
if(i > -1) {
do {
process(values[i]);
}while(--i >= 0);
}
6、字符串拼接
在 Javascript中使用"+" 号来拼接字符串效率是比较低的,因为每次运行都会开辟新的内存并生成新的字符串变量,然后将拼接结果赋值给新变量。与之相比更为高效的做法是使用数组的 join方法,即将需要拼接的字符串放在数组中最后调用其 join方法得到结果。不过由于使用数组也有一定的开销,因此当需要拼接的字符串较多的时候可以考虑用此方法。
1)如果需要连接多个字符串,应该少使用+=:
如
s+=a;s+=b;s+=c;
修改为:
s+=a+b+c;
2)而如果是收集字符串,比如多次对同一个字符进行+=操作的话,最好使用一个缓存,使用js数组来收集,最后join方法连接起来。
var buf=[];
for(var i=0;i<100;i++){
buf.push(i.toString());
}
var all=buf.join("");
7、少操作DOM
DOM操作应该是脚本中最耗性能的一类操作,例如增加、修改、删除 DOM元素或者对 DOM集合进行操作。
(1)最小化现场更新
一旦你需要访问的DOM部分是已经显示的页面的一部分,那么你就是在进行一个现场更新。之所以叫现场更新,是因为需要立即(现场)对页面对用户的显示进行更新,每一个更改,不管是插入单个字符还是移除整个片段,都有一个性能惩罚,因为浏览器需要重新计算无数尺寸以进行更新。现场更新进行的越多,代码完成执行所花的时间也越长。
(2)多使用innerHTML
使用一次innerHTML复制代替构建dom元素。
有两种在页面上创建DOM节点的方法:使用诸如createElement()和appendChild()之类的DOM方法,以及使用innerHTML。对于小的DOM更改,两者效率差不多,但对于大的DOM更改,innerHTML要比标准的DOM方法创建同样的DOM结构快得多。
当使用innerHTML设置为某个值时,后台会创建一个HTML解释器,然后使用内部的DOM调用来创建DOM结构,而非基于JS的DOM调用。而内部方法是编译好的而非解释执行,故执行的更快。
varfrag=document.createDocumentFragment();
for(var i=0;i<1000;i++){
varel=document.createElement('p');
el.innerHTML=i;
frag.appendChild(el);
}
document.body.appendChild(frag);
应该改为:
var html=[];
for(var i=0;i<1000;i++){
html.push('<p>'+i+"</p>");
}
document.body.innerHTML=html.join("");
五、用户体验角度
(1)预加载,懒加载
(2)浏览器缓存,应用缓存
(3)细节设计(全选/反全选/自动补全……所有你能想到的)
如果觉得干货很多,请给一个赞。