Appearance
三.模拟请求->渲染流程
请求报文格式
- 起始行:[方法][空格][请求 URL][HTTP 版本][换行符]
- 首部: [首部名称][:][空格][首部内容][换行符]
- 首部结束:[换行符]
- 实体
响应报文格式
- 起始行:[HTTP 版本][空格][状态码][空格][原因短语][换行符]
- 首部:[首部名称][:][空格][首部内容][换行符]
- 首部结束: [换行符]
- 实体


1.基于 TCP 发送 HTTP 请求
js
const net = require("net");
class HTTPRequest {
constructor(options) {
this.method = options.method || "GET";
this.host = options.host || "127.0.0.1";
this.port = options.port || 80;
this.path = options.path || "/";
this.headers = options.headers || {};
}
send(body) {
return new Promise((resolve, reject) => {
body = Object.keys(body)
.map((key) => `${key}=${encodeURIComponent(body[key])}`)
.join("&");
if (body) {
this.headers["Content-Length"] = body.length;
}
const socket = net.createConnection(
{
host: this.host,
port: this.port,
},
() => {
const rows = [];
rows.push(`${this.method} ${this.path} HTTP/1.1`);
Object.keys(this.headers).forEach((key) => {
rows.push(`${key}: ${this.headers[key]}`);
});
let request = rows.join("\r\n") + "\r\n\r\n" + body;
socket.write(request);
}
);
socket.on("data", function (data) {
// data 为发送请求后返回的结果
});
});
}
}
async function request() {
const request = new HTTPRequest({
method: "POST",
host: "127.0.0.1",
port: 3000,
path: "/",
headers: {
name: "zhufeng",
age: 11,
},
});
let { responseLine, headers, body } = await request.send({ address: "北京" });
}
request();2.解析响应结果
js
const parser = new HTTPParser();
socket.on("data", function (data) {
// data 为发送请求后返回的结果
parser.parse(data);
if (parser.result) {
resolve(parser.result);
}
});3.解析 HTML
js
let stack = [{ type: "document", children: [] }];
const parser = new htmlparser2.Parser({
onopentag(name, attributes) {
let parent = stack[stack.length - 1];
let element = {
tagName: name,
type: "element",
children: [],
attributes,
parent,
};
parent.children.push(element);
element.parent = parent;
stack.push(element);
},
ontext(text) {
let parent = stack[stack.length - 1];
let textNode = {
type: "text",
text,
};
parent.children.push(textNode);
},
onclosetag(tagname) {
stack.pop();
},
});
parser.end(body);4.解析CSS
js
const cssRules = [];
const css = require("css");
function parserCss(text) {
const ast = css.parse(text);
cssRules.push(...ast.stylesheet.rules);
}js
const parser = new htmlparser2.Parser({
onclosetag(tagname) {
let parent = stack[stack.length - 1];
if (tagname == "style") {
parserCss(parent.children[0].text);
}
stack.pop();
},
});5.计算样式
js
function computedCss(element) {
let attrs = element.attributes; // 获取元素属性
element.computedStyle = {}; // 计算样式
Object.entries(attrs).forEach(([key, value]) => {
cssRules.forEach((rule) => {
let selector = rule.selectors[0];
if (
(selector == "#" + value && key == "id") ||
(selector == "." + value && key == "class")
) {
rule.declarations.forEach(({ property, value }) => {
element.computedStyle[property] = value;
});
}
});
});
}6.布局绘制
js
function layout(element) {
// 计算位置 -> 绘制
if (Object.keys(element.computedStyle).length != 0) {
let { background, width, height, top, left } = element.computedStyle;
let code = `
let canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight ;
let context = canvas.getContext("2d")
context.fillStyle = "${background}";
context.fillRect(${top}, ${left}, ${parseInt(width)}, ${parseInt(
height
)});
`;
fs.writeFileSync("./code.js", code);
}
}7.总结:DOM 如何生成的
- 当服务端返回的类型是
text/html时,浏览器会将收到的数据通过HTMLParser进行解析 (边下载边解析) - 在解析前会执行预解析操作,会预先加载
JS、CSS等文件 - 字节流 -> 分词器 -> Tokens -> 根据 token 生成节点 -> 插入到 DOM 树中
- 遇到
js:在解析过程中遇到script标签,HTMLParser会停止解析,(下载)执行对应的脚本。 - 在
js执行前,需要等待当前脚本之上的所有CSS加载解析完毕(js是依赖css的加载)

CSS样式文件尽量放在页面头部,CSS加载不会阻塞 DOM tree 解析,浏览器会用解析出的DOM TREE和CSSOM进行渲染,不会出现闪烁问题。如果CSS放在底部,浏览是边解析边渲染,渲染出的结果不包含样式,后续会发生重绘操作。JS文件放在 HTML 底部,防止JS的加载、解析、执行堵塞页面后续的正常渲染
通过
PerformanceAPI监控渲染流程