写点什么

前端高频面试题合集(中高级必备)

作者:loveX001
  • 2022-11-14
    浙江
  • 本文字数:11403 字

    阅读完需:约 37 分钟

什么情况会阻塞渲染?

首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。


当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。

谈一谈你对 HTTP/2 理解

首先补充一下,http 和 https 的区别,相比于 http,https 是基于 ssl 加密的 http 协议


简要概括:http2.0 是基于 1999 年发布的 http1.0 之后的首次更新


  • 提升访问速度 (可以对于,请求资源所需时间更少,访问速度更快,相比 http1.0)

  • 允许多路复用 :多路复用允许同时通过单一的 HTTP/2 连接发送多重请求-响应信息。改 善了:在 http1.1 中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限 制(连接数量),超过限制会被阻塞

  • 二进制分帧 :HTTP2.0 会将所有的传输信息分割为更小的信息或者帧,并对他们进行二 进制编码

  • 首部压缩

  • 服务器端推送


头部压缩


HTTP 1.1 版本会出现 User-Agent、Cookie、Accept、Server、Range 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。


HTTP 2.0 使用 HPACK 算法进行压缩。


多路复用


  • HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8 个的 TCP 链接请求限制。


HTTP2 中:


  • 同域名下所有通信都在单个连接上完成。

  • 单个连接可以承载任意数量的双向数据流。

  • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装,也就是Stream ID,流标识符,有了它,接收方就能从乱序的二进制帧中选择 ID 相同的帧,按照顺序组装成请求/响应报文。


服务器推送


浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。


相比较 http/1.1 的优势👇


  • 推送资源可以由不同页面共享

  • 服务器可以按照优先级推送资源

  • 客户端可以缓存推送的资源

  • 客户端可以拒收推送过来的资源


二进制分帧


之前是明文传输,不方便计算机解析,对于回车换行符来说到底是内容还是分隔符,都需要内部状态机去识别,这样子效率低,HTTP/2 采用二进制格式,全部传输 01 串,便于机器解码。


这样子一个报文格式就被拆分为一个个二进制帧,用 Headers 帧存放头部字段,Data 帧存放请求体数据。这样子的话,就是一堆乱序的二进制帧,它们不存在先后关系,因此不需要排队等待,解决了 HTTP 队头阻塞问题。


在客户端与服务器之间,双方都可以互相发送二进制帧,这样子 双向传输的序列 ,称为,所以 HTTP/2 中以流来表示一个 TCP 连接上进行多个数据帧的通信,这就是多路复用概念。


那乱序的二进制帧,是如何组装成对于的报文呢?


  • 所谓的乱序,值的是不同 ID 的 Stream 是乱序的,对于同一个 Stream ID 的帧是按顺序传输的。

  • 接收方收到二进制帧后,将相同的 Stream ID 组装成完整的请求报文和响应报文。

  • 二进制帧中有一些字段,控制着优先级流量控制等功能,这样子的话,就可以设置数据帧的优先级,让服务器处理重要资源,优化用户体验。


HTTP2 的缺点


  • TCP 以及 TCP+TLS 建立连接的延时,HTTP/2 使用 TCP 协议来传输的,而如果使用 HTTPS 的话,还需要使用 TLS 协议进行安全传输,而使用 TLS 也需要一个握手过程,在传输数据之前,导致我们需要花掉 3~4 个 RTT。

  • TCP 的队头阻塞并没有彻底解决。在 HTTP/2 中,多个请求是跑在一个 TCP 管道中的。但当 HTTP/2 出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该 TCP 连接中的所有请求。

Canvas 和 SVG 的区别

(1)SVG: SVG 可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言 XML 描述的 2D 图形的语言,SVG 基于 XML 就意味着 SVG DOM 中的每个元素都是可用的,可以为某个元素附加 Javascript 事件处理器。在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。


其特点如下:


  • 不依赖分辨率

  • 支持事件处理器

  • 最适合带有大型渲染区域的应用程序(比如谷歌地图)

  • 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快)

  • 不适合游戏应用


(2)Canvas: Canvas 是画布,通过 Javascript 来绘制 2D 图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。


