js高级程序设计
第一章:JavaScript
简介
从了解JavaScript
开始
JavaScript
由核心(ECMAScript
)、文档对象模型(DOM
)以及浏览器对象模型(BOM
)组成。
1. ECMAScript
:提供核心语言功能
- 官方描述:由
ECMA-262
定义,提供核心语言功能。它与浏览器没有依赖关系,浏览器是该语言的宿主环境之一。宿主环境不仅提供基本的ECMAScript
实现,同时也会提供该语言的扩展(DOM
、BOM
),以便语言与环境之间对接交互,从而能够构建更完善的脚本语言。- 简单地说:
ECMAScript
是JavaScript
的核心,ECMAScript
规定了其基本语法,它并不依赖于浏览器等宿主环境,但可被浏览器等宿主环境识别并执行。
规定的内容
- 语法
- 类型
- 语句
- 关键字
- 保留字
- 操作符
- 对象
ECMAScript
兼容
要想成为ECMAScript
的实现,必须要做到:
- 支持
ECMA-262
规范描述的所有"类型、值、对象、属性、函数以及程序语法和语句" - 支持
Unicode
字符标准(为了能够支持多语言开发)
兼容的实现还可以进行以下扩展:
- 添加
ECMA-262
没有描述的"更多类型、值、对象、属性和函数",主要是指标准中没有规定的新对象和对象的新属性 - 支持
ECMA-262
没有定义的"程序和正则表达式语法",即可以修改和扩展内置的正则表达式语法
2. DOM(Document Object Model)
:提供访问和操作网页内容的方法和接口
- 针对
XML
单经过扩展用于HTML
的应用程序编程接口。DOM
把整个页面映射未一个多层节点结构。HTML
或XML
页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。- 通过
DOM
创建的表示文档的属性图,开发人员获得了控制页面内容和结构的主动权。借助DOM
提供的API
,可以对DOM
树中的任何节点进行操作。
<!--本段代码将映射为下面的结构-->
<html>
<head>
<title>hello</title>
</head>
<body>
<p>hello world!</p>
</body>
</html>
DOM
级别
3. BOM(Browser Object Model)
:提供与浏览器交互的方法和接口
- 没有
BOM
标准可遵循,故每个浏览器都有自己的实现。虽然也存在一些事实标准,例如要有window
对象和navigator
对象等,但是每个浏览器都会为这两个对象乃至其他对象定义自己的属性和方法。BOM
根本上只处理浏览器窗口和框架,但习惯上也把所有针对浏览器的JS
扩展算为BOM
的一部分,下面是一些扩展:
- 弹出新浏览器窗口的功能
- 移动、缩放和关闭浏览器窗口的功能
- 提供浏览器详细信息的
navigator
对象 - 提供浏览器所加载页面的详细信息的
location
对象 - 提供用户显示器分辨率详细信息的
screen
对象 - 对
cookies
的支持 - 像
XMLHttpRequest
和IE
的ActiveXObject
这样的自定义对象
本章后记
读本章时有想到
js
性能优化方面的内容,而对浏览器加载、解析、渲染具体过程并不是很了解,于是有了下面内容,从下面内容出发再去思考如何去进行性能优化
说在前面
重绘和重排
DOM
树中的每一个需要显示的节点在渲染树中至少存在一个对应的节点(隐藏的DOM
元素在渲染树中没有对应的节点)。渲染树中的节点被称为"帧"(frames
)或"盒"(boxes
),符合CSS
模型的定义,理解页面元素为一个具有内边距padding
,外边距(margin
),边框(borders
)和位置(position
)的盒子。一旦DOM Tree
和Render Tree
构建完成,浏览器就开始绘制(paint
)元素。- 当
DOM
的变化影响了元素的几何属性(宽和高)–比如改变边框宽度或给段落增加文字,导致元素的宽高变化—浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使得渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排(reflow
)。完成重排后,浏览器会重新绘制受影响部分到屏幕中,这个过程称为重绘(repaint
)。- 并不是所有变化都会导致
reflow
。例如,改变某个元素的背景颜色时,只会发生一次重绘(不会发生重排),因为该元素的位置和大小没有发生改变,整体布局也不会改变。(应尽量减少重排和重绘,它们都是代价比较高昂的操作,尤其是重排,重排会导致部分页面布局发生改变,其代价更高)
1. 重排(回流)什么时候发生
当下列其中之一情况出现时便会引起重排,根据其改变的范围和程度,渲染树中被影响的部分都要重新进行计算。有些改变甚至会引起整个页面重排(eg:滚动条出现时)
- 添加或删除可见的
DOM
元素时 - 元素位置改变时
- 元素的尺寸改变时(eg:内边距、外边距、宽、高、
border
宽度) - 内容改变时(eg:文本增加/减少、字体变化等文本改变或图片被另一个不同尺寸的图片替代,在文本框中输入文字时)
- 浏览器窗口尺寸改变时
- 页面渲染器初始化(必要的重排)
- CSS伪类的激活(:hover)
- 查询属性或调用方法(见下面)
- 设置
style
属性的值
2. 重排影响的范围
由于浏览器渲染界面是基于流式布局模型的,故一个元素的改变会影响其他元素,影响的范围有两种:
- 全局范围:从根节点
html
开始对Render Tree
各个节点进行渲染(例如:滚动条出现时)- 局部范围:对
Render Tree
的某一部分或某一个节点进行重新布局
<!--当对p节点进行操作时,`hello`以及`body`也会重新渲染,甚至`h5`和`ul`都会受到影响-->
<body>
<div class="hello">
<h4>你好呀</h4>
<p>name:zyy</p>
<h5>女生</h5>
<ul>
<li>18</li>
</ul>
</div>
</body>
3. 渲染队列
由于每次重排都会产生计算消耗,故大多数浏览器通过队列化修改并批量执行来优化重排过程。但是当我们使用下列方法/读取下列属性(一般获取布局相关信息时,也包含所有样式属性)时,会强制刷新队列并要求计划任务立即执行。也就是说:浏览器会将会导致重排或重绘的任务都放入渲染队列中,待队列中的任务多了起来/经过一定时间间隔后会进行批量处理。而当我们需要访问布局相关信息时,是要求要返回最新的(及时性和准确性)信息,故会立刻执行渲染队列中的"待处理变化"并触发重排以返回正确信息,即使是在获取最近未发生改变的或者与最新改变无关的属性,使用下列属性都会刷新渲染队列。(完整导致重排属性请见是什么促使布局/回流)
扫描二维码关注公众号,回复: 11547104 查看本文章
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
getComputedStyle()
(currentStyle
in IE)
设置下列属性或调用下列方法会引起重排(改变了元素的布局相关信息)
width
、height
、margin
、padding
display
、border
、position
、overflow
scrollIntoView()
、scrollTo()
在下面代码中,使用方法1会进行三次重排:方法1修改bodyStyle
修改了三次,在每一次修改后都读取了computed
的一个属性,虽然读取的属性都与bodyStyle
无关,但是会强制刷新队列并重排。
使用方法2只会进行一次重排:在调用computed.backgroundColor
方法后,强制刷新队列并重排,但是当第2,3次获取computed
的属性值时,队列已经为空,没有任务可执行,此时不会再进行重排,直接读取。
//定义变量并获取样式
let computed, tmp = "", bodyStyle = document.body.style;
if (document.body.currentStyle) {
//IE,Opera
computed = document.body.currentStyle;
} else {
computed = window.getComputedStyle(document.body, '');
}
//方法1
bodyStyle.color = "red";
tmp = computed.backgroundColor;
bodyStyle.color="green";
tmp = computed.backgroundImage;
bodyStyle.color="blue";
tmp = computed.backgroundAttachment;
//方法2
bodyStyle.color = "red";
bodyStyle.color="green";
bodyStyle.color="blue";
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;
4. 如何减少重排和重绘
改变样式
若再改变样式时需要减少重排和重绘,则重点是尽量减少对元素样式修改的次数,即合并修改。
- 使用
cssText
—行内样式 - 使用
class
—添加类
下面这段代码不仅访问了三次DOM
元素,并且(最坏的情况下,虽然没人这么做hhh)若在每一次改变样式时都请求布局信息,则会触发三次重排。
let el = document.getElementById("mydiv");
el.style.borderLeft = "1px";
el.style.color = "red";
el.style.padding="5px";
//方法1
el.style.cssText+="border-left:1px;color:red;padding:5px";
//方法2
.active {
border-left: 1px;
color: red;
padding: 5px;
}
el.className="active";
批量修改DOM
如果需要对
DOM
进行一些操作,可使用下面的方式减少重排和重绘(只会在步骤①和步骤③发生两次重排)。如果不采用①③将元素先脱离文档流再带回文档,在步骤②中改变多少次就会发生多少次重排(最坏情况下)。
① 使元素脱离文档流
② 对当前元素做多次改变
③ 把元素带回文档中
在知道如何做后,我们注意到,关键点是如何实现步骤①和步骤③:
- 隐藏元素,在改变结束后再显示元素(
display
) - 使用文档片段(document fragment)在当前
DOM
之外构建一个子树,在改变结束后再将其拷贝回文档,这样只会触发一次重排 - 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。
//index.js 需要添加到页面上的数据
addDataList:[
{
title:"title1",
content:"content1"
},
{
title:"title2",
content:"content2"
},
{
title:"title3",
content:"content3"
}
]
//index.html
<ul id="list">
<li>
<span>title</span>
<span>content</span>
</li>
</ul>
//将addDataList添加到页面上的函数
appendNode(ele,data){
let liEle;
data.forEach(item => {
liEle = document.createElement("li");
liEle.innerHTML = item.content;
ele.appendChild(liEle);
})
}
//append
let ulEle = document.getElementById("list");
appendNode(ulEle,addDataList);
如果使用上述appendNode
方法,addDataList
有多少条数据,便会重排几次。我们可以使用提到的三种方法分别进行优化:
- 隐藏元素,在改变结束后再显示元素(
display
):这个方法是先将ul
节点临时从文档中移除,等到操作完毕后再恢复它,会触发两次重排。
let ulEle = document.getElementById("list");
ulEle.display = "none";
appendNode(ulEle,addDataList);
ulEle.display="block";
- 使用文档片段(document fragment)在当前
DOM
之外构建一个子树,在改变结束后再将其拷贝回文档:这个方法实际是利用文档片段的特性(① 文档片段是一个轻量级的document
对象② 将其append
进Render Tree
中时实际上被添加的是片段子节点,而非其本身),从而实现一次重排的效果。
let fragment= document.createDocumentFragment();
appendNode(fragment,addDataList);
document.getElementById("list").appendChild(fragment);
- 将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素。
let old = document.getElementById("list");
let clone = old.cloneNode(true);
appendNode(clone ,addDataList);
old.parentNode.replaceChild(clone,old);
缓存布局信息
当我们去获取布局信息时,浏览器会强制刷新队列,从而能够拿到最新的信息并返回。强制刷新队列一次,便会引起一次重排。故我们可以尽量减少获取布局信息的次数,将布局信息存储起来,然后操作存储的内容。
//强制刷新 触发多次重排
setInterval(()=>{
myEle.style.left = myEle.offsetLeft + 1 + 'px';
myEle.style.top = myEle.offsetTop + 1 + 'px';
if(myEle.offsetLeft >= 500){
//做一些事情
}
},500)
// 缓存布局信息 相当于读写分离
let curLeft = myEle.offsetLeft;
let curTop = myEle.offsetTop;
setInterval(()=>{
myEle.style.left = curLeft + 1 + 'px';
myEle.style.top = curTop + 1 + 'px';
if(myEle.offsetLeft >= 500){
//做一些事情
}else{
curLeft ++;
curTop ++;
}
},500)
让元素脱离文档流
- 对于一些复杂的动画效果,由于会经常地引起回流重绘,因此我们可以使用定位(绝对定位或固定定位),让其脱离文档流,否则会引起父元素以及后续元素频繁地回流。(在参考文章中找到了一个例子)
- 对于上述例子,当点击按钮后,动画元素变为绝对定位的元素,其动画不会影响其他元素,也便不会导致一次次的重排,当前的帧也稳定在60左右(代表网页处于最优帧率状态,不卡顿)。
- 如果需要实现展开/折叠的动画效果,可通过优化动画的方式。eg:通过牺牲一些平滑来保证
CPU
消耗不会过高。譬如需要实现一个动画,以1个像素单位移动最为平滑,但是重排会过于频繁,会大量消耗CPU
资源,此时我们可以将其改为2/3个像素单位或者相对平滑的像素单位,这样可以减少重排的次数。
浏览器加载、解析、渲染过程中的注意点
- 不同浏览器使用的内核不同,故它们的渲染过程也是不同的,目前是有两个:
- webkit渲染过程
- Gecko渲染过程
-
从上面两个流程图可以看出,浏览器渲染的流程如下(图源渲染树构建、布局及绘制):
1) 当浏览器拿到请求到的
html
时,开始自上而下地解析
2) 浏览器会将html
解析成一个DOM
树,DOM
树的构建是一个深度遍历的过程:当前节点的所有子节点都构建完毕后才会去构建当前节点的下一个兄弟节点。
3) 将css
解析成CSS Rule Tree
(该过程和DOM
构建是并行的关系)
4) 根据DOM
树和CSSOM
来构建Render Tree
。(Render Tree
并不等同于DOM
树,它会舍去一些像head
或display:none
等没必要放在渲染树中的节点)
5)有了Render Tree
后,浏览器已经能知道需要渲染的节点有哪些、它们的CSS
样式有哪些以及各个节点之间的从属关系,下一步则是Layout
,即layout
以及reflow
,即计算出每个节点在屏幕中的位置和每个节点的大小。
6)最后便是绘制,即遍历render Tree
,通过其对应的样式以及计算出来的位置和大小去绘制每个节点于屏幕中。
3. 为了更好地用户体验,渲染引擎会尽可能早地将内容呈现到屏幕上,并不会等到所有的html
都解析完后再去构建和布局render
树,它是解析完一部分就显示一部分,同时,可能还在通过网络下载其余内容。
4. 在解析时遇到<script>
标签时,会停止构建DOM
以及暂停其他资源的下载(目前优化过的浏览器允许并行下载js
文件,但是仍然会阻塞其他资源的下载,且仍然需要等到所有的js
代码下载执行完毕后再继续向下解析)。这样做的原因是:浏览器需要一个稳定的DOM
树结构,而JS
中很有可能有代码直接改变了DOM
树的结构(比如使用document.write
或appendChild
或location.href
进行跳转),浏览器为了防止出现JS
修改DOM
树,需要重新构建DOM
树的情况,所以就会阻塞其他的下载和呈现
5. 再解析遇到需要请求css
资源时,会停止DOM
树的渲染以及阻塞js
的执行,但不会停止DOM
树的构建。原因:由于Render Tree
是依赖于DOM Tree
和CSSOM Tree
的,故css
资源下载会阻塞DOM
树的渲染。由于js
可能会操作已经构建好的DOM
节点和css
样式,故样式会在其后面的js
执行前加载完毕。故css
会阻塞后面的js
执行
6. 图片的加载是在js
、css
等核心文件加载完成之后才会进行加载的(遇到img
会去下载,但是渲染是在最后),因为图片的宽高会改变页面的布局,会导致页面重新布局(reflow
)、重新绘制(repaint
)并重新渲染页面内容。
浏览器加载、解析、渲染的过程
- 先加载
html
文档内容(向服务端请求页面资源),加载完后从上而下进行解析 - 在解析过程中如果遇到
js
外部链接,会停止构建DOM
,并去请求下载资源,等到js
脚本执行完后再继续解析之后的内容(包括构建DOM
、请求其他资源以及执行后续的js
脚本) - 在遇到
<link>
引用外部css
文件时,会新起线程去请求css
资源,并不会停止构建DOM
,但会阻塞DOM
树的渲染。 - 对可渲染的
DOM
节点(即该部分的DOM
树已构建完毕以及样式已经拿到)进行渲染。 - 在渲染过程中,如果遇到
img
标签引用了一张图片,此时向服务器发送请求。但浏览器不会等到图片下载完后才继续渲染后面的内容。 - 若遇到了包含
js
代码的<script>
标签,则立即执行 - 若上一步的
js
代码中包含一句使得某个div
隐藏的代码,则浏览器需要重新渲染这部分内容(渲染不会从根重新渲染,而是只渲染影响的部分) - 如果在解析的最后用户点击了可以切换全局样式(eg:换肤的效果)的按钮时,浏览器先向服务器请求
css
资源,拿到后重新进行渲染页面。
参考文章: