「如何从零到一实现一个玩具浏览器🌏」

3,376 阅读10分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

大家好,我是速冻鱼🐟,一条水系前端💦,喜欢花里胡哨💐,持续沙雕🌲,是隔壁寒草🌿的好兄弟,刚开始写文章。 如果喜欢我的文章,可以关注➕点赞,为我注入能量,与我一同成长吧~

阅读本文 📖

1.您将了解到什么是有限状态机

2.您将了解到浏览器渲染基本流程与原理

3.您将和我一起完成一个玩具浏览器的编写

本文仓库地址:toy-browser

前言 🌵

最近在学习浏览器渲染原理,光知道理论还不行🌝,我们得动手实践才能更深入的了解浏览器渲染背后的点点滴滴💧,下面分享给大家

前置知识 💻

1.什么是有限状态机 ⭐

有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。

  • 每一个状态都是一个机器
    • 在每一个机器里,我们可以做计算、存储、输出......
    • 所有的这些机器接受的输入是一致的
  • 状态机的每一个机器本身没有状态,如果我们用函数来表示的话,它应该是纯函数(无副作用)
  • 每一个机器知道下一个状态
    • 每个机器都有确定的下一个状态(Moore)
    • 每个机器根据输入决定下一个状态(Mealy)

简单说,它有三个特征:

  1. 状态总数(state)是有限的。
  2. 任一时刻,只处在一种状态之中。
  3. 某种条件下,会从一种状态转变(transition)到另一种状态。

举例来说,网页上有一个菜单元素。鼠标悬停的时候,菜单显示;鼠标移开的时候,菜单隐藏。如果使用有限状态机描述,就是这个菜单只有两种状态(显示和隐藏),鼠标会引发状态转变。现在还不太了解没关系,后边我们看代码就好理解多了,这里对状态机不做过多描述。

感兴趣可以看看阮一峰老师的文章JavaScript与有限状态机

极客时间Winter大佬的重学前端也有相关内容

2.我们使用有限状态机来解决什么问题 🌟

有限状态机的写法,逻辑清晰,表达力强,有利于封装事件。一个对象的状态越多、发生的事件越多,就越适合采用有限状态机的写法。

比如使用有限状态机处理字符串

在一个字符串中,如何使用状态机找到字符“abcdef”

function findStr(str) {
    let state= start;
    for (const c of str) {
        state=state(c)
    }
    return state===end
}

function start(c) {
    if (c === 'a') {
        return findA
    } else return start
}

function end(c) {
    return end
}

function findA(c) {
    if (c === 'b') {
        return findB
    } else return start(c)
}

function findA2(c) {
    if (c === 'b') {
        return findB2
    } else return start(c)
}
function findA3(c) {
    if (c === 'b') {
        return findB3
    } else return start(c)
}


function findB(c) {
    if (c === 'a') {
        return findA2
    } else return start(c)

}

function findB2(c) {
    if (c === 'a') {
        return findA3
    } else return start(c)

}

function findB3(c) {
    if (c === 'x') {
        return end
    } else return start(c)

}



console.log(findStr('aaabxababx'))

后边我们实战中也将会使用状态机来对html文本进行解析构建我们的DOM树

3.浏览器渲染的大致流程 💫

  • 发送HTTP请求获取HTML

  • 对获取到的HMTL进行解析得到一颗光秃秃的DOM树

  • 对获取到的CSS进行计算,将计算出来的值添加到DOM树上,形成一棵带有CSS样式属性的渲染树

  • 有了渲染树,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系,从而去计算出每个节点在屏幕中的位置,大小

  • 有了每个dom的位置大小信息后,浏览器就可以将各个节点绘制到屏幕上了

下面不说废话直接开搞^_^

实现流程 🌊

tips☀️:以下代码有点长,不想查看细节的小伙伴可以直接看后边总结,也可以到toy-browser查看源码

