从零开始实现一个玩具版浏览器渲染引擎(二)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 从零开始实现一个玩具版浏览器渲染引擎(二)

CSS 解析器

CSS 样式表是一系列的 CSS 规则集合,而 CSS 解析器的作用就是将 CSS 文本解析为 CSS 规则集合。

div, p {
    font-size: 88px;
    color: #000;
}

例如上面的 CSS 文本,经过解析器解析后,会生成下面的 CSS 规则集合:

[
    {
        "selectors": [
            {
                "id": "",
                "class": "",
                "tagName": "div"
            },
            {
                "id": "",
                "class": "",
                "tagName": "p"
            }
        ],
        "declarations": [
            {
                "name": "font-size",
                "value": "88px"
            },
            {
                "name": "color",
                "value": "#000"
            }
        ]
    }
]

每个规则都有一个 selectordeclarations 属性,其中 selectors 表示 CSS 选择器,declarations 表示 CSS 的属性描述集合。

export interface Rule {
    selectors: Selector[]
    declarations: Declaration[]
}
export interface Selector {
    tagName: string
    id: string
    class: string
}
export interface Declaration {
    name: string
    value: string | number
}

每一条 CSS 规则都可以包含多个选择器和多个 CSS 属性。

解析 CSS 规则 parseRule()

private parseRule() {
    const rule: Rule = {
        selectors: [],
        declarations: [],
    }
    rule.selectors = this.parseSelectors()
    rule.declarations = this.parseDeclarations()
    return rule
}

parseRule() 里,它分别调用了 parseSelectors() 去解析 CSS 选择器,然后再对剩余的 CSS 文本执行 parseDeclarations() 去解析 CSS 属性。

解析选择器 parseSelector()

private parseSelector() {
    const selector: Selector = {
        id: '',
        class: '',
        tagName: '',
    }
    switch (this.rawText[this.index]) {
        case '.':
            this.index++
            selector.class = this.parseIdentifier()
            break
        case '#':
            this.index++
            selector.id = this.parseIdentifier()
            break
        case '*':
            this.index++
            selector.tagName = '*'
            break
        default:
            selector.tagName = this.parseIdentifier()
    }
    return selector
}
private parseIdentifier() {
    let result = ''
    while (this.index < this.len && this.identifierRE.test(this.rawText[this.index])) {
        result += this.rawText[this.index++]
    }
    this.sliceText()
    return result
}

选择器我们只支持标签名称、前缀为 # 的 ID 、前缀为任意数量的类名 . 或上述的某种组合。如果标签名称为 *,则表示它是一个通用选择器,可以匹配任何标签。

标准的 CSS 解析器在遇到无法识别的部分时,会将它丢掉,然后继续解析其余部分。主要是为了兼容旧浏览器和防止发生错误导致程序中断。我们的 CSS 解析器为了实现简单,没有做这方面的做错误处理。

解析 CSS 属性 parseDeclaration()

private parseDeclaration() {
    const declaration: Declaration = { name: '', value: '' }
    this.removeSpaces()
    declaration.name = this.parseIdentifier()
    this.removeSpaces()
    while (this.index < this.len && this.rawText[this.index] !== ':') {
        this.index++
    }
    this.index++ // clear :
    this.removeSpaces()
    declaration.value = this.parseValue()
    this.removeSpaces()
    return declaration
}

parseDeclaration() 会将 color: red; 解析为一个对象 { name: "color", value: "red" }

小结

CSS 解析器相对来说简单多了,因为很多知识点在 HTML 解析器中已经讲到。整个 CSS 解析器的代码大概 100 多行,如果你阅读过 HTML 解析器的源码,相信看 CSS 解析器的源码会更轻松。

构建样式树

本阶段的目标是写一个样式构建器,输入 DOM 树和 CSS 规则集合,生成一棵样式树 Style tree。

样式树的每一个节点都包含了 CSS 属性值以及它对应的 DOM 节点引用:

interface AnyObject {
    [key: string]: any
}
export interface StyleNode {
    node: Node // DOM 节点
    values: AnyObject // style 属性值
    children: StyleNode[] // style 子节点
}

先来看一个简单的示例:

<div>test</div>
div {
    font-size: 88px;
    color: #000;
}

上述的 HTML、CSS 文本在经过样式树构建器处理后生成的样式树如下:

{
    "node": { // DOM 节点
        "tagName": "div",
        "attributes": {},
        "children": [
            {
                "nodeValue": "test",
                "nodeType": 3
            }
        ],
        "nodeType": 1
    },
    "values": { // CSS 属性值
        "font-size": "88px",
        "color": "#000"
    },
    "children": [ // style tree 子节点
        {
            "node": {
                "nodeValue": "test",
                "nodeType": 3
            },
            "values": { // text 节点继承了父节点样式
                "font-size": "88px",
                "color": "#000"
            },
            "children": []
        }
    ]
}

遍历 DOM 树

现在我们需要遍历 DOM 树。对于 DOM 树中的每个节点,我们都要在样式树中查找是否有匹配的 CSS 规则。

export function getStyleTree(eles: Node | Node[], cssRules: Rule[], parent?: StyleNode) {
    if (Array.isArray(eles)) {
        return eles.map((ele) => getStyleNode(ele, cssRules, parent))
    }
    return getStyleNode(eles, cssRules, parent)
}

匹配选择器

匹配选择器实现起来非常容易,因为我们的CSS 解析器仅支持简单的选择器。 只需要查看元素本身即可判断选择器是否与元素匹配。

/**
 * css 选择器是否匹配元素
 */
function isMatch(ele: Element, selectors: Selector[]) {
    return selectors.some((selector) => {
        // 通配符
        if (selector.tagName === '*') return true
        if (selector.tagName === ele.tagName) return true
        if (ele.attributes.id === selector.id) return true
        if (ele.attributes.class) {
            const classes = ele.attributes.class.split(' ').filter(Boolean)
            const classes2 = selector.class.split(' ').filter(Boolean)
            for (const name of classes) {
                if (classes2.includes(name)) return true
            }
        }
        return false
    })
}

当查找到匹配的 DOM 节点后,再将 DOM 节点和它匹配的 CSS 属性组合在一起,生成样式树节点 styleNode:

function getStyleNode(ele: Node, cssRules: Rule[], parent?: StyleNode) {
    const styleNode: StyleNode = {
        node: ele,
        values: getStyleValues(ele, cssRules, parent),
        children: [],
    }
    if (ele.nodeType === NodeType.Element) {
        // 合并内联样式
        if (ele.attributes.style) {
            styleNode.values = { ...styleNode.values, ...getInlineStyle(ele.attributes.style) }
        }
        styleNode.children = ele.children.map((e) => getStyleNode(e, cssRules, styleNode)) as unknown as StyleNode[]
    }
    return styleNode
}
function getStyleValues(ele: Node, cssRules: Rule[], parent?: StyleNode) {
    const inheritableAttrValue = getInheritableAttrValues(parent)
    // 文本节点继承父元素的可继承属性
    if (ele.nodeType === NodeType.Text) return inheritableAttrValue
    return cssRules.reduce((result: AnyObject, rule) => {
        if (isMatch(ele as Element, rule.selectors)) {
            result = { ...result, ...cssValueArrToObject(rule.declarations) }
        }
        return result
    }, inheritableAttrValue)
}

在 CSS 选择器中,不同的选择器优先级是不同的,比如 id 选择器就比类选择器的优先级要高。但是我们这里没有实现选择器优先级,为了实现简单,所有的选择器优先级是一样的。

继承属性

文本节点无法匹配选择器,那它的样式从哪来?答案就是继承,它可以继承父节点的样式。

