初衷
从学前端到现在也有一年半了,看了很多大佬们写到如何学习前端,也看到了java等成熟的面向对象语言的一些如何学习的文章,所以内心也是希望尝试一下,将自己这段时间学习的内容做个连接,形成自己的知识体系,也希望可以帮到那些还处在迷茫学习中的前端er一个提示吧。
浏览器的组成
界面控件- 除浏览器主窗口外的其他部分,包括地址栏,前进后退按钮等都属于浏览器的界面控件浏览器引擎- 将用户界面的指令传递给渲染引擎渲染引擎- 负责解析HTML显示在浏览器主窗口网络请求- 用户网络请求,例如HTTP请求等UI后端- 绘制基本的窗口小部件JS解释器- 解释和执行JS代码数据存储- 持久层,如sessionStorage和localStorage及浏览器内置的数据库
需要注意:谷歌浏览器每个tab页都会开启一个独立的进程,并且拥有一个独立的渲染引擎
浏览器中的进程与线程
GUI渲染线程JavaScript引擎线程定时器触发线程事件触发线程异步HTTP请求线程
以上浏览器的线程都是我们作为前端需要去了解的部分,接下来所说其实就是第一条的渲染引擎,后面的部分会慢慢道来
浏览器的渲染过程
- 解析HTML元素和文本节点,此时
document.readyState = 'loading'。 - 遇到外部link的CSS文件,浏览器会另外创建线程进行解析,所以外部CSS文件不会阻塞页面解析,但是会阻塞页面渲染。
- 遇到外部的script脚本,如果没有async,defer属性,则会阻塞文档加载,等待脚本加载完成后继续解析文档;如果脚本有async,defer属性,浏览器会另外创建线程来加载脚本,对于async属性的脚本,加载完成后会立即执行。
- 遇到img、video等资源,先正常解析DOM结构,浏览器会异步加载src指向的资源,然后继续解析文档。
- DOM解析完之后,
document.readyState = 'interactive',此时defer属性的脚本开始顺序执行,执行完毕后触发了DOMContentLoaded事件,标志着程序执行从同步脚本执行阶段转换为事件驱动阶段。 - async属性脚本加载完就会执行,不论是在HTML解析阶段还是在DOMContentLoaded之后执行,async属性脚本会阻塞load事件,当所有的async脚本执行完毕,以及img等资源加载完毕,
document.readyState = 'complete',window对象会触发load事件。
我们可以通过简单的代码来看看浏览器中这个document.readyState究竟是什么
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<p>Hello World</p>
<img src="./cnode_icon_64.png" alt="">
<script src="./index.js"></script>
<script>
console.log(document.readyState)
window.addEventListener('load',function(){
console.log(document.readyState)
})
window.addEventListener('DOMContentLoaded',function(){
console.log(document.readyState)
})
</script>
</body>
</html>
代码其实很简单,我们只是引入了一个外部的css文件和js文件,通过监听一些事件来观察document.readyState的变化。下面是控制台打印的结果
document.readyState,可以看到确实如上面所说的顺序所打印,而这个属性就是代表了文档的一个加载的过程,下面我们再来看看如果在脚本中加入了defer和async又会发生什么
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="./index.css">
<script>
console.log(document.readyState)
window.addEventListener('load',function(){
console.log('触发了load事件')
console.log(document.readyState)
})
window.addEventListener('DOMContentLoaded',function(){
console.log('触发了DOMContentLoaded事件')
console.log(document.readyState)
})
</script>
</head>
<body>
<p>Hello World</p>
<img src="./cnode_icon_64.png" alt="">
<script defer src="./index.js"></script>
<script async src="./index2.js"></script>
</body>
</html>
这里只是改变了一下脚本的顺序,并且添加了一个外部脚本,并且给脚本添加了defer和async属性,我们来看看打印的结果
:::从浏览器绘制页面的过程来看
- 解析HTML元素生成DOM树
- 解析CSS生成CSS树,将CSS树和DOM树一起生成Render树
- 进行布局Render树,也就是所谓的
Layout - 遍历Render树,需要注意的是Render树的计算只需要遍历一次就能够完成,但是table及其内部的元素除外,这就是我们布局时尽量避免使用table的原因。浏览器将其的每一个节点渲染到浏览器上,即所谓的
Paint
回流(reflow)和重绘(repaint)
- 回流(reflow):就是页面元素布局发生变化,浏览器需要重新回到渲染之前的状态重新计算
- 重绘(repaint):当元素节点的自身不影响布局的属性,如背景色、字体颜色、边框颜色等发生改变时会触发重绘
触发回流一定会触发重绘,但是重绘却不一定会触发回流
这里有一个大佬的文档,非常详细的描述了导致回流和重绘的操作 触发浏览器回流的属性方法一览表
性能影响
- 回流的代价比重绘更高
- 现代浏览器会对频繁的回流和重绘操作进行优化,即会将所有的回流或重绘的操作放入一个队列,然后进行一起操作,避免多次进行回流和重绘
知道了以上这些知识,那么我们大概就可以明白,为什么说要把CSS的脚本放在head头,而js的脚本尽量放在文档的结尾了,因为这样不会阻塞文档的解析,文档解析变快了,那么用户看到的页面速度肯定也就快了
浏览器缓存
这部分的知识这里知识提及一下,后面会在HTTP重点介绍一下
- 强缓存,就是max-age,no-cache等字段
- 协商缓存,etag,lastmodified等
- 本地缓存localStorage和sessionStorage
浏览器兼容性问题
因为作者本人对浏览器兼容了解的并不多,平时开发最多兼容ie11,所以也就不误人子弟了,有兴趣的同学可以参考这篇文章浏览器兼容性问题解决方案 · 总结
浏览器的内置对象
- 判断浏览器类型通过使用浏览器内置对象navigator的useAgent属性
- 浏览器桌面通知Notification
参考链接
写在最后
非常感谢以上链接的作者们,笔者也会把这些做成能随时翻阅查找的一个网站上,当然在Github上可以查看源码,如果有什么不足的地方,也欢迎各位大佬给予建议。