前端学习体系(1)--浏览器篇

423 阅读7分钟

初衷

从学前端到现在也有一年半了,看了很多大佬们写到如何学习前端,也看到了java等成熟的面向对象语言的一些如何学习的文章,所以内心也是希望尝试一下,将自己这段时间学习的内容做个连接,形成自己的知识体系,也希望可以帮到那些还处在迷茫学习中的前端er一个提示吧。

浏览器的组成

  • 界面控件 - 除浏览器主窗口外的其他部分,包括地址栏,前进后退按钮等都属于浏览器的界面控件
  • 浏览器引擎 - 将用户界面的指令传递给渲染引擎
  • 渲染引擎 - 负责解析HTML显示在浏览器主窗口
  • 网络请求 - 用户网络请求,例如HTTP请求等
  • UI后端 - 绘制基本的窗口小部件
  • JS解释器 - 解释和执行JS代码
  • 数据存储 - 持久层,如sessionStorage和localStorage及浏览器内置的数据库

需要注意:谷歌浏览器每个tab页都会开启一个独立的进程,并且拥有一个独立的渲染引擎

浏览器中的进程与线程

  1. GUI渲染线程
  2. JavaScript引擎线程
  3. 定时器触发线程
  4. 事件触发线程
  5. 异步HTTP请求线程

以上浏览器的线程都是我们作为前端需要去了解的部分,接下来所说其实就是第一条的渲染引擎,后面的部分会慢慢道来

浏览器的渲染过程

  1. 解析HTML元素和文本节点,此时document.readyState = 'loading'
  2. 遇到外部link的CSS文件,浏览器会另外创建线程进行解析,所以外部CSS文件不会阻塞页面解析,但是会阻塞页面渲染。
  3. 遇到外部的script脚本,如果没有async,defer属性,则会阻塞文档加载,等待脚本加载完成后继续解析文档;如果脚本有async,defer属性,浏览器会另外创建线程来加载脚本,对于async属性的脚本,加载完成后会立即执行。
  4. 遇到img、video等资源,先正常解析DOM结构,浏览器会异步加载src指向的资源,然后继续解析文档。
  5. DOM解析完之后,document.readyState = 'interactive',此时defer属性的脚本开始顺序执行,执行完毕后触发了DOMContentLoaded事件,标志着程序执行从同步脚本执行阶段转换为事件驱动阶段。
  6. 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的变化。下面是控制台打印的结果

打印了两遍的loading是因为这里外部的js文件也打印了一次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属性,我们来看看打印的结果

因为添加了defer和async属性,浏览器会另外创建线程去加载脚本,所以这里打印出来的结果顺序不能完全相信,我们可以通过给及脚本添加大量计算,来看看load事件是否被触发,因为这里不好展示,大家回去可以对着代码来尝试一下。

:::从浏览器绘制页面的过程来看

  1. 解析HTML元素生成DOM树
  2. 解析CSS生成CSS树,将CSS树和DOM树一起生成Render树
  3. 进行布局Render树,也就是所谓的Layout
  4. 遍历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上可以查看源码,如果有什么不足的地方,也欢迎各位大佬给予建议。