探索浏览器渲染器原理

前言

作为一名前端er,浏览器和我们在工作中几乎是抬头不见低头见,除了高效的实现一些web功能以外,我们还需要深入浏览器,去了解一些原理知识,那我们就在这里一起讨论学习一下浏览器的渲染知识,最后实现一个浏览器对html的解析器

Critical Rendering Path

Critical Rendering Path,中文翻译过来,叫做关键渲染路径。指的是浏览器从请求HTML,CSS,JavaScript文件开始,到将它们最终以像素输出到屏幕上这一过程。

构建dom

image

构建css

1
2
3
4
5
6
7
假设有如下CSS代码:

body { font-size: 16px }
p { font-weight: bold }
span { color: red }
p span { display: none }
img { float: right }

image

构建渲染树

这也是关键的一步,浏览器使用DOM和CSSOM构建出Render Tree。此时不像构建DOM一样把所有节点构建出来,浏览器只构建需要在屏幕上显示的部分,因此像,这些标签就无需构建了。同时,对于display: none的元素,也无需构建。

image
然后通过我们耳熟能详的浏览器layout和paint渲染这颗树到浏览器上
本篇文章主要介绍实现解析html文件,分析各种嵌套标签,生成一个简单的抽象语法树

思路分析

实现htmlparser主要分为词法分析和语法分析两步
词法分析需要把每一种类型的 token 识别出来

语法分析 语法分析就是对上面分出的 token 进行组装,生成 ast。

说的简单就是一句话,词法分析是将各种标签提取出来,语法分析是分析其中的嵌套关系,生成ast

那么,ast到底长什么样呢?

image

其实它就是一个对象,包含了各个标签的特性和他们的嵌套关系

所以,我们的目的就是为了将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

上一篇

2021年终总结