其特点如下:


  • 依赖分辨率

  • 不支持事件处理器

  • 弱的文本渲染能力

  • 能够以 .png 或 .jpg 格式保存结果图像

  • 最适合图像密集型的游戏,其中的许多对象会被频繁重绘


注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。

Sass、Less 是什么?为什么要使用他们?

他们都是 CSS 预处理器,是 CSS 上的一种抽象层。他们是一种特殊的语法/语言编译成 CSS。 例如 Less 是一种动态样式语言,将 CSS 赋予了动态语言的特性,如变量,继承,运算, 函数,LESS 既可以在客户端上运行 (支持 IE 6+, Webkit, Firefox),也可以在服务端运行 (借助 Node.js)。


为什么要使用它们?


  • 结构清晰,便于扩展。 可以方便地屏蔽浏览器私有语法差异。封装对浏览器语法差异的重复处理, 减少无意义的机械劳动。

  • 可以轻松实现多重继承。 完全兼容 CSS 代码,可以方便地应用到老项目中。LESS 只是在 CSS 语法上做了扩展,所以老的 CSS 代码也可以与 LESS 代码一同编译。


参考:前端进阶面试题详细解答

水平垂直居中的实现

  • 利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。


.parent {    position: relative;} .child {    position: absolute;    left: 50%;    top: 50%;    transform: translate(-50%,-50%);}
复制代码


  • 利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况:


.parent {    position: relative;}
.child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto;}
复制代码


  • 利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况


.parent {    position: relative;}
.child { position: absolute; top: 50%; left: 50%; margin-top: -50px; /* 自身 height 的一半 */ margin-left: -50px; /* 自身 width 的一半 */}
复制代码


  • 使用 flex 布局,通过 align-items:center 和 justify-content:center 设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的问题,该方法在移动端用的较多:


.parent {    display: flex;    justify-content:center;    align-items:center;}
复制代码

label 的作用是什么?如何使用?

label 标签来定义表单控件的关系:当用户选择 label 标签时,浏览器会自动将焦点转到和 label 标签相关的表单控件上。


  • 使用方法 1:


<label for="mobile">Number:</label><input type="text" id="mobile"/>
复制代码


  • 使用方法 2:


<label>Date:<input type="text"/></label>
复制代码

什么是物理像素,逻辑像素和像素密度,为什么在移动端开发时需要用到 @3x, @2x 这种图片?

以 iPhone XS 为例,当写 CSS 代码时,针对于单位 px,其宽度为 414px & 896px,也就是说当赋予一个 DIV 元素宽度为 414px,这个 DIV 就会填满手机的宽度;


而如果有一把尺子来实际测量这部手机的物理像素,实际为 1242*2688 物理像素;经过计算可知,1242/414=3,也就是说,在单边上,一个逻辑像素=3 个物理像素,就说这个屏幕的像素密度为 3,也就是常说的 3 倍屏。


对于图片来说,为了保证其不失真,1 个图片像素至少要对应一个物理像素,假如原始图片是 500300 像素,那么在 3 倍屏上就要放一个 1500900 像素的图片才能保证 1 个物理像素至少对应一个图片像素,才能不失真。 当然,也可以针对所有屏幕,都只提供最高清图片。虽然低密度屏幕用不到那么多图片像素,而且会因为下载多余的像素造成带宽浪费和下载延迟,但从结果上说能保证图片在所有屏幕上都不会失真。


还可以使用 CSS 媒体查询来判断不同的像素密度,从而选择不同的图片:


