浏览器渲染原理 1
Posted lin-fighting
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浏览器渲染原理 1相关的知识,希望对你有一定的参考价值。
进程与线程
- 进程是操作系统资源分配的基本单位,进程种包含线程。
- 线程是由进程所管理的,为了提升浏览器的稳定性和安全性,浏览器采用了多进程模型。
- 浏览器进程: 负责界面显示,用户交互,子进程管理,提供存储等
- 渲染进程:每个页面都有单独的渲染进程,核心用于渲染页面
- 网络进程:主要处理网络资源加载(html, CSS ,JS等)
- GPU进程:3d绘制,提高性能。
- 插件进程:chrome种安装的一些插件。
从输入url到浏览器显示,发生了什么?
从浏览器进程看:
- 输入url或者关键字。如果是关键字,根据默认的引擎生成地址。(浏览器进程里面做的事情)
- 浏览器进程,准备一个渲染进程,用于渲染页面
- 网络进程加载资源,最终将加载的资源交给渲染进程处理
- 渲染完毕显示。
网络七层模型:物理层 数据链路层 网络层(ip) 传输层(tcp udp) (会话层,表示层,应用层)
- 输入url后,先查找缓存,检测缓存是否过期,没过期直接返回缓存内容。
- 看域名是否被解析过,本地缓存,dns解析,将域名解析成ip地址(DNS基于UDP) ip+端口号 host
- 请求如果是htpps (ssl协商)
- ip地址来进行寻址,排队等待,最多能发送6个http请求
- tcp创建连接,用于传输(三次握手)
- 利用tcp传输数据(拆分成数据包, 有序)可靠,有序,服务器会按照顺序来接受
- http请求(请求行 请求头 请求体)
- 默认不会断开, Keep-alive,为了下次传输数据的时候,可以复用上次创建的连接。
- 服务器收到请求后,返回数据(响应行 响应头 响应体)
- 服务器返回301, 30进行重定向操作(重新重投开始走下来)
- 服务器返回304,查询浏览器缓存并且返回。
浏览器接受资源后:
- 1 浏览器无法直接使用HTML,需要将HMTL转成DOM树(document)
- 2 浏览器无法解析纯文本的css样式,需要对css进行解析城styleSheets(css样式表)。CSSDOM(document.styleSheets)
- 3 根据dom树和css对象计算出DOM树种每个节点的具体样式(Attachment)
- 4 创建渲染(render tree),将DOM树种可见节点,添加到render tree中,并且计算每个节点渲染到页面的坐标位置(即layout阶段)
- 5 通过render tree,进行分层(根据定位属性,透明属性,transform属性,clip属性)生成图层树
- 6 将不同图层进行绘制(painting),转交给合成线程处理,最终生成页面,并显示到浏览器上(painting,display)
- 从浏览器的performance可以看出,浏览器一开始发送请求,到获取数据后,第一件事就是解析HTML城DOM树,期间遇到css不会去parse,接着才是parse解析css,然后将解析后的css和DOM树进行布局,生成render tree,再更新图层树,进行layout,最后直接painting(绘制),然后触发load事件。
HTML解析
css为什么放顶部不放在底部。
-
浏览器在解析HTML的过程中,从上往下,遇到一个标签渲染一个。当顶部遇到link的时候,css不会阻塞Html的解析
-
但是如果顶部存放link标签,当HTML解析成DOM树的时候,需要与css样式表结合生成render tree。
此时浏览器的操作:解析hmtl=》触发DomcontentLoad =>解析样式表 =》重新计算样式 =》更新图层=》绘制
放在顶部的link,Html最后呈现的时候需要依赖他。
-
而如果link标签放在底部,那么html解析城dom树并且生成render tree,paintine到页面上不需要依赖link。
浏览器的操作是:解析HTMl => 重新计算样式 =》布局=》绘制=》复合图层=》渲染
等Link的css请求回来后,浏览器需要继续 解析样式表 =》 解析HTMl => 重新计算样式 =》触发相关事件
所以css放在底部,可能会导致重绘效果。
js会阻塞dom的解析,需要暂停dom解析去执行js,js可能会操作样式,所以还需要等待样式加载完成。
<body>
<div class="div">123123</div>
<script>
let i = 0
while(i < 1000000000)
i++
console.log(i);
</script>
<div class="div">123123</div>
</body>
上面这段代码,浏览器的操作时:
解析hmtl=>解析样式表(Js执行需要等待css加载完毕)=>js执行=>重新计算样式=》布局=》绘制=》遇到下面的div=》又开始解析Html=> 重新计算样式=>布局=》绘制
js会阻塞html解析,也会阻塞渲染,并且,js要等待上面的css加载完毕,保证js里面可以操作样式。
<div class="div">123123</div>
<script src="./1.js">
</script>
<div>123123</div>
将js抽离到文件去,通过script加载。
资源请求回来之后,默认会进行link script的预加载,所以css和js脚本是并行加载的。虽然执行步骤是跟上面一样的,但是css和js文件时并行请求的。
总结
-
解析前遇到link和scirpt,会进行并行加载css和js文件。
-
html会生成字节流->分词器->tokens->根据token生成节点->插入到DOM树种。
-
css放在顶部,dom渲染会依赖css。放在底部,dom初次渲染不依赖,但是css加载完毕会引起dom的重绘。css不阻塞html解析
-
内嵌在html的js会阻塞html解析,并且会等待当前脚本之上的样式表解析完毕后才会执行js(保证js可以操作css),然后才会继续解析html。js依赖css的加载。
-
js一般放在底部,为的是操作完整的dom和不影响html的解析。
渲染流程
往大了看,浏览器就是帮助我们发送请求,然后将响应资源加载出来的软件。我们可以自己模拟一个客户端。模拟获取数据并且解析html为dom树,和css解析成styleSheet的实现。
首先通过http模块创建一个服务器
const Http = require("http");
const fs = require("fs");
const path = require("path");
const server = new Http.createServer();
server.on("request", (req, res) =>
console.log(req.headers);
fs.createReadStream("./1.html").pipe(res);
);
server.listen(3000);
//这是要相应的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.div
color: red;
</style>
</head>
<body>
<div class="div">123123</div>
<script>
let i = 0
while (i < 100000)
i++
console.log(i);
</script>
<div>123123</div>
</body>
</html>
接着模拟客户端发送请求:我们知道http是基于tcp连接的。我们只要自己实现httpi请求头,并且创建tcp连接,将数据发送出去即可。
const net = require("net");
//创建一个HttpRequest类,用来发送请求
class HttpRequest
constructor(options = )
this.host = options.host;
this.method = options.method || "GET";
this.port = options.port;
this.path = options.path;
this.headers = options.headers;
send()
return new Promise((resolve, reject) =>
// 构建http请求
const rows = [];
rows.push(`$this.method $this.path HTTP/1.1`); //模拟浏览器的请求行
// 处理请求体的heades
Object.keys(this.headers).forEach((item) =>
rows.push(`$item: $this.headers[item]`);
);
// 处理请求头
// GET / HTTP/1.1
// xxx:xx
//
//
const data = rows.join("\\r\\n") + "\\r\\n\\r\\n"; //加上换行符
console.log("data", data);
// 通过tcp传输
const socket = net.createConnection(
host: this.host,
port: this.port,
,
() =>
console.log("创建连接成功");
// 创建连接成功之后,传输http数据
socket.write(data);
);
let responseData = [];
// 也是一个可读流,tcp传输是分段的。监听服务器数据返回,返回的不只有文件内容,还有响应头
socket.on("data", function (chunk)
responseData.push(chunk);
);
socket.on("end", () =>
responseData = Buffer.concat(responseData);
let [headers, body] = responseData.toString().split("220");
resolve(
headers,
body,
);
);
);
接着我们发送请求
async function request()
const request = new HttpRequest(
host: "127.0.0.1",
method: "GET",
port: 3000,
path: "/",
headers:
name: "lin",
age: 12,
,
);
// 发送请求,响应行,响应头,响应体
let headers, body = await request.send();
// 处理body,对html解析生成dom树,对css文本解析,生成styleSheets。
关键就是自己实现http请求头,并且通过net创建了tcp连接,将数据发送出去。然后通过可读流,获取返回数据,返回的数据不仅有文件内容,还有响应头,如下:
HTTP/1.1 200 OK
Date: Tue, 22 Feb 2022 14:50:37 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
220
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.div
color: red;
</style>
</head>
<body>
<div class="div">123123</div>
<script>
let i = 0
while (i < 100000)
i++
console.log(i);
</script>
<div>123123</div>
</body>
</html>
0
这就是具体的响应内容。我们通过分割获取到html的部分。
接着模拟浏览器解析html为dom树。使用htmlparser2,
const HtmlParser = require("htmlparser2");
// 发送请求,响应行,响应头,响应体
let headers, body = await request.send();
// html解析城dom tree,就是做词法分析最后生成dom tree
// 解析后需要生城tree,典型的栈型结构
let stack = [ type: "document", children: [] ];
// 浏览器根据响应内容来解析文件
const parser = new HtmlParser.Parser(
// document html header body
// 遇到一个tag,获取他的tagName和属性
onopentag(name, attributes)
let parent = stack[stack.length - 1];
//
let element =
tagName: name,
attributes,
children: [],
parent,
;
parent.children.push(element);
stack.push(element); // 当前的tag可能也有儿子
,
// 获取tag的内容
ontext(text)
let parent = stack[stack.length - 1]; // 因为上面的tag已经作为一个元素Push进stack了,这里直接获取,将text放入到children就行
let textNode =
type: "text",
text,
;
parent.children.push(textNode);
,
// 关闭tag,遇到闭合的,就将其从栈中取出,到最后的html退出,stack此时剩一个document,他是一颗树,通过children跟parent将整个Html变成dom树。
onclosetag(name)
//只考虑内联css
if(name === 'style')
let parent = stack[stack.length - 1];
const cssText = parent.children[0].text
parserCss(cssText)
console.log('cssText',cssText);
// 遇到闭合,
stack.pop();
if (stack.length === 1)
//console.dir(stack, depth: null );
,
);
parser.write(body);
主要是通过一个栈结构,使用HtmlParser.parser,在遇到html标签的时候,将其压入栈中,然后匹配到内容的时候,将其存入对应元素的children,再到闭合标签的时候,将其推出栈。
比如document和header标签,遇到document标签,压入栈中,而下一个标签就是header,他会将header标签作为document的chidlren,并且将header标签压入栈中。然后遇到header标签的闭合标签,就将header标签推出栈,此时栈里只有一个document标签。他通过children连接到了header标签,以此类推,构建城整颗dom树。
等所有dom标签被解析后,最后栈里剩的就是一个document元素,他没有闭合标签。打印的结果应该是:
[
<ref *4>
type: 'document',
children: [
type: 'text', text: '\\r\\n' ,
type: 'text', text: '\\r\\n' ,
<ref *2>
tagName: 'html',
attributes: lang: 'en' ,
children: [
type: 'text', text: '\\r\\n\\r\\n' ,
<ref *1>
tagName: 'head',
attributes: ,
children: [
type: 'text', text: '\\r\\n ' ,
tagName: 'meta',
attributes: charset: 'UTF-8' ,
children: [],
parent: [Circular *1]
,
type: 'text', text: '\\r\\n ' ,
tagName: 'meta',
attributes: 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' ,
children: [],
parent: [Circular *1]
,
type: 'text', text: '\\r\\n ' ,
tagName: 'meta',
attributes:
name: 'viewport',
content: 'width=device-width, initial-scale=1.0'
,
children: [],
parent: [Circular *1]
,
type: 'text', text: '\\r\\n ' ,
tagName: 'title',
attributes: ,
children: [ type: 'text', text: 'Document' ],
parent: [Circular *1]
,
type: 'text', text: '\\r\\n ' ,
tagName: 'style',
attributes: ,
children: [
type: 'text',
text: '\\r\\n' +
' .div \\r\\n' +
' color: red;\\r\\n' +
' \\r\\n' +
' '
],
parent: [Circular *1]
,
type: 'text', text: '\\r\\n\\r\\n'
],
parent: [Circular *2]
,
type: 'text', text: '\\r\\n\\r\\n' ,
<ref *3>
tagName: 'body',
attributes: ,
children: [
type: 'text', text: '\\r\\n ' ,
tagName: 'div',
attributes: class: 'div' ,
children: [ type: 'text', text: '123123' ],
parent: [Circular *3]
,
type: 'text', text: '\\r\\n ' ,
tagName: 'script',
attributes: ,
children: [
type: 'text',
text: 浏览器渲染原理