浏览器环境概述
1. 代码嵌入网页的方法
网页中嵌入 JavaScript 代码,主要有四种方法。
- <script>元素直接嵌入代码。
<script id="mydata" type="x-custom-data">
console.log('Hello World');
</script>
<!-- 可以使用<script>节点的text属性读出它的内容,<script>存在于DOM中 -->
document.getElementById('mydata').text
- <script>标签加载外部脚本
<!-- script标签允许设置一个integrity属性,写入该外部脚本的 Hash 签名,用来验证脚本的一致性。-->
<script src="/assets/application.js"
integrity="sha256-TvVUHzSfftWg1rcfL6TIJ0XKEGrgLyEq6lEpcmrG9qs=">
</script>
- 事件属性,(比如onclick和onmouseover)
<button id="myBtn" onclick="console.log(this.id)">点击</button>
- URL 协议
<a href="javascript: void new Date().toLocaleTimeString();">点击</a>
<a href="javascript: new Date().toLocaleTimeString();void 0;">点击</a>
<!-- 在脚本前加上void,或者在脚本最后加上void 0,可以防止书签替换掉当前文档。 -->
2. script 元素
2.1 工作原理
正常的网页加载流程:
- 浏览器一边下载 HTML 网页,一边开始解析。也就是说,不等到下载完,就开始解析。
- 解析过程中,浏览器发现<script>元素,就暂停解析,把网页渲染的控制权转交给 JavaScript 引擎。
- 如果<script>元素引用了外部脚本,就下载该脚本再执行,否则就直接执行代码 。
- JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页。
加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。
如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为阻塞效应。同样解析和执行 CSS,也会产生阻塞。
为了避免这种情况,较好的做法是将<script>标签都放在页面底部,而不是头部,如下:
<body>
<!-- 其他代码 -->
<script>
console.log(document.body.innerHTML);
</script>
</body>
此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。
2.2 defer 属性
为了解决脚本文件下载阻塞网页渲染的问题,一个方法是对<script>元素加入defer属性。它的作用是延迟脚本的执行,等到 DOM 加载生成后,再执行脚本。且执行顺序就是它们在页面上出现的顺序。
<script src="a.js" defer></script>
<script src="b.js" defer></script>
对于内置而不是加载外部脚本的script标签,以及动态生成的script标签,defer属性不起作用。另外,使用defer加载的外部脚本不应该使用document.write方法。
2.3 async 属性
解决“阻塞效应”的另一个方法是对<script>元素加入async属性。会在解析 HTML 网页同时并行下载<script>标签中的外部脚本,脚本下载完成,浏览器暂停解析 HTML 网页,开始执行下载的脚本。脚本执行完毕,浏览器恢复解析 HTML 网页。
<script src="a.js" async></script>
<script src="b.js" async></script>
async属性可以保证脚本下载的同时,浏览器继续渲染。需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序。哪个脚本先下载结束,就先执行那个脚本。另外,使用async属性的脚本文件里面的代码,不应该使用document.write方法。
defer属性和async属性到底应该使用哪一个?
一般来说,如果脚本之间没有依赖关系,就使用async属性,如果脚本之间有依赖关系,就使用defer属性。如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定。
3. 浏览器的组成
浏览器的核心是两部分:渲染引擎和 JavaScript 解释器(又称 JavaScript 引擎)。
3.1 渲染引擎
渲染引擎的主要作用是,将网页代码渲染为用户视觉可以感知的平面文档
不同的浏览器有不同的渲染引擎。
- Firefox:Gecko 引擎
- Safari:WebKit 引擎
- Chrome:Blink 引擎
- IE: Trident 引擎
- Edge: EdgeHTML 引擎
渲染引擎处理网页,通常分成四个阶段。
- 解析代码:HTML 代码解析为 DOM,CSS 代码解析为 CSSOM(CSS Object Model)。
- 对象合成:将 DOM 和 CSSOM 合成一棵渲染树(render tree)。
- 布局:计算出渲染树的布局(layout)。
- 绘制:将渲染树绘制到屏幕。
以上四步并非严格按顺序执行,往往第一步还没完成,第二步和第三步就已经开始了。所以,会看到这种情况:网页的 HTML 代码还没下载完,但浏览器已经显示出内容了。
3.2 重流和重绘
渲染树转换为网页布局,称为“布局流”(flow);布局显示到页面的这个过程,称为“绘制”(paint)。它们都具有阻塞效应,并且会耗费很多时间和计算资源。
页面生成以后,脚本操作和样式表操作,都会触发“重流”(reflow)和“重绘”(repaint)。用户的互动也会触发重流和重绘,比如设置了鼠标悬停(a:hover)效果、页面滚动、在输入框中输入文本、改变窗口大小等等。
重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流。比如改变元素颜色,只会导致重绘,而不会导致重流;改变元素的布局,则会导致重绘和重流。
大多数情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局重新生成网页。
作为开发者,应该尽量设法降低重绘的次数和成本。比如,尽量不要变动高层的 DOM 元素,而以底层 DOM 元素的变动代替;再比如,重绘table布局和flex布局,开销都会比较大。
下面是一些优化技巧。
- 读取 DOM 或者写入 DOM,尽量写在一起,不要混杂。不要读取一个 DOM 节点,然后立刻写入,接着再读取一个 DOM 节点。
- 缓存 DOM 信息。
- 不要一项一项地改变样式,而是使用 CSS class 一次性改变样式。
- 使用documentFragment操作 DOM
- 动画使用absolute定位或fixed定位,这样可以减少对其他元素的影响。
- 只在必要时才显示隐藏元素。
- 使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流。
- 使用虚拟 DOM(virtual DOM)库。