大家好,我是速冻鱼🐟,一条水系前端💦,喜欢花里胡哨💐,持续沙雕🌲,是隔壁寒草🌿的好兄弟,刚开始写文章。 如果喜欢我的文章,可以关注➕点赞,为我注入能量,与我一同成长吧~
阅读本文 📖
1.您将了解到什么是有限状态机
2.您将了解到浏览器渲染基本流程与原理
3.您将和我一起完成一个玩具浏览器的编写
本文仓库地址:toy-browser
前言 🌵
最近在学习浏览器渲染原理,光知道理论还不行🌝,我们得动手实践才能更深入的了解浏览器渲染背后的点点滴滴💧,下面分享给大家
前置知识 💻
1.什么是有限状态机 ⭐
有限状态机(Finite-state machine)是一个非常有用的模型,可以模拟世界上大部分事物。
- 每一个状态都是一个机器
- 在每一个机器里,我们可以做计算、存储、输出......
- 所有的这些机器接受的输入是一致的
- 状态机的每一个机器本身没有状态,如果我们用函数来表示的话,它应该是纯函数(无副作用)
- 每一个机器知道下一个状态
- 每个机器都有确定的下一个状态(Moore)
- 每个机器根据输入决定下一个状态(Mealy)
简单说,它有三个特征:
- 状态总数(state)是有限的。
- 任一时刻,只处在一种状态之中。
- 某种条件下,会从一种状态转变(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:、就算是一个
parser.js
由于状态太多,这里只例举了部分状态,我们要通过这个状态机对html的每个字符进行词法分析得到token好进行后边的语法分析
parser.js
2.3 对获取到的token进行语法分析构建dom树
我们拿到每一个词法分析过后的token
进行语法解析,根据每个token
的属性执行不同的逻辑来构建我们的语法树🌳(其实我们css计算也会在最初emittoken
的时候进行)
使用栈这个数据结构来维护我们的dom树🌴,根据每个token
的type
来对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 最后展示我们渲染过后生成的图片🖼
总结 🍁
终于终于终于,历经千辛万苦🌈,我们终于从客户端发送http请求
到服务端响应请求
,解析响应报文
,获取html文本
,通过词法语法
解析html文本
获取dom树
🎄,在解析html
过程中进行了css属性计算
,样式匹配
,位置计算
最终获取到了一棵带有样式
,有位置
的dom树
🎄,最后完成渲染
的完整过程
,不知道小伙伴们是不是感觉收获满满🍉,也对整个浏览器渲染流程有了一个完整的认识
,如果你还是有很多疑问❓,您可以到toy-browser下载这个项目,在本地跑一下,自己感受一下整个过程🤓,我相信效果可能会更好~
toy-browser源代码仓库地址:toy-browser👣
滴滴
:本文是通过自己的学习与理解,以及查阅资料最终完成的,在语言表达上面肯定有很多不严谨的地方,或者表达错误的地方,对知识的理解可能不是很全面,希望大家保持一颗辩证的心来阅读,更好的是自己去实践一下,通过我的源码,自己去深入研究一下,效果肯定是极好的,感谢您的阅读~~~
参考文献 📚
结束语 🌞
那么我的第三篇文章
就结束了,文章的目的其实很简单,就是对日常工作的总结和输出
,输出一些觉得对大家有用的东西,菜不菜不重要,但是热爱🔥,希望大家能够喜欢我的文章,我真的很用心在写,也希望通过文章认识更多志同道合的朋友,如果你也喜欢折腾
,欢迎加我好友
,一起沙雕
,一起进步
。