在 CSS 中存在很多继承属性,即使子元素没有声明这些属性,也可以从父节点里继承。比如字体颜色、字体家族等属性,都是可以被继承的。为了实现简单,这里只支持继承父节点的 colorfont-size 属性。

// 子元素可继承的属性,这里只写了两个,实际上还有很多
const inheritableAttrs = ['color', 'font-size']
/**
 * 获取父元素可继承的属性值
 */
function getInheritableAttrValues(parent?: StyleNode) {
    if (!parent) return {}
    const keys = Object.keys(parent.values)
    return keys.reduce((result: AnyObject, key) => {
        if (inheritableAttrs.includes(key)) {
            result[key] = parent.values[key]
        }
        return result
    }, {})
}

内联样式

在 CSS 中,内联样式的优先级是除了 !important 之外最高的。

<spanstyle="color: red; background: yellow;">

我们可以在调用 getStyleValues() 函数获得当前 DOM 节点的 CSS 属性值后,再去取当前节点的内联样式值。并对当前 DOM 节点的 CSS 样式值进行覆盖。

styleNode.values = { ...styleNode.values, ...getInlineStyle(ele.attributes.style) }
function getInlineStyle(str: string) {
    str = str.trim()
    if (!str) return {}
    const arr = str.split(';')
    if (!arr.length) return {}
    return arr.reduce((result: AnyObject, item: string) => {
        const data = item.split(':')
        if (data.length === 2) {
            result[data[0].trim()] = data[1].trim()
        }
        return result
    }, {})
}
目录
相关文章
|
JavaScript 前端开发 容器
从零开始实现一个玩具版浏览器渲染引擎(三)
从零开始实现一个玩具版浏览器渲染引擎(三)
50 0
|
前端开发 JavaScript API
从零开始实现一个玩具版浏览器渲染引擎(四)
从零开始实现一个玩具版浏览器渲染引擎(四)
92 0
|
Rust 自然语言处理 前端开发
从零开始实现一个玩具版浏览器渲染引擎(一)
从零开始实现一个玩具版浏览器渲染引擎
76 0
|
Web App开发 JavaScript 前端开发
浏览器渲染引擎工作原理|学习笔记
快速学习浏览器渲染引擎工作原理
浏览器渲染引擎工作原理|学习笔记
|
Web App开发 存储 移动开发
浏览器内核(渲染引擎)介绍|学习笔记
快速学习浏览器内核(渲染引擎)介绍
|
Web App开发 存储 JavaScript
浏览器渲染引擎与阻塞
浏览器渲染引擎与阻塞
|
Web App开发 JavaScript 前端开发
画了20张图,详解浏览器渲染引擎工作原理(下)
今天我们来学习一下浏览器渲染引擎的工作原理,文章内容较多,建议先收藏再学习! 先来看看Chrome浏览器的架构图:
312 0
|
17天前
|
JSON 移动开发 JavaScript
在浏览器执行js脚本的两种方式
【10月更文挑战第20天】本文介绍了在浏览器中执行HTTP请求的两种方式:`fetch`和`XMLHttpRequest`。`fetch`支持GET和POST请求,返回Promise对象,可以方便地处理异步操作。`XMLHttpRequest`则通过回调函数处理请求结果,适用于需要兼容旧浏览器的场景。文中还提供了具体的代码示例。
在浏览器执行js脚本的两种方式
|
15天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端大模型入门:Transformer.js 和 Xenova-引领浏览器端的机器学习变革
除了调用API接口使用Transformer技术,你是否想过在浏览器中运行大模型?Xenova团队推出的Transformer.js,基于JavaScript,让开发者能在浏览器中本地加载和执行预训练模型,无需依赖服务器。该库利用WebAssembly和WebGPU技术,大幅提升性能,尤其适合隐私保护、离线应用和低延迟交互场景。无论是NLP任务还是实时文本生成,Transformer.js都提供了强大支持,成为构建浏览器AI应用的核心工具。
456 1

热门文章

最新文章

下一篇
无影云桌面