写点什么

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

作者:速冻鱼
  • 2021 年 12 月 11 日
  • 本文字数:4221 字

    阅读完需:约 14 分钟

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

大家好,我是速冻鱼🐟,一条水系前端💦,喜欢花里胡哨💐,持续沙雕🌲欢迎小伙伴们加我微信sudongyuer拉你进群关注我的公众号:前端速冻鱼一起进步,期待与大家共同成长🥂

阅读本文 📖

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 文本进行解析构建我们的抽象语法树(AST)

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 计算也就会在最初生成 dom 树的时候进行)


使用栈这个数据结构来维护我们的 dom 树🌴,根据tokentype来对Node进行入栈和出栈的操作,最后遍历完每个 token,对每个 token 进行逻辑处理后,栈顶只剩下我们的document对象,这个document对象就是我们 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 的时候就会进行 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


写在最后


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

发布于: 2021 年 12 月 11 日阅读数: 27
用户头像

速冻鱼

关注

Talk is cheap,show me your code 2021.08.30 加入

大家好,我是速冻鱼🐟,一条水系前端💦,喜欢花里胡哨💐,持续沙雕🌲 欢迎小伙伴们加我微信:sudongyuer拉你进群, 公众号: 前端速冻鱼,欢迎一起讨论,期待与大家共同成长🥂

评论

发布
暂无评论
「如何从零到一实现一个玩具浏览器🌏」