1、Js事件循环(Event Loop)机制
juejin.cn/post/684490… (很细致,文章很赞)
2、关于dom
破解前端面试(80% 应聘者不及格系列):从 DOM 说起juejin.cn/post/684490…
3、事件委托, 标准 DOM 事件的发生流程
一、事件机制
事件是在编程时系统内发生的动作或者发生的事情,系统会在事件出现的时候触发某种信号并且会提供一个自动加载某种动作的机制(来自MDN)。
每个事件都有事件处理器(有时也叫事件监听器),也就是触发事件时运行的代码块。严格来说事件监听器监听事件是否发生,然后事件处理器对事件做出反应。
二、DOM事件流
事件传播是一种机制,用于定义事件如何传播或通过DOM树传播,事件传播有两种方式:事件捕获(Capture)和事件冒泡(Bubble)。
事件传播形式上有三个阶段:
- 捕获阶段:从窗口进入事件目标阶段
- 目标阶段: 目标阶段
- 冒泡阶段:从事件目标回到窗口
但是,目标极端在现代浏览器中没有单独处理,所以当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。
三、事件捕获
事件发生时,在捕获阶段,事件从窗口向下通过DOM树传播到目标节点,即从最外层元素(祖先元素)触发事件响应函数,逐级往下,直到目标元素。(从外到内)
如果目标元素的任何祖先(即父、祖父等)和目标本身具有针对该类型事件专门注册的捕获事件侦听器,则这些侦听器将在捕获阶段执行。
四、事件冒泡
在事件冒泡阶段,正好相反。
事件冒泡模式流程:事件发生时,先触发目标元素(最直接元素)的事件响应函数,然后触发其父元素的事件响应函数,并逐级上溯到祖先元素。(从内到外)
五、W3C事件模型
因为有捕获和冒泡两种传播方式,W3C制定了一个标准可以让我们自己选择使用哪种传播方式addEventListener('click',fn,?)
第三个参数?是一个bool值,决定使用捕获或者冒泡。
当你addEventListener函数第三个参数为true时就表示你使用的是事件捕获。父级元素先触发,子级元素后触发。
当你addEventListener函数第三个参数为空或为false时就表示你使用的是事件冒泡。子级元素先触发,父级元素后触发。
六、target vs currentTarget
e.target 用户操作的元素
e.currentTarget 程序员监听的元素
this是e.currentTarget,不推荐使用
例:
div>span{文字},用户点击文字
e.target就是span
this是e.currentTarget就是div
七、阻止事件传播
在嵌套的元素中,并且每个元素都有事件处理程序时,当单击内部元素,所有处理程序都将同时执行,因为事件会出现在DOM树中。
为了防止这种情况,可以使用**event.stopPropagation()**方法停止事件冒泡。
<div id="div1" style="border: 1px solid red; width: 100px; height: 100px;">
<div id="div2" style="border: 1px solid blue; width: 50px; height: 50px;"></div>
</div>
<script>
hi.addEventListener("click", function(){
console.log('div1')
});
hello.addEventListener("click", function(e){
console.log('div2')
e.stopPropagation()
});
</script>
因为在子元素点击事件中使用了event.stopPropagation()阻止冒泡事件,所以最终只打印出了目标元素'div2',父元素的'div1'并没有被打印出。
八、阻止默认事件
有些事件具有与之关联的默认操作。例如点击一个链接浏览器带你到链接的目标,点击一个表单提交按钮浏览器提交表单等等。
可以使用事件对象的preventDefault()方法来防止此类默认操作。但是,阻止默认操作并不会停止事件传播,事件像往常一样继续传播到DOM树。
<a id='div1' href='https://baidu.com'>点击跳转</a>
<script>
a.addEventListener("click", function(e){
e.preventDefault();
});
</script>
我们给a添加点击事件,当用户点击点击跳转就阻止a标签的默认事件,所以点击后不会有跳转。
九、是否可以阻止冒泡
并不是所有事件都可以阻止冒泡的,具体可以MDN搜索scroll event,看到Bubbles和Cancelable
Bubbles的意思是该事件是否冒泡
Cancelable的意思是开发者是否可以阻止冒泡
event.target & event.currentTarget
e.target指向事件触发的元素e.currentTarget指向事件绑定的元素
十、事件委托
事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
事件委托:不监听元素 C 自身,而是监听其祖先元素 P,然后判断 e.target 是不是该元素 C(或该元素的子元素)
阻止默认动作:e.preventDefault() 或者 return false
阻止冒泡:e.stopPropagation()
优点:
省监听数,减少内存消耗
<div id="div1">
<button>click 1</button>
<button>click 2</button>
<button>click 3</button>
<button>click 4</button>
<button>click 5</button>
</div>
<script>
div1.addEventListener('click', (e)=> {
//把目标元素赋给t
const t = e.target
// 判断是否匹配目标元素
if (t.tagName.toLowerCase() === 'button') {
console.log('button内容是:' + t.textContent);
}
});
</script>
可以监听动态元素(不存在的元素)
<div id="div1">
</div>
<script>
setTimeout(()=>{
//div1里面添加一个button
const button = document.creatElement('button')
button.textContent = 'click 1'
div1.appendChild(button)
},1000)
div1.addEventListener('click',(e)=>{
const t=e.target
if (t.tagName.toLowerCase() ==='button'){
console.log('button被click')
}
});
</script>
封装事件委托
<div id="div1">
</div>
<script>
setTimeout(()=>{
const button = document.creatElement('button')
button.textContent = 'click 1'
div1.appendChild(button)
},1000)
on('click','#div1','button',()=>{
console.log('button被点击了')
})
function on(eventType, element, selector, fn){
//判断如果element不是元素
if(!(element instanceof Element)){
element = document.querySelector(element)
}
element.addEventListener(eventType,(e)=>{
const t = e.target
//matches判断一个元素是否满足一个选择器
if(t.matches(selector)){
fn(e)
}
})
}
</script>
4、requestAnimationFrame 和 polyfill
5、DocumentFragment
DocumentFragment对象是什么?
MDN解释:
DocumentFragment 表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为DocumentFragment不是真实DOM树的一部分,它的变化不会引起DOM树的重新渲染的操作(reflow回流) ,且不会导致性能等问题。---MDN
W3C解释:
DocumentFragment 接口表示文档的一部分(或一段)。更确切地说,它表示一个或多个邻接的 Document 节点和它们的所有子孙节点。 DocumentFragment 节点不属于文档树,继承的 parentNode 属性总是 null。 不过它有一种特殊的行为,该行为使得它非常有用,即当请求把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作,尤其是与 Range 接口一起使用时更是如此。 可以用 Document.createDocumentFragment() 方法创建新的空 DocumentFragment 节点。---W3C
怎么去理解呢?我们结合上面两解释可以得知,DocumentFragment 节点不属于DOM树,因此它的变化不会引起DOM树的变化;
我们知道,DOM树的操作会引起回流,那我们可以将DocumentFragment作为一个暂时的DOM节点存储器,当我们在DocumentFragment 修改完成时,我们就可以将存储DOM节点的DocumentFragment一次性加入DOM树,从而减少回流次数,达到性能优化的目的。
DocumentFragment对象怎么用?
我们可以使用document.createDocumentFragment()创建一个DocumentFragment,每个新建的DocumentFragment都会继承所有node方法。且DocumentFragment拥有nodeValue,nodeName,nodeType属性。
let fragment = document.createDocumentFragment();
console.log(fragment.nodeValue); //null
console.log(fragment.nodeName); //#document-fragment
console.log(fragment.nodeType); //11
使用DocumentFragment能解决直接操作DOM引发大量回流的问题,比如我们要给ul添加五个li节点,区别就像这样:
6、DOM元素尺寸offsetWidth,scrollWidth,clientWidth等详解
7、前端常见跨域解决方案(全)
8、防抖
9、怎么快速定位哪个组件出现性能问题
当面试官问这个问题,没有 get 到面试官的点,扯了一堆乱七八糟没用的 - -。 后来面试官说主要是用 timeline 工具。 大意是通过 timeline 来查看每个函数的调用时常,定位出哪个函数的问题,从而能判断哪个组件出了问题。
附上两个使用 timeline 的文章:
10、前端模块化:CommonJS,AMD,CMD,ES6
11、HTTP HTTPs协议
最后,简而概之,HTTP/2的通过支持请求与响应的多路复用来减少延迟,通过压缩HTTP首部字段将协议开销降至最低,同时增加对请求优先级和服务器端推送的支持。
HTTP/2性能得到了极大的提升,我们在HTTP 1.1时代做的有些优化反而成了鸡肋,在升级过程中,如何让HTTP/2 和 HTTP 1.1的用户都能得到最优的性能,这是对于我们的另外一大挑战。
12、浏览器工作机制全面梳理(从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理)进程和线程
13、浏览器缓存知识小结及应用
14、loaded和domcontentloaded
DOMContentLoaded顾名思义,就是dom内容加载完毕。那什么是dom内容加载完毕呢?我们从打开一个网页说起。当输入一个URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成。
这时候问题又来了,什么是HTML文档被加载和解析完成。要解决这个问题,我们就必须了解浏览器渲染原理。
当我们在浏览器地址输入URL时,浏览器会发送请求到服务器,服务器将请求的HTML文档发送回浏览器,浏览器将文档下载下来后,便开始从上到下解析,解析完成之后,会生成DOM。如果页面中有css,会根据css的内容形成CSSOM,然后DOM和CSSOM会生成一个渲染树,最后浏览器会根据渲染树的内容计算出各个节点在页面中的确切大小和位置,并将其绘制在浏览器上。
上面我们看到在解析html的过程中,html的解析会被中断,这是因为javascript会阻塞dom的解析。当解析过程中遇到
同时javascript的执行会受到标签前面样式文件的影响。如果在标签前面有样式文件,需要样式文件加载并解析完毕后才执行脚本。这是因为javascript可以查询对象的样式。
这里需要注意一点,在现在浏览器中,为了减缓渲染被阻塞的情况,现代的浏览器都使用了猜测预加载。当解析被阻塞的时候,浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来。
在这里我们可以明确DOMContentLoaded所计算的时间,当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。
接下来,我们来说说load,页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发。
我们在 jQuery 中经常使用的 (document).load(function() { // ...代码... }); 监听的是 load 事件。在用jquery的时候,我们一般都会将函数调用写在ready方法内,就是页面被解析后,我们就可以访问整个页面的所有dom元素,可以缩短页面的可交互时间,提高整个页面的体验。
下面我们在来看看如何实现这两个函数
1、onload事件
onload事件所有的浏览器都支持,所以我们不需要什么兼容,只要通过调用
window.onload = function(){
}
2、DOMContentLoaded 事件
DOMContentLoaded不同的浏览器对其支持不同,所以在实现的时候我们需要做不同浏览器的兼容。
1)支持DOMContentLoaded事件的,就使用DOMContentLoaded事件;
2)IE6、IE7不支持DOMContentLoaded,但它支持onreadystatechange事件,该事件的目的是提供与文档或元素的加载状态有关的信息。
- 更低的ie还有个特有的方法doScroll, 通过间隔调用:document.documentElement.doScroll("left");
可以检测DOM是否加载完成。 当页面未加载完成时,该方法会报错,直到doScroll不再报错时,就代表DOM加载完成了。该方法更接近DOMContentLoaded的实现。
最后我们来回答这个问题:我们为什么一再强调将css放在头部,将js文件放在尾部
在面试的过程中,经常会有人在回答页面的优化中提到将js放到body标签底部,原因是因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不会影响前面的页面的渲染。那么问题来了,既然Dom树完全生成好后页面才能渲染出来,浏览器又必须读完全部HTML才能生成完整的Dom树,script标签不放在body底部是不是也一样,因为dom树的生成需要整个文档解析完毕。
我们再来看一下chrome在页面渲染过程中的,绿色标志线是First Paint的时间。纳尼,为什么会出现firstpaint,页面的paint不是在渲染树生成之后吗?其实现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有HTML解析之前开始构建和布局渲染树。部分的内容将被解析并显示。也就是说浏览器能够渲染不完整的dom树和cssom,尽快的减少白屏的时间。假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间。
15、从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!
zhuanlan.zhihu.com/p/34453198?…
16、如何理解getComputedStyle
17、回流,重绘,浏览器渲染原理
**可以总结浏览器的渲染过程:blog.csdn.net/qq\_4629917…
**
1、通过HTML Parser,解析HTML,生产DOM树,通过CSS Parser,解析CSS,生产CSSOM树
2、将DOM树和CSSOM树通过Attachment结合,生成渲染树(Render Tree)
3、根据生成的渲染树,进行Layout,得到节点的几何信息(位置,大小) (这个节点信息都是可见节点)
4、根据渲染树以及Layout得到的几何信息,通过Painting得到节点的绝对像素
5、在Painting后,渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理。然后展示在页面上
回流是什么?何时发生回流?
回流:当浏览器发现某个部分发生了页面布局和几何信息的变化,就需要倒回去重新渲染了,重新渲染,就又要经过Layout计算可见节点在设备视口(viewport)内的几何信息,以及之后的Paiting和Display将这些信息渲染在页面上
这个重新计算可见节点的在设备视口(viewport)内的几何信息并渲染的过程就被称为回流
回流会从html这个根节点开始递归往下,依次计算所有可见节点的几何信息,且回流是无可避免的
总结一下:回流其实就是当浏览器中某个部分的页面布局或者几何信息发生变化时,就又会重新执行浏览器渲染的Layout和Paiting以及Display过程
只要行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都引起企它内部、周围甚至整个页面的重新渲染:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
重绘是什么?何时发生重绘?
重绘:改变某个元素的背景色、文字颜色、边框颜色等不影响它周围或内部布局的属性时,就会重新执行浏览器的Paiting和Display过程(元素的几何信息没有变)
只要是不改变元素的几何信息的都只会重绘:
- 改变元素的背景色
- 改变文字颜色
- 改变边框颜色
回流一定重绘,重绘不一定回流
在前面我们知道,回流是触发了浏览器渲染过程中的Layout,Paiting,Display,而重绘是触发了浏览器渲染过程中的Paiting,Display 也就是说回流的过程包括了重绘,如果触发了回流,那么必定重绘会随着浏览器渲染过程一起发生,所以回流一定重绘 而重绘的发生,并不会执行Layout,因此不一定会产生回流,所以重绘不一定回流
关于display:none和visibility:hidden
display:none 的节点是不可见的,因此不会被加入Render Tree的,而visibility:hidden的节点会被加入Render Tree
display:none 改为 display:block 时,算是增加了一个可见节点,因此会重新渲染,所以触发回流,而visibility:hidden,节点是已经在Render Tree中的,所以会触发重绘
浏览器的队列优化机制
因为每一次的回流与重绘都是非常消耗性能的,所以大多数浏览器引入了异步队列的形式,当改变元素样式时,并不会立刻回流或重绘一次,而是把这些操作加入到队列中,在经过至少一个浏览器刷新时间(16.6ms)或者队列中操作到达了一个阈值,才会清空队列,执行一次回流或重绘,这叫做异步重绘或增量异步重绘
但是,当执行一些获取布局信息的操作时,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。比如以下属性和方法:
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight width、height
getComputedStyle()
getBoundingClientRect
所以,应该避免频繁的使用上述的属性和方法,因为它们都会强制渲染并刷新队列。
如何减少回流与重绘?(分别从CSS和JS两个层面去解答)
从CSS层面减少回流与重绘
使用visibility代替display:none
因为visibility只会触发一次重绘,而display:none会触发回流(回流必重绘)
避免使用table表格布局
因为table的一个小改动都可能造成整个table的回流,而table及其内部元素的几何信息计算,是可能需要多次计算的,通常要比平常元素多花3倍时间!
一些复杂动画效果,使用绝对定位让其脱离文档流
一些动画效果,可能会频繁的改变几何信息(位置,尺寸,大小)和样式,所以会频繁的触发回流与重绘,因此可以使用绝对定位使其脱离文档流,这样就不会影响其他元素的布局,这样就减少了其影响其他元素布局从而产生的回流。
尽可能在DOM树的最末端改变class
回流是不可避免的,但可以通过在末端改变class,这样回流涉及到节点会变得很少。
CSS3硬件加速
CSS3 硬件加速又叫做 GPU 加速,是利用 GPU 进行渲染,减少 CPU 操作的一种优化方案。由于 GPU 中的 transform 等 CSS 属性不会触发 重绘,所以能大大提高网页的性能。
CSS 中的以下几个属性能触发硬件加速:
- transform
- opacity
- filter
- will-change
注意:GPU 渲染会影响字体的抗锯齿效果。这是因为 GPU 和 CPU 具有不同的渲染机制,即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。
创建独立图层
在GPU渲染过程中,一些元素会因为一些规则,被提升到独立的层,而独立出来后,是不会影响其他DOM的布局,就像脱离文档流一样,所以可以利用这一特性,将频繁变换的DOM元素主动提升到独立图层中,那么就可以减少Layout和Paiting的时间了
可以使用以下规则创建独立图层:
使用加速视频解码的 video 元素。
拥有 3D(WebGL) 上下文或者加速 2D 上下文的 canvas 元素。
3D 或者透视变换(perspective,transform) 的 CSS 属性。
混合插件(Flash)。 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素。
拥有加速 CSS 过滤器的元素。
元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)。
元素有一个兄弟元素在复合图层渲染,并且该兄弟元素的 z-index 较小,那这个元素也会被应用到复合图层。
避免设置多层内联样式
CSSOM树是树的数据结构,因此查找样式时,是递归去查找的,而这个递归过程是很复杂的,因此我们应该尽量少写过于具体的选择器和少添加无意义的标签
从JS层面减少回流和重绘
尽量减少多次修改DOM样式
最好一次性修改DOM的样式
const el = document.querySelector('#el')
el.style.backgroundColor = 'skyblue'
el.style.opicity = 0.5
el.style.width = 300 + 'px'
在之前提到过,浏览器是会将这些重绘与回流操作加入到队列中的,所以可能其最后只会执行一次,但是这里我改变了width,也就是会强制渲染并清空队列的属性,这时候就会触发三次回流与重绘,因此尽可能的将多次修改DOM样式合并成一次
可以使用cssText将其合并:
const el = document.querySelector('#el')
el.style.cssText += 'background-color: skyblue; opicity: 0.5; width: 300px;';
避免频繁操作DOM
当我们操作DOM时,可以使用以下方法来减少回流与重绘
- 使其脱离文档流
- 进行DOM操作
- 将其添加回文档流中
脱离文档流和添加回文档这两次回流是无可避免的,但是中间的DOM操作,则是在Render Tree之外进行的,因此不会产生任何的回流与重绘
而脱离文档流,可以用两种方法
- display:none 使其节点不可见,因此能脱离渲染树
- documentFragment 文档片段
操作都很简单,就是display:none后或创建文档片段后对其进行DOM操作,之后再将其display:block和添加到文档中
display的伪代码:
div.style.display = 'none' // 节点不可见,触发一次回流
div.style.width = 200 + 'px' // 此时为不可见节点,脱离了Render Tree,因此不会产生回流和重绘
div.style.height = 300 + 'px'
div.style.display = 'block' // 节点可见,触发一次回流
documentFragment的伪代码:
const fragment = document.createDocumentFragment() // 触发一次回流
// 对fargment 进行DOM操作
div.appendChild(fragment) // 将文档片段插进DOM中,触发一次回流
避免频繁读取会引发回流/重绘的属性以及避免触发同步布局事件
访问一些引发回流或重绘甚至强制刷新队列的属性不能太频繁
18、CPU 和GPU
CPU:叫做中央处理器(central processing unit)作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元
GPU:叫做图形处理器。图形处理器(英语:Graphics Processing Unit,缩写:GPU),又称显示核心、视觉处理器、显示芯片,是一种专门在个人电脑、工作站、游戏机和一些移动设备(如平板电脑、智能手机等)上做图像和图形相关运算工作的微处理器
19、普通文档流,absolute文档流,复合图层的区别
www.dazhuanlan.com/jul324/topi…
浏览器渲染的图层一般包含两大类:普通图层以及复合图层
首先这个图层是对于硬件来说的,也就是 GPU。在默认的渲染过程中,都只有一个图层,就是一个默认的复合层。
无论有几个文档流,怎么添加元素都是在这个复合层中渲染的。
当然,我们可以通过开启硬件加速,开启一个新的复合图层。这样在 GPU 中,会对这个新的图层进行单独绘制。
不同的图层的绘制也是互不影响的,同时这样也是会脱离文档流的。
如何开启硬件加速
在浏览器中,如果打开的 3d 变化,浏览器会自动开启硬件加速 (开启一个新的复合图层)。
最常用的方式:translate3d、translateZ。
很多时候在本身没有使用 3D 变化的时候,可以用transform: translateZ(0)来使用。
还有就是opacity属性/过渡动画
注意事项
在使用硬件加速的时候,我们应该尽可能的规定 index,防止后面的元素也创建复合图层。
因为如果开启硬件加速的元素图层 index 较低,在它之后的 index 层级的元素也会创建复合图层。我们应该避免这种情况的出现
作用
开启硬件加速,当然可以使页面的动画效果加载的更加流畅。但是,也不能过度的使用,浪费资源,造成卡顿。
同时本身还脱离的文档流,大量的变动也不会引起整个页面重绘,回流
20、浏览器渲染原理
21、渲染过程中遇到js文件怎么处理?
JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。
JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建。
原本DOM和CSSOM的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。
这是什么情况?
这是因为JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。前面我们介绍,不完整的CSSOM是无法使用的,但JavaScript中想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。
22、async 和 defer的作用是什么? 有什么区别
接下来我们对比下 defer 和 async 属性的区别:
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
1)情况1
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
2)情况2 异步加载
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
3)情况3 延迟执行
defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
重点:DOMContentLoaded和load?
**www.cnblogs.com/gg-qq/p/113…
**
**DOMContentLoaded:**当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载。
load: load 仅用于检测一个完全加载的页面,页面的html、css、js、图片等资源都已经加载完之后才会触发 load 事件
为什么操作 DOM 慢?
因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
渲染页面时常见哪些不良现象?
FOUC:由于浏览器渲染机制(比如firefox),再CSS加载之前,先呈现了HTML,就会导致展示出无样式内容,然后样式突然呈现的现象;
白屏:有些浏览器渲染机制(比如chrome)要先构建DOM树和CSSOM树,构建完成后再进行渲染,如果CSS部分放在HTML尾部,由于CSS未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把js文件放在头部,脚本会阻塞后面内容的呈现,脚本会阻塞其后组件的下载,出现白屏问题。
总结:
- 浏览器工作流程:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。
- CSSOM会阻塞渲染,只有当CSSOM构建完毕后才会进入下一个阶段构建渲染树。
- 通常情况下DOM和CSSOM是并行构建的,但是当浏览器遇到一个script标签时,DOM构建将暂停,直至脚本完成执行。但由于JavaScript可以修改CSSOM,所以需要等CSSOM构建完毕后再执行JS。
- 如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,建议将 script 标签放在 body 标签底部。
23、JS引擎解析流程
引擎对JS的处理过程可以简述如下:
-
读取代码,进行词法分析(Lexical analysis),然后将代码分解成词元(token)
-
对词元进行语法分析(parsing),然后将代码整理成语法树(syntax tree)
-
使用翻译器(translator),将代码转为字节码(bytecode)
-
使用字节码解释器(bytecode interpreter),将字节码转为机器码
最终计算机执行的就是机器码。
回收机制
JS有垃圾处理器,所以无需手动回收内存,而是由垃圾处理器自动处理。
一般来说,垃圾处理器有自己的回收策略。
譬如对于那些执行完毕的函数,如果没有外部引用(被引用的话会形成闭包),则会回收。(当然一般会把回收动作切割到不同的时间段执行,防止影响性能)
常用的两种垃圾回收规则是:
标记清除 引用计数
Javascript引擎基础GC方案是(simple GC):mark and sweep(标记清除),简单解释如下:
遍历所有可访问的对象。
回收已不可访问的对象。
譬如:(出自javascript高程)
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。
从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
而当变量离开环境时,则将其标记为“离开环境”。
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包,也就是说在环境中的以及相关引用的变量会被去除标记)。
而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
关于引用计数,简单点理解:
跟踪记录每个值被引用的次数,当一个值被引用时,次数+1,减持时-1,下次垃圾回收器会回收次数为0的值的内存(当然了,容易出循环引用的bug)
GC的缺陷
和其他语言一样,javascript的GC策略也无法避免一个问题: GC时,停止响应其他操作
这是为了安全考虑。
而Javascript的GC在100ms甚至以上
对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。
这就是引擎需要优化的点: 避免GC造成的长时间停止响应。
GC优化策略
这里介绍常用到的:分代回收(Generation GC)
目的是通过区分“临时”与“持久”对象:
多回收“临时对象”区(young generation)
少回收“持久对象”区(tenured generation)
减少每次需遍历的对象,从而减少每次GC的耗时。
像node v8引擎就是采用的分代回收(和java一样,作者是java虚拟机作者。)
http 2.0
http2.0不是https,它相当于是http的下一代规范(譬如https的请求可以是http2.0规范的)
然后简述下http2.0与http1.1的显著不同点:
- http1.1中,每请求一个资源,都是需要开启一个tcp/ip连接的,所以对应的结果是,每一个资源对应一个tcp/ip请求,由于tcp/ip本身有并发数限制,所以当资源一多,速度就显著慢下来
- http2.0中,一个tcp/ip请求可以请求多个资源,也就是说,只要一次tcp/ip请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。
所以,如果http2.0全面应用,很多http1.1中的优化方案就无需用到了(譬如打包成精灵图,静态资源多域名拆分等)
然后简述下http2.0的一些特性:
-
多路复用(即一个tcp/ip连接可以请求多个资源)
-
首部压缩(http头部压缩,减少体积)
-
二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
-
服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
-
请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)
单独拎出来的缓存问题,http的缓存
缓存可以简单的划分成两种类型:强缓存(200 from cache)与协商缓存(304)
区别简述如下:
- 强缓存(
200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求 - 协商缓存(
304)时,浏览器会向服务端发起http请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存
对于协商缓存,使用Ctrl + F5强制刷新可以使得缓存无效
但是对于强缓存,在未过期时,必须更新资源路径才能发起新的请求(更改了路径相当于是另一个资源了,这也是前端工程化中常用到的技巧)
缓存头部简述
上述提到了强缓存和协商缓存,那它们是怎么区分的呢?
答案是通过不同的http头部控制
先看下这几个头部:
If-None-Match/E-tag、
If-Modified-Since/Last-Modified、
Cache-Control/Max-Age、Pragma/Expires
属于协商缓存控制的:
(http1.1)If-None-Match/E-tag
(http1.0)If-Modified-Since/Last-Modified
If-Modified-Since/Last-Modified:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是If-Modified-Since,而服务端的是Last-Modified,它的作用是,在发起请求时,如果If-Modified-Since和Last-Modified匹配,那么代表服务器资源并未改变,因此服务端不会返回资源实体,而是只返回头部,通知浏览器可以使用本地缓存。**Last-Modified**,顾名思义,指的是文件最后的修改时间,而且只能精确到**1s**以内
If-None-Match/E-tag:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是If-None-Match,而服务端的是E-tag,同样,发出请求后,如果If-None-Match和E-tag匹配,则代表内容未变,通知浏览器使用本地缓存,和Last-Modified不同,E-tag更精确,它是类似于指纹一样的东西,基于**FileEtag INode Mtime Size**生成,也就是说,只要文件变,指纹就会变,而且没有1s精确度的限制。(其实有点类似于版本号)
24、网站中有三个js文件大小超过100KB,超级影响加载速度,想问怎样优化?
1. 将你的JS文件进行压缩,百度一下,工具一把
2. 服务器端开启gzip压缩
3. 如果你是3个单独的JS,那么你可以合成1个文件,减少请求次数
4. 将你的
9. 如果你还是觉得很慢,那就玩些高级玩意儿吧(如使用CDN加速等)
10. 上面方法都试过,还是不奏效?那我也没辙了,除非能现场分析。
25、ts 面试题目
**26、**原型 / 构造函数 / 实例
1、原型(prototype): 一个简单的对象,用于实现对象的 属性继承。可以简单的理解成对象的爹。在 Firefox 和 Chrome 中,每个JavaScript对象中都包含一个__proto__ (非标准)的属性指向它爹(该对象的原型),可obj.__proto__进行访问。
2、构造函数: 可以通过new来 新建一个对象 的函数。
3、实例: 通过构造函数和new创建出来的对象,便是实例。 实例通过__proto__指向原型,通过constructor指向构造函数。
说了一大堆,大家可能有点懵逼,这里来举个栗子,以Object为例,我们常用的Object便是一个构造函数,因此我们可以通过它构建实例。
// 实例
const instance = new Object()
则此时, 实例为instance, 构造函数为Object,我们知道,构造函数拥有一个prototype的属性指向原型,因此原型为:
// 原型
const prototype = Object.prototype
这里我们可以来看出三者的关系:
实例.__proto__ === 原型
原型.constructor === 构造函数
构造函数.prototype === 原型
// 这条线其实是是基于原型进行获取的,可以理解成一条基于原型的映射线
// 例如:
// const o = new Object()
// o.constructor === Object --> true
// o.__proto__ = null;
// o.constructor === Object --> false
// 注意: 其实实例上并不是真正有 constructor 这个指针,它其实是从原型链上获取的
// instance.hasOwnProperty('constructor') === false (感谢 刘博海 Brian 童鞋🥳)
实例.constructor === 构造函数
放大来看,我画了张图供大家彻底理解:
27、js中基本数据类型和引用数据类型的区别
1、基本数据类型和引用数据类型
ECMAScript包括两个不同类型的值:基本数据类型和引用数据类型。
基本数据类型指的是简单的数据段,引用数据类型指的是有多个值构成的对象。
当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值。
2、常见的基本数据类型:
**Number、String 、Boolean、Null和Undefined。**基本数据类型是按值访问的,因为可以直接操作保存在变量中的实际值。示例:
var a = 10;var b = a; b = 20;console.log(a); // 10值
上面,b获取的是a值得一份拷贝,虽然,两个变量的值相等,但是两个变量保存了两个不同的基本数据类型值。 b只是保存了a复制的一个副本。所以,b的改变,对a没有影响。
下图演示了这种基本数据类型赋值的过程:
3、引用类型数据:
也就是对象类型Object type,比如:Object 、Array 、Function 、Data等。
javascript的引用数据类型是保存在堆内存中的对象。
与其他语言的不同是,你不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。
/所以,引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象。
var obj1 = new Object(); var obj2 = obj1; obj2.name = "我有名字了"; console.log(obj1.name); // 我有名字了
说明这两个引用数据类型指向了同一个堆内存对象。obj1赋值给onj2,实际上这个堆内存对象在栈内存的引用地址复制了一份给了obj2,
但是实际上他们共同指向了同一个堆内存对象。实际上改变的是堆内存对象。
下面我们来演示这个引用数据类型赋值过程:
4、总结区别
a 声明变量时不同的内存分配:
1)原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。
这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。
2)引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。
这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。
地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。
b 不同的内存分配机制也带来了不同的访问机制
1)在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,
首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。
2)而原始类型的值则是可以直接访问到的。
c 复制变量时的不同
1)原始值:在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
2)引用值:在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,
也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。
(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)。多了一个指针
d 参数传递的不同(把实参复制给形参的过程)
首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。
但是为什么涉及到原始类型与引用类型的值时仍然有区别呢?还不就是因为内存分配时的差别。
1)原始值:只是把变量里的值传递给参数,之后参数和这个变量互不影响。
2)引用值:对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心!
因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。
29、模块化
模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持。
-
分类:
- es6:
import / export - commonjs:
require / module.exports / exports - amd:
require / defined
- es6:
-
require与import的区别require支持 动态导入,import不支持,正在提案 (babel 下可支持)require是 同步 导入,import属于 异步 导入require是 值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化
**30、**数组(array)
1、map: 遍历数组,返回回调返回值组成的新数组
2、forEach: 无法break,可以用try/catch中throw new Error来停止
3、filter: 过滤
4、some: 有一项返回true,则整体为true
5、every: 有一项返回false,则整体为false
6、join: 通过指定连接符生成字符串
7、push / pop: 末尾推入和弹出,改变原数组, push 返回数组长度, pop 返回原数组最后一项;
8、unshift / shift: 头部推入和弹出,改变原数组,unshift 返回数组长度,shift 返回原数组第一项 ;
9、sort(fn) / reverse: 排序与反转,改变原数组
10、concat: 连接数组,不影响原数组, 浅拷贝
11、slice(start, end): 返回截断后的新数组,不改变原数组
12、splice(start, number, value...): 返回删除元素组成的数组,value 为插入项,改变原数组
13、indexOf / lastIndexOf(value, fromIndex): 查找数组项,返回对应的下标
14、reduce / reduceRight(fn(prev, cur), defaultPrev): 两两执行,prev 为上次化简函数的return值,cur 为当前值
-
-
当传入
defaultPrev时,从第一项开始; -
当未传入时,则为第二项
-
15、数组乱序:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
arr.sort(function () {
return Math.random() - 0.5;
});
16、数组拆解: flat: [1,[2,3]] --> [1, 2, 3]
Array.prototype.flat = function() {
return this.toString().split(',').map(item => +item )
}
31、原型、原型链、作用域、作用域链、闭包
什么是原型、原型链?
原型:相当于一个模具,用来生产实例对象。
原型链:原型对象有个指针指向构造函数,实例对象又有一个指针指向原型对象,就形成了一条原型链,最终指向null。
为什么存在?
原型:就是js里实现面向对象的方式,也就是说,js就是基于原型的面向对象。
原型链:是js实现继承的方式。
作用域、作用域链?
什么是作用域、作用域链?
- 作用域:所谓作用域,就是变量或者是函数能作用的范围。
那么JavaScript里有什么作用域呢?
1、全局作用域
除了函数中定义的变量之外,都是全局作用域。
举个栗子:
var a = 10;
function bar(){
console.log(a);
}
bar();//10
以上的a就是全局变量,到处可以访问a。
然鹅,
var a = 10;
function bar(){
console.log(a);
var a = 20;
}
bar();//undefined
什么鬼?undefined?
是的,你没看错。因为先搜索函数的变量看是否存在a,存在,又由于a被预解析(变量提升),提升的a绑定了这里的a作用域,所以结果就是undefined。
2、局部作用域
函数里用var声明的变量。
举个栗子:
var a = 10;
function bar(){
var a = 20;
console.log(a);
}
bar();//20
3、没有块级作用域(至ES5),ES6中有块级作用域
ES6之前,除了函数之外的块都不具备块级作用域。
常见的经典例子:
for(var i=0;i<4;i++){
setTimeout(function(){
console.log(i);
},200);
}
//4 4 4 4
解决办法:
for(var i=0;i<4;i++){
(function(j){
setTimeout(function(){
console.log(j);
},200);
})(i)
}
//0 1 2 3
- 作用域链
变量随着作用长辈函数一级一级往上搜索,直到找到为止,找不到就报错,这个过程就是作用域链起的作用。
var num = 30;
function f1(){
var num = 20;
function f2(){
var num = 10;
function f3(){
var num = 5;
console.log(num);
}
f3();
}
f2();
}
f1();
闭包
闭包:js里为了实现数据和方法私有化的方式。内层函数可以调用外层函数的变量和方法。
经典的面试题
如果有这样的需求
- go('l') -> gol
- go()('l') -> gool
- go()()('l') -> goool
-
var go = function (a) { var str = 'go'; var add0 = function (a) { str += 'o'; return a ? str += a : add0;// 巧妙使用 } return a ? str += a : add0;// 巧妙使用 } console.log(go('l'));//gol console.log(go()('l'));//gool console.log(go()()('l'));//goool
继承
既然前面说到继承的问题。继承指的是一个对象可以共享父级对象的一些属性。那么为什么需要继承?比如上文的问题中,形状Shape有顶点这个属性,三角形和矩形都可以继承该属性而不需要再重新定义。那么就ES6以前跟ES6以后JavaScript中实现继承的问题来聊聊吧。
- ES6之前
组合继承
function Parent(name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.getName = function() {
return this.name;
}
function child(name, age, sex) {
Parent.call(this, name, age);
this.sex = sex;
}
child.prototype = new Parent()
var c1 = new child('zenquan', '23', 'M')
console.log(c1.getName())
console.log(c1)
这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
function parent(name, age) {
this.name = name;
this.age = age;
}
parent.prototype.getName = function() {
return this.name;
}
function child(name, age, sex) {
parent.call(this, name, age);
this.sex = sex;
}
child.prototype = Object.create(parent.prototype, {
constructor: {
value: child,
enumerable: true,
writable: true,
configurable: true
}
})
var c1 = new child('zenquan', 23, 'M');
console.log(c1.getName())
console.log(c1)
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
ES6之后class继承
以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承,并且实现起来很简单
class parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
getName() {
return this.name;
}
}
class child extends parent {
constructor(name, age, sex) {
super(name, age);
this.sex = sex;
}
}
var c1 = new child('zenquan', 23, 'M');
console.log(c1);