my-image { background: (low.png); }@media only screen and (min-device-pixel-ratio: 1.5) {  #my-image { background: (high.png); }}
复制代码

浏览器乱码的原因是什么?如何解决?

产生乱码的原因:


  • 网页源代码是gbk的编码,而内容中的中文字是utf-8编码的,这样浏览器打开即会出现html乱码,反之也会出现乱码;

  • html网页编码是gbk,而程序从数据库中调出呈现是utf-8编码的内容也会造成编码乱码;

  • 浏览器不能自动检测网页编码,造成网页乱码。


解决办法:


  • 使用软件编辑 HTML 网页内容;

  • 如果网页设置编码是gbk,而数据库储存数据编码格式是UTF-8,此时需要程序查询数据库数据显示数据前进程序转码;

  • 如果浏览器浏览时候出现网页乱码,在浏览器中找到转换编码的菜单进行转换。

display 的属性值及其作用

DNS 完整的查询过程

DNS 服务器解析域名的过程:


  • 首先会在浏览器的缓存中查找对应的 IP 地址,如果查找到直接返回,若找不到继续下一步

  • 将请求发送给本地 DNS 服务器,在本地域名服务器缓存中查询,如果查找到,就直接将查找结果返回,若找不到继续下一步

  • 本地 DNS 服务器向根域名服务器发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址

  • 本地 DNS 服务器向顶级域名服务器发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址

  • 本地 DNS 服务器向权威域名服务器发送请求,域名服务器返回对应的结果

  • 本地 DNS 服务器将返回结果保存在缓存中,便于下次使用

  • 本地 DNS 服务器将返回结果返回给浏览器


比如要查询 IP 地址,首先会在浏览器的缓存中查找是否有该域名的缓存,如果不存在就将请求发送到本地的 DNS 服务器中,本地 DNS 服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。

Promise.resolve

Promise.resolve = function(value) {    // 1.如果 value 参数是一个 Promise 对象,则原封不动返回该对象    if(value instanceof Promise) return value;    // 2.如果 value 参数是一个具有 then 方法的对象,则将这个对象转为 Promise 对象,并立即执行它的then方法    if(typeof value === "object" && 'then' in value) {        return new Promise((resolve, reject) => {           value.then(resolve, reject);        });    }    // 3.否则返回一个新的 Promise 对象,状态为 fulfilled    return new Promise(resolve => resolve(value));}
复制代码

CSS 预处理器/后处理器是什么?为什么要使用它们?

预处理器, 如:lesssassstylus,用来预编译sass或者less,增加了css代码的复用性。层级,mixin, 变量,循环, 函数等对编写以及开发 UI 组件都极为方便。


后处理器, 如: postCss,通常是在完成的样式表中根据css规范处理css,让其更加有效。目前最常做的是给css属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。


css预处理器为css增加一些编程特性,无需考虑浏览器的兼容问题,可以在CSS中使用变量,简单的逻辑程序,函数等在编程语言中的一些基本的性能,可以让css更加的简洁,增加适应性以及可读性,可维护性等。


其它css预处理器语言:Sass(Scss), Less, Stylus, Turbine, Swithch css, CSS Cacheer, DT Css


使用原因:


  • 结构清晰, 便于扩展

  • 可以很方便的屏蔽浏览器私有语法的差异

  • 可以轻松实现多重继承

  • 完美的兼容了CSS代码,可以应用到老项目中

Iterator 迭代器

Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。


Iterator 语法:


const obj = {    [Symbol.iterator]:function(){}}
复制代码


[Symbol.iterator] 属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。


  • 迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 方法,改变指针的指向,让其指向下一条数据

  • 每一次的 next 都会返回一个对象,该对象有两个属性

  • value 代表想要获取的数据

  • done 布尔值,false 表示当前指针指向的数据有值,true 表示遍历已经结束


Iterator 的作用有三个:


  • 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

  • 第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。

  • 第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员。

  • 不断调用指针对象的 next 方法,直到它指向数据结构的结束位置。


每一次调用 next 方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value 和 done 两个属性的对象。其中,value 属性是当前成员的值,done 属性是一个布尔值,表示遍历是否结束。


let arr = [{num:1},2,3]let it = arr[Symbol.iterator]() // 获取数组中的迭代器console.log(it.next())  // { value: Object { num: 1 }, done: false }console.log(it.next())  // { value: 2, done: false }console.log(it.next())  // { value: 3, done: false }console.log(it.next())  // { value: undefined, done: true }
复制代码


对象没有布局 Iterator 接口,无法使用for of 遍历。下面使得对象具备 Iterator 接口


  • 一个数据结构只要有 Symbol.iterator 属性,就可以认为是“可遍历的”

  • 原型部署了 Iterator 接口的数据结构有三种,具体包含四种,分别是数组,类似数组的对象,Set 和 Map 结构


为什么对象(Object)没有部署 Iterator 接口呢?


  • 一是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。然而遍历遍历器是一种线性处理,对于非线性的数据结构,部署遍历器接口,就等于要部署一种线性转换

  • 对对象部署Iterator接口并不是很必要,因为Map弥补了它的缺陷,又正好有Iteraotr接口


let obj = {    id: '123',    name: '张三',    age: 18,    gender: '男',    hobbie: '睡觉'}
obj[Symbol.iterator] = function () { let keyArr = Object.keys(obj) let index = 0 return { next() { return index < keyArr.length ? { value: { key: keyArr[index], val: obj[keyArr[index++]] } } : { done: true } } }}
for (let key of obj) { console.log(key)}
复制代码


浏览器渲染进程的线程有哪些

浏览器的渲染进程的线程总共有五种: (1)GUI 渲染线程 负责渲染浏览器页面,解析 HTML、CSS,构建 DOM 树、构建 CSSOM 树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。


注意:GUI 渲染线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。


(2)JS 引擎线程 JS 引擎线程也称为 JS 内核,负责处理 Javascript 脚本程序,解析 Javascript 脚本,运行代码;JS 引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页中无论什么时候都只有一个 JS 引擎线程在运行 JS 程序;


注意:GUI 渲染线程与 JS 引擎线程的互斥关系,所以如果 JS 执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。


(3)时间触发线程 时间触发线程属于浏览器而不是 JS 引擎,用来控制事件循环;当 JS 引擎执行代码块如 setTimeOut 时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理;


注意:由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行);


(4)定时器触发进程 定时器触发进程即 setInterval 与 setTimeout 所在线程;浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;


注意:W3C 在 HTML 标准中规定,定时器的定时时间不能小于 4ms,如果是小于 4ms,则默认为 4ms。


(5)异步 http 请求线程


  • XMLHttpRequest 连接后通过浏览器新开一个线程请求;

  • 检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待 JS 引擎空闲后执行;

viewport

<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />    // width    设置viewport宽度,为一个正整数,或字符串‘device-width’    // device-width  设备宽度    // height   设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置    // initial-scale    默认缩放比例(初始缩放比例),为一个数字,可以带小数    // minimum-scale    允许用户最小缩放比例,为一个数字,可以带小数    // maximum-scale    允许用户最大缩放比例,为一个数字,可以带小数    // user-scalable    是否允许手动缩放
复制代码


  • 延伸提问

  • 怎样处理 移动端 1px 被 渲染成 2px问题


局部处理


  • meta标签中的 viewport属性 ,initial-scale 设置为 1

  • rem按照设计稿标准走,外加利用transfromescale(0.5) 缩小一倍即可;


全局处理


  • mate标签中的 viewport属性 ,initial-scale 设置为 0.5

  • rem 按照设计稿标准走即可

DNS 协议是什么

概念: DNS 是域名系统 (Domain Name System) 的缩写,提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。它是一个由分层的 DNS 服务器组成的分布式数据库,是定义了主机如何查询这个分布式数据库的方式的应用层协议。能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。


作用: 将域名解析为 IP 地址,客户端向 DNS 服务器(DNS 服务器有自己的 IP 地址)发送域名查询请求,DNS 服务器告知客户机 Web 服务器的 IP 地址。

Css3 新特性

1.过渡 transition2.动画 animation3.形状转换 transform4.阴影 box-shadow5.滤镜 Filter6.颜色 rgba7.栅格布局 gird8.弹性布局 flex等等还多...
复制代码

浏览器

浏览器架构

单进程浏览器时代


单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含了网络、插件、JavaScript 运行环境、渲染引擎和页面等。其实早在 2007 年之前,市面上浏览器都是单进程的



  • 缺点

  • 不稳定:一个插件的意外崩溃会引起整个浏览器的崩溃

  • 不流畅:所有页面的渲染模块、JavaScript 执行环境以及插件都是运行在同一个线程中的,这就意味着同一时刻只能有一个模块可以执行

  • 不安全:可以通过浏览器的漏洞来获取系统权限,这些脚本获取系统权限之后也可以对你的电脑做一些恶意的事情,同样也会引发安全问题

  • 以上这些就是当时浏览器的特点,不稳定,不流畅,而且不安全


多进程浏览器时代


  • 由于进程是相互隔离的,所以当一个页面或者插件崩溃时,影响到的仅仅是当前的页面进程或者插件进程,并不会影响到浏览器和其他页面,这就完美地解决了页面或者插件的崩溃会导致整个浏览器崩溃,也就是不稳定的问题

  • JavaScript 也是运行在渲染进程中的,所以即使 JavaScript 阻塞了渲染进程,影响到的也只是当前的渲染页面,而并不会影响浏览器和其他页面,因为其他页面的脚本是运行在它们自己的渲染进程中的

  • Chrome 把插件进程和渲染进程锁在沙箱里面,这样即使在渲染进程或者插件进程里面执行了恶意程序,恶意程序也无法突破沙箱去获取系统权限。



最新的 Chrome 浏览器包括:1个浏览器(Browser)主进程1个 GPU 进程1个网络(NetWork)进程多个渲染进程多个插件进程


  • 浏览器进程 。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程 。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程 。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程 。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程 。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响

如何高效操作 DOM

1. 为什么说 DOM 操作耗时


1.1 线程切换


  • 浏览器为了避免两个引擎同时修改页面而造成渲染结果不一致的情况,增加了另外一个机制,这两个引擎具有互斥性,也就是说在某个时刻只有一个引擎在运行,另一个引擎会被阻塞。操作系统在进行线程切换的时候需要保存上一个线程执行时的状态信息并读取下一个线程的状态信息,俗称上下文切换。而这个操作相对而言是比较耗时的

  • 每次 DOM 操作就会引发线程的上下文切换——从 JavaScript 引擎切换到渲染引擎执行对应操作,然后再切换回 JavaScript 引擎继续执行,这就带来了性能损耗。单次切换消耗的时间是非常少的,但是如果频繁地大量切换,那么就会产生性能问题


比如下面的测试代码,循环读取一百万次 DOM 中的 body 元素的耗时是读取 JSON 对象耗时的 10 倍。


// 测试次数:一百万次const times = 1000000// 缓存body元素console.time('object')let body = document.body// 循环赋值对象作为对照参考for(let i=0;i<times;i++) {  let tmp = body}console.timeEnd('object')// object: 1.77197265625ms
console.time('dom')// 循环读取body元素引发线程切换for(let i=0;i<times;i++) { let tmp = document.body}console.timeEnd('dom')// dom: 18.302001953125ms
复制代码


1.2 重新渲染


另一个更加耗时的因素是元素及样式变化引起的再次渲染,在渲染过程中最耗时的两个步骤为重排(Reflow)与重绘(Repaint)


浏览器在渲染页面时会将 HTML 和 CSS 分别解析成 DOM 树和 CSSOM 树,然后合并进行排布,再绘制成我们可见的页面。如果在操作 DOM 时涉及到元素、样式的修改,就会引起渲染引擎重新计算样式生成 CSSOM 树,同时还有可能触发对元素的重新排布和重新绘制


  • 可能会影响到其他元素排布的操作就会引起重排,继而引发重绘

  • 修改元素边距、大小

  • 添加、删除元素

  • 改变窗口大小

  • 引起重绘

  • 设置背景图片

  • 修改字体颜色

  • 改变 visibility属性值


了解更多关于重绘和重排的样式属性,可以参看这个网址:https://csstriggers.com/ (opens new window)


2. 如何高效操作 DOM


明白了 DOM 操作耗时之后,要提升性能就变得很简单了,反其道而行之,减少这些操作即可


2.1 在循环外操作元素


比如下面两段测试代码对比了读取 1000 次 JSON 对象以及访问 1000 次 body 元素的耗时差异,相差一个数量级


const times = 10000;console.time('switch')for (let i = 0; i < times; i++) {  document.body === 1 ? console.log(1) : void 0;}console.timeEnd('switch') // 1.873046875msvar body = JSON.stringify(document.body)console.time('batch')for (let i = 0; i < times; i++) {  body === 1 ? console.log(1) : void 0;}console.timeEnd('batch') // 0.846923828125ms
复制代码


2.2 批量操作元素


比如说要创建 1 万个 div 元素,在循环中直接创建再添加到父元素上耗时会非常多。如果采用字符串拼接的形式,先将 1 万个 div 元素的 html 字符串拼接成一个完整字符串,然后赋值给 body 元素的 innerHTML 属性就可以明显减少耗时


const times = 10000;console.time('createElement')for (let i = 0; i < times; i++) {  const div = document.createElement('div')  document.body.appendChild(div)}console.timeEnd('createElement')// 54.964111328125msconsole.time('innerHTML')let html=''for (let i = 0; i < times; i++) {  html+='<div></div>'}document.body.innerHTML += html // 31.919921875msconsole.timeEnd('innerHTML')
复制代码

说一下 slice splice split 的区别?

// slice(start,[end])// slice(start,[end])方法:该方法是对数组进行部分截取,该方法返回一个新数组// 参数start是截取的开始数组索引,end参数等于你要取的最后一个字符的位置值加上1(可选)。// 包含了源函数从start到 end 所指定的元素,但是不包括end元素,比如a.slice(0,3);// 如果出现负数就把负数与长度相加后再划分。// slice中的负数的绝对值若大于数组长度就会显示所有数组// 若参数只有一个,并且参数大于length,则为空。// 如果结束位置小于起始位置,则返回空数组// 返回的个数是end-start的个数// 不会改变原数组var arr = [1,2,3,4,5,6]/*console.log(arr.slice(3))//[4,5,6] 从下标为0的到3,截取3之后的数console.log(arr.slice(0,3))//[1,2,3] 从下标为0的地方截取到下标为3之前的数console.log(arr.slice(0,-2))//[1,2,3,4]console.log(arr.slice(-4,4))//[3,4]console.log(arr.slice(-7))//[1,2,3,4,5,6]console.log(arr.slice(-3,-3))// []console.log(arr.slice(8))//[]*/// 个人总结:slice的参数如果是正数就从左往右数,如果是负数的话就从右往左边数,// 截取的数组与数的方向一致,如果是2个参数则截取的是数的交集,没有交集则返回空数组 // ps:slice也可以切割字符串,用法和数组一样,但要注意空格也算字符
// splice(start,deletecount,item)// start:起始位置// deletecount:删除位数// item:替换的item// 返回值为被删除的字符串// 如果有额外的参数,那么item会插入到被移除元素的位置上。// splice:移除,splice方法从array中移除一个或多个数组,并用新的item替换它们。//举一个简单的例子 var a=['a','b','c']; var b=a.splice(1,1,'e','f'); console.log(a) //['a', 'e', 'f', 'c'] console.log(b) //['b']
var a = [1, 2, 3, 4, 5, 6];//console.log("被删除的为:",a.splice(1, 1, 8, 9)); //被删除的为:2// console.log("a数组元素:",a); //1,8,9,3,4,5,6
// console.log("被删除的为:", a.splice(0, 2)); //被删除的为:1,2// console.log("a数组元素:", a) //3,4,5,6console.log("被删除的为:", a.splice(1, 0, 2, 2)) //插入 第二个数为0,表示删除0个 console.log("a数组元素:", a) //1,2,2,2,3,4,5,6
// split(字符串)// string.split(separator,limit):split方法把这个string分割成片段来创建一个字符串数组。// 可选参数limit可以限制被分割的片段数量。// separator参数可以是一个字符串或一个正则表达式。// 如果separator是一个空字符,会返回一个单字符的数组,不会改变原数组。var a="0123456"; var b=a.split("",3); console.log(b);//b=["0","1","2"]// 注意:String.split() 执行的操作与 Array.join 执行的操作是相反的。
复制代码


用户头像

loveX001

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
前端高频面试题合集(中高级必备)_JavaScript_loveX001_InfoQ写作社区