前言
作为一名前端er,浏览器和我们在工作中几乎是抬头不见低头见,除了高效的实现一些web功能以外,我们还需要深入浏览器,去了解一些原理知识,那我们就在这里一起讨论学习一下浏览器的渲染知识,最后实现一个浏览器对html的解析器
Critical Rendering Path
Critical Rendering Path,中文翻译过来,叫做关键渲染路径。指的是浏览器从请求HTML,CSS,JavaScript文件开始,到将它们最终以像素输出到屏幕上这一过程。
构建dom
构建css
1 | 假设有如下CSS代码: |
构建渲染树
这也是关键的一步,浏览器使用DOM和CSSOM构建出Render Tree。此时不像构建DOM一样把所有节点构建出来,浏览器只构建需要在屏幕上显示的部分,因此像
,这些标签就无需构建了。同时,对于display: none的元素,也无需构建。
然后通过我们耳熟能详的浏览器layout和paint渲染这颗树到浏览器上
本篇文章主要介绍实现解析html文件,分析各种嵌套标签,生成一个简单的抽象语法树
思路分析
实现htmlparser主要分为词法分析和语法分析两步
词法分析需要把每一种类型的 token 识别出来
语法分析 语法分析就是对上面分出的 token 进行组装,生成 ast。
说的简单就是一句话,词法分析是将各种标签提取出来,语法分析是分析其中的嵌套关系,生成ast
那么,ast到底长什么样呢?
其实它就是一个对象,包含了各个标签的特性和他们的嵌套关系
所以,我们的目的就是为了将html文件,通过js变成这样的一个ast对象
首先,我们要把 startTag、endTag、comment、docType 还有 attribute 的正则表达式写出来:
正则
结束标签就是 </ 开头,然后 a-zA-Z0-9 和 - 出现多次,之后是 >
const endTagReg = /^<\/([a-zA-Z0-9\-]+)>/;
注释标签是 中间夹着非 –> 字符出现任意次
const commentReg = /^<!\-\-[^(-->)]*\-\->/;
doctype 标签是 字符出现多次,加 >
const docTypeReg = /^<!doctype [^>]+>/;
attribute 是多个空格开始,加 a-zA-Z0-9 或 - 出现多次,接一个 =,之后是非 > 字符出多次
const attributeReg = /^(?:[ ]+([a-zA-Z0-9\-]+=[^>]+))/;
开始标签是 < 开头,接 a-zA-Z0-9 和 - 出现多次,然后是属性的正则,最后是 > 结尾
const startTagReg = /^<([a-zA-Z0-9\-]+)(?:([ ]+[a-zA-Z0-9\-]+=[^> ]+))*>/;
之后上打了注释的代码
//开始标签
const startTagReg = /^<([a-zA-Z0-9\-]+)(?:([ ]+[a-zA-Z0-9\-]+=[^> ]+))*>/;
//标签内属性
const attributeReg = /^(?:[ ]+([a-zA-Z0-9\-]+=[^> ]+))/;
//结束标签
const endTagReg = /^<\/([a-zA-Z0-9\-]+)>/;
//注释标签
const commentReg = /^<!\-\-[^(-->)]*\-\->/;
//文档类型标签
const docTypeReg = /^<!doctype [^>]+>/;
function parse(html,options) {
// body...
function advance(len){
html = html.slice(len)
}
while(html){
if(html.startsWith('<')){
//处理注释标签字符串
let commentMatch = html.match(commentReg);
if (commentMatch) {
options.onComment({
type: 'comment',
value: commentMatch[0]
})
advance(commentMatch[0].length);
continue;
}
//处理文档类型标签字符串
let doctypeMatch = html.match(docTypeReg);
if (docTypeMatch) {
options.onDoctype({
type: 'docType',
value: docTypeMatch[0]
});
advance(docTypeMatch[0].length);
continue;
}
//处理文档标签开始字符串
let startTagMatch = html.match(startTagReg);
if(startTagMatch) {
options.onStartTag({
type: 'tagStart',
value: startTagMatch[1]
});
advance(startTagMatch[1].length + 1);
let attributeMath;
//在标签内遍历属性
while(attributeMath = html.match(attributeReg)) {
options.onAttribute({
type: 'attribute',
value: attributeMath[1]
});
advance(attributeMath[0].length);
}
advance(1);
continue;
}
}else{
//处理标签内文本
let textEndIndex = html.indexOf('<');
options.onText({
type: 'text',
value: html.slice(0, textEndIndex)
});
textEndIndex = textEndIndex === -1 ? html.length: textEndIndex;
advance(textEndIndex);
}
}
}
function parserHtml(str){
let ast = {
children :[]
};
let currentNode = ast;
let parentNode = null;
parse(str,{
onComment(node) {
},
onStartTag(token) {
const tag = {
tagName: token.value,
attributes: [],
text: '',
children: []
};
curParent.children.push(tag);
prevParent = curParent;
curParent = tag;
},
onAttribute(token) {
const [ name, value ] = token.value.split('=');
curParent.attributes.push({
name,
value: value.replace(/^['"]/, '').replace(/['"]$/, '')
});
},
onEndTag(token) {
curParent = prevParent;
},
onDoctype(token) {
},
onText(token) {
curParent.text = token.value;
}
})
return ast.children[0];
}
总结
了解htmlparser是很重要的,大名鼎鼎的vue的模版编译,react的jsx语法,都是基于htmlparser做的,同时这个知识点也是淘系前端的多年不变的面试题之一,拿到ast后就可以构建一个渲染方法,关于渲染的部分我们之后讨论吧~
参考博客 掘金 https://juejin.cn/post/6970706892632883213