美团前端常见面试题整理
Object.assign()
描述:Object.assign()
方法用于将所有可枚举(Object.propertyIsEnumerable()
返回 true
)和自有(Object.hasOwnProperty()
返回 true
)属性的值从一个或多个源对象复制到目标对象。它将返回修改后的目标对象(请注意这个操作是浅拷贝)。
实现:
setTimeout、Promise、Async/Await 的区别
(1)setTimeout
(2)Promise
Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作, 会先执行 then/catch 等,当主栈完成后,才会去调用 resolve/reject 中存放的方法执行,打印 p 的时候,是打印的返回结果,一个 Promise 实例。
当 JS 主线程执行到 Promise 对象时:
promise1.then() 的回调就是一个 task
promise1 是 resolved 或 rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
(3)async/await
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
例如:
func1 的运行结果其实就是一个 Promise 对象。因此也可以使用 then 来处理后续逻辑。
await 的含义为等待,也就是 async 函数需要等待 await 后的函数执行完成并且有了返回结果(Promise 对象)之后,才能继续执行下面的代码。await 通过返回一个 Promise 对象来实现同步的效果。
代码输出问题
输出结果:1 undefined 2
解析:
console.log(new A().a),new A()为构造函数创建的对象,本身没有 a 属性,所以向它的原型去找,发现原型的 a 属性的属性值为 1,故该输出值为 1;
console.log(new B().a),ew B()为构造函数创建的对象,该构造函数有参数 a,但该对象没有传参,故该输出值为 undefined;
console.log(new C(2).a),new C()为构造函数创建的对象,该构造函数有参数 a,且传的实参为 2,执行函数内部,发现 if 为真,执行 this.a = 2,故属性 a 的值为 2。
如何解决跨越问题
(1)CORS
下面是 MDN 对于 CORS 的定义:
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain)上的 Web 应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
CORS 需要浏览器和服务器同时支持,整个 CORS 过程都是浏览器完成的,无需用户参与。因此实现 CORS 的关键就是服务器,只要服务器实现了 CORS 请求,就可以跨源通信了。
浏览器将 CORS 分为简单请求和非简单请求:
简单请求不会触发 CORS 预检请求。若该请求满足以下两个条件,就可以看作是简单请求:
1)请求方法是以下三种方法之一:
HEAD
GET
POST
2)HTTP 的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
若不满足以上条件,就属于非简单请求了。
(1)简单请求过程:
对于简单请求,浏览器会直接发出 CORS 请求,它会在请求的头信息中增加一个 Orign 字段,该字段用来说明本次请求来自哪个源(协议+端口+域名),服务器会根据这个值来决定是否同意这次请求。如果 Orign 指定的域名在许可范围之内,服务器返回的响应就会多出以下信息头:
如果 Orign 指定的域名不在许可范围之内,服务器会返回一个正常的 HTTP 回应,浏览器发现没有上面的 Access-Control-Allow-Origin 头部信息,就知道出错了。这个错误无法通过状态码识别,因为返回的状态码可能是 200。
在简单请求中,在服务器内,至少需要设置字段:Access-Control-Allow-Origin
(2)非简单请求过程
非简单请求是对服务器有特殊要求的请求,比如请求方法为 DELETE 或者 PUT 等。非简单请求的 CORS 请求会在正式通信之前进行一次 HTTP 查询请求,称为预检请求。
浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些 HTTP 请求方式和头信息字段,只有得到肯定的回复,才会进行正式的 HTTP 请求,否则就会报错。
预检请求使用的请求方法是 OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是 Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:
Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法。
Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段。
服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有 Access-Control-Allow-Origin 这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。
服务器回应的 CORS 的字段如下:
只要服务器通过了预检请求,在以后每次的 CORS 请求都会自带一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。
在非简单请求中,至少需要设置以下字段:
减少 OPTIONS 请求次数:
OPTIONS 请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少 OPTIONS 请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的 URL 的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。
CORS 中 Cookie 相关问题:
在 CORS 请求中,如果想要传递 Cookie,就要满足以下三个条件:
在请求中设置
withCredentials
默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.
Access-Control-Allow-Credentials 设置为 true
Access-Control-Allow-Origin 设置为非
*
(2)JSONP
jsonp 的原理就是利用<script>
标签没有跨域限制,通过<script>
标签 src 属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而前端拿到 callback 函数返回的数据。1)原生 JS 实现:
服务端返回如下(返回时即执行全局函数):
2)Vue axios 实现:
后端 node.js 代码:
JSONP 的缺点:
具有局限性, 仅支持 get 方法
不安全,可能会遭受 XSS 攻击
(3)postMessage 跨域
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:
页面和其打开的新窗口的数据传递
多窗口之间消息传递
页面与嵌套的 iframe 消息传递
上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数:
data: html5 规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用 JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
1)a.html:(domain1.com/a.html)
2)b.html:(domain2.com/b.html)
(4)nginx 代理跨域
nginx 代理跨域,实质和 CORS 跨域原理一样,通过配置文件设置请求响应头 Access-Control-Allow-Origin…等字段。
1)nginx 配置解决 iconfont 跨域浏览器跨域访问 js、css、img 等常规静态资源被同源策略许可,但 iconfont 字体文件(eot|otf|ttf|woff|svg)例外,此时可在 nginx 的静态资源服务器中加入以下配置。
2)nginx 反向代理接口跨域跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用 HTTP 接口只是使用 HTTP 协议,不需要同源策略,也就不存在跨域问题。实现思路:通过 Nginx 配置一个代理服务器域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域访问。
nginx 具体配置:
(5)nodejs 中间件代理跨域
node 中间件实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中域名,实现当前域的 cookie 写入,方便接口登录认证。
1)非 vue 框架的跨域 使用 node + express + http-proxy-middleware 搭建一个 proxy 服务器。
前端代码:
中间件服务器代码:
2)vue 框架的跨域
node + vue + webpack + webpack-dev-server 搭建的项目,跨域请求接口,直接修改 webpack.config.js 配置。开发环境下,vue 渲染服务和接口代理服务都是 webpack-dev-server 同一个,所以页面与代理接口之间不再跨域。
webpack.config.js 部分配置:
(6)document.domain + iframe 跨域
此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。1)父窗口:(domain.com/a.html)
1)子窗口:(child.domain.com/a.html)
(7)location.hash + iframe 跨域
实现原理:a 欲与 b 跨域相互通信,通过中间页 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。
具体实现:A 域:a.html -> B 域:b.html -> A 域:c.html,a 与 b 不同域只能通过 hash 值单向通信,b 与 c 也不同域也只能单向通信,但 c 与 a 同域,所以 c 可通过 parent.parent 访问 a 页面所有对象。
1)a.html:(domain1.com/a.html)
2)b.html:(.domain2.com/b.html)
(8)window.name + iframe 跨域
window.name 属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
1)a.html:(domain1.com/a.html)
2)proxy.html:(domain1.com/proxy.html)
中间代理页,与 a.html 同域,内容为空即可。3)b.html:(domain2.com/b.html)
通过 iframe 的 src 属性由外域转向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
(9)WebSocket 协议跨域
WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。
原生 WebSocket API 使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。
1)前端代码:
2)Nodejs socket 后台:
数组扁平化
数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:
现在就是要实现 flat 这种效果。
ES5 实现:递归。
ES6 实现:
Node 中的 Event Loop 和浏览器中的有什么区别?process.nextTick 执行顺序?
Node 中的 Event Loop 和浏览器中的是完全不相同的东西。
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。
(2)Pending callbacks:执行推迟到下一个循环迭代的 I / O 回调(系统调用相关的回调)。
(3)Idle/Prepare:仅供内部使用。
(4)Poll(轮询阶段):
当回调队列不为空时:会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回调后,变为下面的情况。
当回调队列为空时(没有回调或所有回调执行完毕):但如果存在有计时器(setTimeout、setInterval 和 setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的 I/O 操作完成,并马上执行相应的回调,直到所有回调执行完毕。
(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。
(6)Close callbacks:执行一些关闭回调,比如 socket.on('close', ...)等。
下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机的
对于以上代码来说,setTimeout
可能执行在前,也可能执行在后
首先
setTimeout(fn, 0) === setTimeout(fn, 1)
,这是由源码决定的进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行
setTimeout
回调那么如果准备时间花费小于 1ms,那么就是
setImmediate
回调先执行了
当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:
在上述代码中,setImmediate
永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate
回调,所以就直接跳转到 check 阶段去执行回调了。
上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,
对于以上代码来说,其实和浏览器中的输出是一样的,microtask 永远执行在 macrotask 前面。
最后来看 Node 中的 process.nextTick
,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
对于以上代码,永远都是先把 nextTick 全部打印出来。
参考 前端进阶面试题详细解答
如何提⾼webpack 的打包速度?
(1)优化 Loader
对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。
首先我们优化 Loader 的文件搜索范围
对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules
中使用的代码都是编译过的,所以完全没有必要再去处理一遍。
当然这样做还不够,还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间
(2)HappyPack
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
(3)DllPlugin
DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin 的使用方法如下:
然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin
将依赖文件引入项目中
(4)代码压缩
在 Webpack3 中,一般使用 UglifyJS
来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin
来并行运行 UglifyJS
,从而提高效率。
在 Webpack4 中,不需要以上这些操作了,只需要将 mode
设置为 production
就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log
这类代码的功能。
(5)其他
可以通过一些小的优化点来加快打包速度
resolve.extensions
:用来表明文件后缀列表,默认查找顺序是['.js', '.json']
,如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面resolve.alias
:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径module.noParse
:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助
如何减少 Webpack 打包体积
(1)按需加载
在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash
这种大型类库同样可以使用这个功能。
按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise
,当 Promise
成功以后去执行回调。
(2)Scope Hoisting
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
比如希望打包两个文件:
对于这种情况,打包出来的代码会类似这样:
但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:
这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules
就可以了:
(3)Tree Shaking
Tree Shaking 可以实现删除项目中未被引用的代码,比如:
对于以上情况,test
文件中的变量 b
如果没有在项目中使用到的话,就不会被打包到文件中。
如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。
常见的 HTTP 请求头和响应头
HTTP Request Header 常见的请求头:
Accept:浏览器能够处理的内容类型
Accept-Charset:浏览器能够显示的字符集
Accept-Encoding:浏览器能够处理的压缩编码
Accept-Language:浏览器当前设置的语言
Connection:浏览器与服务器之间连接的类型
Cookie:当前页面设置的任何 Cookie
Host:发出请求的页面所在的域
Referer:发出请求的页面的 URL
User-Agent:浏览器的用户代理字符串
HTTP Responses Header 常见的响应头:
Date:表示消息发送的时间,时间的描述格式由 rfc822 定义
server:服务器名称
Connection:浏览器与服务器之间连接的类型
Cache-Control:控制 HTTP 缓存
content-type:表示后面的文档属于什么 MIME 类型
常见的 Content-Type 属性值有以下四种:
(1)application/x-www-form-urlencoded:浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。
(2)multipart/form-data:该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。
(3)application/json:服务器消息主体是序列化后的 JSON 字符串。
(4)text/xml:该种方式主要用来提交 XML 格式的数据。
React 事务机制
左右居中方案
行内元素:
text-align: center
定宽块状元素: 左右
margin
值为auto
不定宽块状元素:
table
布局,position + transform
原函数形参不定长(此时 fn.length
为 0)
TCP 粘包是怎么回事,如何处理?
默认情况下, TCP 连接会启⽤延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到⼀起作⼀次发送 (缓冲⼤⼩⻅ socket.bufferSize ), 这样可以减少 IO 消耗提⾼性能.
如果是传输⽂件的话, 那么根本不⽤处理粘包的问题, 来⼀个包拼⼀个包就好了。但是如果是多条消息, 或者是别的⽤途的数据那么就需要处理粘包.
下面看⼀个例⼦, 连续调⽤两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下⼏种常⻅的情况:A. 先接收到 data1, 然后接收到 data2 .B. 先接收到 data1 的部分数据, 然后接收到 data1 余下的部分以及 data2 的全部.C. 先接收到了 data1 的全部数据和 data2 的部分数据, 然后接收到了 data2 的余下的数据.D. ⼀次性接收到了 data1 和 data2 的全部数据.
其中的 BCD 就是我们常⻅的粘包的情况. ⽽对于处理粘包的问题, 常⻅的解决⽅案有:
多次发送之前间隔⼀个等待时间:只需要等上⼀段时间再进⾏下⼀次 send 就好, 适⽤于交互频率特别低的场景. 缺点也很明显, 对于⽐较频繁的场景⽽⾔传输效率实在太低,不过⼏乎不⽤做什么处理.
关闭 Nagle 算法:关闭 Nagle 算法, 在 Node.js 中你可以通过 socket.setNoDelay() ⽅法来关闭 Nagle 算法, 让每⼀次 send 都不缓冲直接发送。该⽅法⽐较适⽤于每次发送的数据都⽐较⼤ (但不是⽂件那么⼤), 并且频率不是特别⾼的场景。如果是每次发送的数据量⽐较⼩, 并且频率特别⾼的, 关闭 Nagle 纯属⾃废武功。另外, 该⽅法不适⽤于⽹络较差的情况, 因为 Nagle 算法是在服务端进⾏的包合并情况, 但是如果短时间内客户端的⽹络情况不好, 或者应⽤层由于某些原因不能及时将 TCP 的数据 recv, 就会造成多个包在客户端缓冲从⽽粘包的情况。 (如果是在稳定的机房内部通信那么这个概率是⽐较⼩可以选择忽略的)
进⾏封包/拆包: 封包/拆包是⽬前业内常⻅的解决⽅案了。即给每个数据包在发送之前, 于其前/后放⼀些有特征的数据, 然后收到数据的时 候根据特征数据分割出来各个数据包。
懒加载与预加载的区别
这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户需要访问时,再去加载,这样可以提高网站的首屏加载速度,提升用户的体验,并且可以减少服务器的压力。它适用于图片很多,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,如果图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。
预加载指的是将所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。 通过预加载能够减少用户的等待时间,提高用户的体验。我了解的预加载的最常用的方式是使用 js 中的 image 对象,通过为 image 对象来设置 scr 属性,来实现图片的预加载。
内存泄露
意外的全局变量: 无法被回收
定时器: 未被正确关闭,导致所引用的外部变量无法被释放
事件监听: 没有正确销毁 (低版本浏览器可能出现)
闭包
第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
第二种情况是我们设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
第三种情况是我们获取一个 DOM 元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。
dom
引用:dom
元素被删除时,内存中的引用未被正确清空控制台
console.log
打印的东西
可用
chrome
中的timeline
进行内存标记,可视化查看内存的变化情况,找出异常点。
nextTick
nextTick
可以让我们在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
nextTick
主要使用了宏任务和微任务。根据执行环境分别尝试采用
Promise
MutationObserver
setImmediate
如果以上都不行则采用
setTimeout
定义了一个异步方法,多次调用
nextTick
会将方法存入队列中,通过这个异步方法清空当前队列
渲染机制
1. 浏览器如何渲染网页
概述:浏览器渲染一共有五步
处理
HTML
并构建DOM
树。处理
CSS
构建CSSOM
树。将
DOM
与CSSOM
合并成一个渲染树。根据渲染树来布局,计算每个节点的位置。
调用
GPU
绘制,合成图层,显示在屏幕上
第四步和第五步是最耗时的部分,这两步合起来,就是我们通常所说的渲染
具体如下图过程如下图所示
渲染
网页生成的时候,至少会渲染一次
在用户访问的过程中,还会不断重新渲染
重新渲染需要重复之前的第四步(重新生成布局)+第五步(重新绘制)或者只有第五个步(重新绘制)
在构建
CSSOM
树时,会阻塞渲染,直至CSSOM
树构建完成。并且构建CSSOM
树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的CSS
选择器,执行速度越慢当
HTML
解析到script
标签时,会暂停构建DOM
,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载JS
文件。并且CSS
也会影响JS
的执行,只有当解析完样式表才会执行JS
,所以也可以认为这种情况下,CSS
也会暂停构建DOM
2. 浏览器渲染五个阶段
2.1 第一步:解析 HTML 标签,构建 DOM 树
在这个阶段,引擎开始解析
html
,解析出来的结果会成为一棵dom
树dom
的目的至少有2
个
作为下个阶段渲染树状图的输入
成为网页和脚本的交互界面。(最常用的就是
getElementById
等等)
当解析器到达 script 标签的时候,发生下面四件事情
html
解析器停止解析,如果是外部脚本,就从外部网络获取脚本代码
将控制权交给
js
引擎,执行js
代码恢复
html
解析器的控制权
由此可以得到第一个结论 1
由于
<script>
标签是阻塞解析的,将脚本放在网页尾部会加速代码渲染。defer
和async
属性也能有助于加载外部脚本。defer
使得脚本会在dom
完整构建之后执行;async
标签使得脚本只有在完全available
才执行,并且是以非阻塞的方式进行的
2.2 第二步:解析 CSS 标签,构建 CSSOM 树
我们已经看到
html
解析器碰到脚本后会做的事情,接下来我们看下html
解析器碰到样式表会发生的情况js
会阻塞解析,因为它会修改文档(document
)。css
不会修改文档的结构,如果这样的话,似乎看起来css
样式不会阻塞浏览器html
解析。但是事实上css
样式表是阻塞的。阻塞是指当cssom
树建立好之后才会进行下一步的解析渲染
通过以下手段可以减轻 cssom 带来的影响
将
script
脚本放在页面底部尽可能快的加载
css
样式表将样式表按照
media type
和media query
区分,这样有助于我们将css
资源标记成非阻塞渲染的资源。非阻塞的资源还是会被浏览器下载,只是优先级较低
2.3 第三步:把 DOM 和 CSSOM 组合成渲染树(render tree)
2.4 第四步:在渲染树的基础上进行布局,计算每个节点的几何结构
布局(
layout
):定位坐标和大小,是否换行,各种position
,overflow
,z-index
属性
2.5 调用 GPU 绘制,合成图层,显示在屏幕上
将渲染树的各个节点绘制到屏幕上,这一步被称为绘制
painting
3. 渲染优化相关
3.1 Load 和 DOMContentLoaded 区别
Load
事件触发代表页面中的DOM
,CSS
,JS
,图片已经全部加载完毕。DOMContentLoaded
事件触发代表初始的HTML
被完全加载和解析,不需要等待CSS
,JS
,图片加载
3.2 图层
一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用。
通过以下几个常用属性可以生成新图层
3D
变换:translate3d
、translateZ
will-change
video
、iframe
标签通过动画实现的
opacity
动画转换position: fixed
3.3 重绘(Repaint)和回流(Reflow)
重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大
重绘是当节点需要更改外观而不会影响布局的,比如改变
color
就叫称为重绘回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流
以下几个动作可能会导致性能问题
改变
window
大小改变字体
添加或删除样式
文字改变
定位或者浮动
盒模型
很多人不知道的是,重绘和回流其实和 Event loop 有关
当
Event loop
执行完Microtasks
后,会判断document
是否需要更新。因为浏览器是60Hz
的刷新率,每16ms
才会更新一次。然后判断是否有
resize
或者scroll
,有的话会去触发事件,所以resize
和scroll
事件也是至少16ms
才会触发一次,并且自带节流功能。判断是否触发了
media query
更新动画并且发送事件
判断是否有全屏操作事件
执行
requestAnimationFrame
回调执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好更新界面
以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback
回调
常见的引起重绘的属性
color
border-style
visibility
background
text-decoration
background-image
background-position
background-repeat
outline-color
outline
outline-style
border-radius
outline-width
box-shadow
background-size
3.4 常见引起回流属性和方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发重排,下面列一些栗子
添加或者删除可见的
DOM
元素;元素尺寸改变——边距、填充、边框、宽度和高度
内容变化,比如用户在
input
框中输入文字浏览器窗口尺寸改变——
resize
事件发生时计算
offsetWidth
和offsetHeight
属性设置
style
属性的值
回流影响的范围
由于浏览器渲染界面是基于流失布局模型的,所以触发重排时会对周围 DOM 重新排列,影响的范围有两种
全局范围:从根节点
html
开始对整个渲染树进行重新布局。局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
全局范围回流
当
p
节点上发生reflow
时,hello
和body
也会重新渲染,甚至h5
和ol
都会收到影响
局部范围回流
用局部布局来解释这种现象:把一个
dom
的宽高之类的几何信息定死,然后在dom
内部触发重排,就只会重新渲染该dom
内部的元素,而不会影响到外界
3.5 减少重绘和回流
使用
translate
替代top
使用
visibility
替换display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局)把
DOM
离线后修改,比如:先把DOM
给display:none
(有一次Reflow)
,然后你修改100
次,然后再把它显示出来不要把
DOM
结点的属性值放在一个循环里当成循环里的变量
不要使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用
requestAnimationFrame
CSS
选择符从右往左匹配查找,避免DOM
深度过深将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。比如对于
video
标签,浏览器会自动将该节点变为图层。
僵尸进程和孤儿进程是什么?
孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
meta 标签:自动刷新/跳转
假设要实现一个类似 PPT 自动播放的效果,你很可能会想到使用 JavaScript 定时器控制页面跳转来实现。但其实有更加简洁的实现方法,比如通过 meta 标签来实现:
上面的代码会在 5s 之后自动跳转到同域下的 page2.html 页面。我们要实现 PPT 自动播放的功能,只需要在每个页面的 meta 标签内设置好下一个页面的地址即可。
另一种场景,比如每隔一分钟就需要刷新页面的大屏幕监控,也可以通过 meta 标签来实现,只需去掉后面的 URL 即可:
meta viewport 相关
掌握页面的加载过程
网页加载流程
当我们打开网址的时候,浏览器会从服务器中获取到 HTML 内容
浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素
<head>
元素内容会先被解析,此时浏览器还没开始渲染页面
我们看到
<head>
元素里有用于描述页面元数据的<meta>
元素,还有一些<link>
元素涉及外部资源(如图片、CSS 样式
等),此时浏览器会去获取这些外部资源。除此之外,我们还能看到<head>
元素中还包含着不少的<script>
元素,这些<script>
元素通过src
属性指向外部资源当浏览器解析到这里时(步骤 3),会暂停解析并下载 JavaScript 脚本
当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,控制权会交回给渲染引擎,渲染引擎继续往下解析
HTML
页面此时
<body>
元素内容开始被解析,浏览器开始渲染页面
在这个过程中,我们看到
<head>
中放置的<script>
元素会阻塞页面的渲染过程:把 JavaScript 放在<head>
里,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面
。如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,用户体验会变得很糟糕
因此,对于对性能要求较高、需要快速将内容呈现给用户的网页,常常会将 JavaScript 脚本放在
<body>
的最后面。这样可以避免资源阻塞,页面得以迅速展示
。我们还可以使用defer/async/preload
等属性来标记<script>
标签,来控制 JavaScript 的加载顺序
延迟加载的方式有哪些
js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。
几种方式是:
将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行
给 js 脚本添加
defer
属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了defer
属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样给 js 脚本添加
async
属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个async
属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行动态创建
DOM
标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建script
标签来引入 js 脚本
怎么判断页面是否加载完成
Load
事件触发代表页面中的DOM
,CSS
,JS
,图片已经全部加载完毕。DOMContentLoaded
事件触发代表初始的HTML
被完全加载和解析,不需要等待CSS
,JS
,图片加载
评论