这里我不会很详细的去介绍代码的每一步实现,重要的想让大家对整个渲染流程有个全面的认识🍎

1.用node模拟我们的服务端 🐻

接收请求,返回我们的HTML就是我们的node服务要做的事情,就这么简单^_^

server.js

const http = require("http");

http
  .createServer((req, res) => {
    let body = [];
    req
      .on("error", (err) => {
        console.error(err);
      })
      .on("data", (chunk) => {
        console.log("chunk", chunk);
        body.push(chunk);
      })
      .on("end", () => {
        body = Buffer.concat(body).toString();
        console.log("body", body);
        res.writeHead(200, { "Content-Type": "text/html" });
        res.end(
`<html uname=sudongyu>
<head>
   <style>
    #container {
    width: 500px;
    height: 300px;
    display: flex;
    background-color: rgb(255,255,255);
}
#container #myid{
    width: 200px;
    height: 100px;
    background-color: rgb(255,0,0);
}
#container .c1{
    flex: 1;
    background-color: rgb(0,255,0);
}
   </style>
</head>
<body style="background: black">
     <div id="container">
        <div id="myid"></div>
        <div class="c1"></div>
    </div>
</body>
</html>`
        );
      });
  })
  .listen("8088");
console.log("server started");

2.客户端编写 🐼

在客户端我们会发送http请求->获取响应报文->解析响应体->获取html文本信息->对html文本进行解析->获取dom树->计算css属性->获取渲染树->layout->获取有位置的dom树->render->Bitmap->浏览器展示我们的画面

先从整体看看我们client需要做什么,看不懂没关系,我会分开解释每一个流程在代码中的具体实现

client.js

2.1 发送http请求获取html

tips⭐:以下代码只展示了核心调用部分,想要看全部实现的小伙伴可以查看我的源码toy-browser

parser.js

2.2 对获取到的html文本进行词法分析获取token

敲黑板👨‍🏫这里就要开始用到我们上文提到的有限状态机对html进行解析了哦

tips⭐:以下代码只展示了核心调用部分,想要看全部实现的小伙伴可以查看我的源码toy-browser

对html的每一个字符使用有限状态机进行词法分析,形成token(token指有效部分,这里可以理解为一个htm标签,eg:<div>、<html>就算是一个token)

parser.js

由于状态太多,这里只例举了部分状态,我们要通过这个状态机对html的每个字符进行词法分析得到token好进行后边的语法分析

parser.js

2.3 对获取到的token进行语法分析构建dom树

我们拿到每一个词法分析过后的token进行语法解析,根据每个token的属性执行不同的逻辑来构建我们的语法树🌳(其实我们css计算也会在最初emittoken的时候进行)

使用栈这个数据结构来维护我们的dom树🌴,根据每个tokentype来对Node节点进行入栈和出栈的操作,最后遍历完每个token,对每个token进行逻辑处理后,栈顶只剩下我们的document对象,这个document对象就是我们dom树的对象表现形式,它的children属性就保存了dom树的层级结构

)

parser.js

let stack = [{ type: "document", children: [] }]; //doms树解析用的栈

根据每个token不同的type,执行不同的逻辑,添加Node到我们的dom树🌴上

parser.js

当遇到type为style的token时,使用一个数组rules[]来维护这个样式规则

parser.js

let rules=[];

/**
 * 添加样式规则的方法
 * @param text
 */
function addCSSRules(text){
    //调用css这个现成的库对css样式文本进行词法语法分析获取css的Ast
    var ast=css.parse(text);
    // console.log(JSON.stringify(ast,null,4));
    rules.push(...ast.stylesheet.rules);
}

2.4 对dom树进行css计算并获取渲染树

其实这一步我们是在获取到token并emit的时候就会进行css计算,为了方便理解,所以单独划分一步。

可以看到这里我们拿到token后,进行语法分析的时候就会进行css计算

parser.js

dom树的每个元素节点进行css计算,计算完成后,每个元素节点对象上就会维护一个computedStyle属性,这样我们的dom树就变成了一颗带有css样式渲染树了🎄

parser.js

2.5 对渲染树的每个元素进行位置的计算

这里根据浏览器的排版规则来对我们设置的属性进行位置的计算,这里我们只实现了flex这个排版的算法,因为它比较容易实现,能力又不是太差,这里只是为了感受排版的过程🍃。

浏览器排版规则包括🌻:

第一代就是 正常流 —— 包含了 position, display,flow;

第二代就是 flex —— 这个就比较接近人的自然思维;

第三代就是 grid —— 是一种更强大的排版模式;

第四代可能是 Houdini —— 是一组底层 API,它们公开了 CSS 引擎的各个部分,从而使开发人员能够通过加入浏览器渲染引擎的样式和布局过程来扩展 CSS。

同样是在parser.js解析语法树的时候调用layout函数对我们的dom元素进行位置计算

parser.js

解析dom元素上的computedStyle属性然后根据我们的flex排版规则计算出dom元素的位置大小获得一颗带有位置的dom树🎄

layout.js

2.6 万事俱备,只欠东风!有了一棵带样式、带位置的dom树,我们就可以进行最后一步渲染啦

经历千辛万苦的dom树解析,我们终于拥有一棵带样式、带位置的dom树,这里我们使用images这个开源库模拟我们的浏览器渲染,最终会在src目录生成一张图片来模拟浏览器渲染。

使用npm或者yarn安装images开源库🍉,这个库可以帮助我们生成图片,在client.js中调用render函数进行我们的渲染过程,最终生成图片🖼。

client.js

render函数中,我们遍历元素的属性,获取宽高背景颜色,调用images库提供的API完成渲染逻辑

render.js

2.7 最后展示我们渲染过后生成的图片🖼

viewport.jpg

总结 🍁

终于终于终于历经千辛万苦🌈,我们终于从客户端发送http请求到服务端响应请求解析响应报文获取html文本,通过词法语法解析html文本获取dom树🎄,在解析html过程中进行了css属性计算样式匹配位置计算最终获取到了一棵带有样式,有位置dom树🎄,最后完成渲染的完整过程,不知道小伙伴们是不是感觉收获满满🍉,也对整个浏览器渲染流程有了一个完整的认识,如果你还是有很多疑问❓,您可以到toy-browser下载这个项目,在本地跑一下,自己感受一下整个过程🤓,我相信效果可能会更好~

toy-browser源代码仓库地址:toy-browser👣

滴滴:本文是通过自己的学习与理解,以及查阅资料最终完成的,在语言表达上面肯定有很多不严谨的地方,或者表达错误的地方,对知识的理解可能不是很全面,希望大家保持一颗辩证的心来阅读,更好的是自己去实践一下,通过我的源码,自己去深入研究一下,效果肯定是极好的,感谢您的阅读~~~

参考文献 📚

结束语 🌞

那么我的第三篇文章就结束了,文章的目的其实很简单,就是对日常工作的总结和输出,输出一些觉得对大家有用的东西,菜不菜不重要,但是热爱🔥,希望大家能够喜欢我的文章,我真的很用心在写,也希望通过文章认识更多志同道合的朋友,如果你也喜欢折腾,欢迎加我好友,一起沙雕,一起进步

github🤖:sudongyu

个人博客👨‍💻:速冻鱼blog

vx👦:sudongyuer

写在最后

伙伴们,写作不易,如果喜欢我的口水话给🐟🐟点一个赞👍或者关注➕都是对我最大的支持。

加我微信:sudongyuer,邀你进群,一起学习前端,成为更优秀的工程师~(群二维码在这里->前端要早睡, 二维码过期了的话看链接沸点中的评论,我会把最新的二维码放在评论区,当然也可以加我微信我拉你进群,毕竟我也是有趣的前端,认识我也不赖🌟~)