接上一篇博客浏览器工作原理——动手实现一个toy-browser(二):生成DOM树和计算CSS,本篇主要介绍浏览器工作流程的排版和渲染环节。
目录
1 浏览器工作原理——排版生成带位置的DOM
1.1 根据浏览器属性进行排版
1.1.1 css的几代布局演变
- 传统布局
- 正常流
- float浮动
- position定位
- flex弹性布局
- grid网格布局
1.1.2 flex网格布局相关属性
- flex-direction: row(水平排布)
- Main(排版时元素的延伸方向): width, x, left, right
- Cross: height, y, top, bottom
- flex-direction: column(垂直排布)
- Main: height, y, top, bottom
- Cross: width, x, left, right
使用主轴和交叉轴的概念对排版方向进行抽象,以便能够适应多种书写方式。
1.1.3 排版的准备工作
- 设定flex相关属性(flexDirection, justifyContent, alignContent, alignItems, flexWrap)的默认值
- 根据flexDirection和flexWrap的值设定抽象的mainSize,mainStart,mainEnd,mainSign,mainBase等变量,方便后续代码的编写
function getStyle(element) {
if(!element.style) {
element.style = {}
}
for(let prop in element.computedStyle) {
element.style[prop] = element.computedStyle[prop].value
// 带px的样式值转成像素值
if(element.style[prop].toString().match(/(px)$/)) {
element.style[prop] = parseInt(element.style[prop])
}
// 数值型样式值转数值
if(element.style[prop].toString().match(/^[0-9]+(\.[0-9]+)?$/)) {
element.style[prop] = parseFloat(element.style[prop])
}
}
return element.style
}
var elementStyle = getStyle(element)
if(elementStyle.display !== "flex") {
return // 暂时只处理flex布局
}
var items = element.children.filter(item => {return item.type === "element"}) // 将文本节点滤除
items.sort((a, b) => {// 按flex的order属性升序排序
return (a.order || 0) - (b.order || 0)
})
var style = elementStyle
;["width", "height"].forEach(size => {// 宽高缺失值的处理
if(style[size] === "auto" || style[size] === "") {
style[size] = null
}
})
// 给flex的各属性设置默认值
if(!style.flexDirection || style.flexDirection === "auto") {
style.flexDirection = "row"
}
if(!style.justifyContent || style.justifyContent === "auto") {
style.justifyContent = "flex-start"
}
if(!style.alignContent || style.alignContent === "auto") {
style.alignContent = "flex-start"
}
if(!style.alignItems || style.alignItems === "auto") {
style.alignItems = "stretch"
}
if(!style.flexWrap || style.flexWrap === "auto") {
style.flexWrap = "nowrap"
}
// 根据flexDirection和flexWrap的值设置相关抽象参数
let mainSize, mainStart, mainEnd, mainBase, mainSign,
crossSize, crossStart, crossEnd, crossBase, crossSign
if(style.flexDirection === "row") {
mainSize = "width"
mainStart = "left"
mainEnd = "right"
mainSign = +1
mainBase = 0
crossSize = "height"
crossStart = "top"
crossEnd = "bottom"
}else if(style.flexDirection === "row-reverse") {
mainSize = "width"
mainStart = "right"
mainEnd = "left"
mainSign = -1
mainBase = style.width
crossSize = "height"
crossStart = "top"
crossEnd = "bottom"
}else if(style.flexDirection === "column") {
mainSize = "height"
mainStart = "top"
mainEnd = "bottom"
mainSign = +1
mainBase = 0
crossSize = "width"
crossStart = "left"
crossEnd = "right"
}else if(style.flexDirection === "column-reverse") {
mainSize = "height"
mainStart = "bottom"
mainEnd = "top"
mainSign = -1
mainBase = style.height
crossSize = "width"
crossStart = "left"
crossEnd = "right"
}
if(style.flexWrap === "wrap-reverse") {
var tmp = crossStart
crossStart = crossEnd
crossEnd = tmp
crossSign = -1
crossBase = crossSize === "width" ? style.width : style.height
}else{
crossSign = +1
crossBase = 0
}
// 未设置mainSize,进行auto sizing,计算所有子元素排进一行的mainSize
let isAutoMainSize = false
if(!style[mainSize]) {
style[mainSize] = 0
for(let item of element.children) {
let itemStyle = getStyle(item)
if(itemStyle[mainSize] !== null && itemStyle[mainSize] !== void(0)) {
style[mainSize] += itemStyle[mainSize]
}
}
isAutoMainSize = true
}
1.2 收集元素进行
- 根据主轴尺寸将元素排进行
- 如果设置了nowrap属性,所有元素强行分配进第一行
// flex的分行算法
let flexLine = []
let flexLines = [flexLine]
let mainSpace = style[mainSize]
let crossSpace = 0
// 将元素排进行
for(let item of items) {
let itemStyle = getStyle(item)
if(itemStyle[mainSize] === null || itemStyle[mainSize] === void(0)) {
itemStyle[mainSize] = 0
}
if(itemStyle[crossSize] === null || itemStyle[crossSize] === void(0)) {
itemStyle[crossSize] = 0
}
if(itemStyle.flex) {// 元素具有flex属性,可伸缩,一定能放进当前行
flexLine.push(item)
}else if(style.flexWrap === "nowrap" && isAutoMainSize) {//不换行且mainSize已计算出,元素依次排入一行
mainSpace -= itemStyle[mainSize]
if(itemStyle[crossSize] !== null && itemStyle[crossSize] !== void(0)) {
crossSpace = Math.max(itemStyle[crossSize], crossSpace)
}
flexLine.push(item)
}else{// 排入多行
if(itemStyle[mainSize] > style[mainSize]) {
itemStyle[mainSize] = style[mainSize]
}
if(mainSpace < itemStyle[mainSize]) {// 主轴剩余空间放不下当前元素,结束当前行,另起一行
flexLine.mainSpace = mainSpace
flexLine.crossSpace = crossSpace
flexLines.push(flexLine)
flexLine = [item]
mainSpace = element[mainSize]
crossSpace = 0
}else{
flexLine.push(item)
}
// 更新主轴剩余空间和交叉轴空间
mainSpace -= itemStyle[mainSize]
if(itemStyle[crossSize] !== null && itemStyle[crossSize] !== void(0)) {
crossSpace = Math.max(itemStyle[crossSize], crossSpace)
}
}
}
flexLine.mainSpace = mainSpace
if(style.flexWrap === "nowrap" || isAutoMainSize) {
// 如果style(即elementStyle)定义了crossSize,将其直接作为flexLine的crossSpace
flexLine.crossSpace = (style[crossSize] !== undefined) ? style[crossSize] : crossSpace
}else{
flexLine.crossSpace = crossSpace
}
1.3 计算主轴方向
- 如果主轴的剩余空间小于0,将所有flex项的mainBase设为0,然后将剩余元素等比例缩小并确定位置
- 如果主轴的剩余空间大于0, 循环处理每个flexLine
- 统计每个flexLine的所有flex项
- 如果flex项数量大于0,将所有flex项的mainBase按flex比例分配主轴剩余空间,依次确定主轴位置
- 否则根据justifyContent确定flexLine中各元素的位置
// 计算主轴方向
if(mainSpace < 0) {
// 排不下,将所有flex元素的mainSize设为0,非flex元素等比例压缩
let scale = style[mainSize]/ (style[mainSize] - mainSpace)
let currentBase = mainBase
flexLine.forEach((item)=> {
let itemStyle = getStyle(item)
if(itemStyle.flex) {
itemStyle[mainSize] = 0
}
itemStyle[mainSize] *= scale
itemStyle[mainStart] = currentBase
itemStyle[mainEnd] = itemStyle[mainStart] + mainSign*itemStyle[mainSize]
currentBase = itemStyle[mainEnd]
})
}else{
// 排得下,遍历flexLines,根据flexLine的flex项数量决定使用按flex比例排版还是依据justifyContent排版
flexLines.forEach(items=>{
let mainSpace = items.mainSpace
let flexTotal = 0
for(let i=0; i<items.length; i++) {
let item = items[i]
let itemStyle = getStyle(item)
if(itemStyle.flex) {
flexTotal += itemStyle.flex
}
}
let currentBase = mainBase
if(flexTotal > 0) {
for(let i=0; i<items.length; i++) {
let item = items[i]
let itemStyle = getStyle(item)
itemStyle[mainStart] = currentBase
if(itemStyle.flex) {
// 将mainSpace空间按flex比例分配
itemStyle[mainSize] = mainSpace * itemStyle.flex / flexTotal
}
itemStyle[mainEnd] = itemStyle[mainStart] + mainSign*itemStyle[mainSize]
currentBase = itemStyle[mainEnd]
}
}else{
// 根据justifyContent确定currentBase和step,实现非flex元素的排版
let step = 0 // 间距
if(items.justifyContent === "flex-end") {
currentBase = mainSpace*mainSign + mainBase
}
if(items.justifyContent === "center") {
currentBase = mainSpace/2*mainSign + mainBase
}
if(items.justifyContent === "space-between") {
step = mainSign*mainSpace/(items.length-1)
}
if(items.justifyContent === "space-around") {
step = mainSign*mainSpace/items.length
currentBase = mainBase + step/2
}
for(let i=0; i<items.length; i++) {
let item = items[i]
let itemStyle = getStyle(item)
itemStyle[mainStart] = currentBase
itemStyle[mainEnd] = itemStyle[mainStart] + mainSign*itemStyle[mainSize]
currentBase = itemStyle[mainEnd] + step
}
}
})
}
1.4 计算交叉轴方向
- 根据一行中最大元素尺寸计算行高
- 根据alignContent和各行行高确定各行的基准位置和间隔
- 根据alignItems确定行内元素具体位置
1.2~1.4节相关代码
// 计算交叉轴方向
if(!style[crossSize]) { // 未定义交叉轴尺寸,累加各行得到elementStyle[crossSize]
crossSpace = 0
style[crossSize] = 0
for(let i=0; i<flexLines.length; i++) {
style[crossSize] += flexLines[i][crossSpace]
}
}else{// 已定义交叉轴尺寸,减去各行剩余空间得到整个剩余空间
crossSpace = style[crossSize]
for(let i=0; i<flexLines.length; i++) {
crossSpace -= flexLines[i][crossSpace]
}
}
if(style.flexWrap === "wrap-reverse") {
crossBase = style[crossSize]
}else{
crossBase = 0
}
// let lineHeight = style[crossSize] / flexLines.length
// 根据alignContent属性确定crossBase和step
let step
if(style.alignContent === "flex-start") {
crossBase += 0
step = 0
}
if(style.alignContent === "flex-end") {
crossBase += crossSpace*crossSign
step = 0
}
if(style.alignContent === "center") {
crossBase += (crossSpace/2)*crossSign
step = 0
}
if(style.alignContent === "space-between") {
step = (crossBase/ (flexLines.length-1))*crossSign
crossBase += 0
}
if(style.alignContent === "space-around") {
step = (crossBase/ flexLines.length)*crossSign
crossBase += step/2
}
if(style.alignContent === "stretch") {
crossBase += 0
step = 0
}
// 对flexLine进行交叉轴排版
flexLines.forEach(items => {
let lineCrossSize = style.alignContent === "stretch" ?
items.crossSpace + crossSpace/items.length : // 将剩余空间平均分配给每行
items.crossSpace // 每行的交叉轴空间
// 对行内元素进行交叉轴排版
for(let i=0; i<items.length; i++) {
let item = items[i]
let itemStyle = getStyle(item)
let align = itemStyle.alignSelf || style.alignItems
if(item === null) {
itemStyle[crossSize] = (align === "stretch") ? lineCrossSize : 0
}
if(align === "flex-start") {
itemStyle[crossStart] = crossBase
itemStyle[crossEnd] = itemStyle[crossStart] + crossSign*itemStyle[crossSize]
}
if(align === "flex-end") {
itemStyle[crossEnd] = crossBase + crossSign*lineCrossSize
itemStyle[crossStart] = itemStyle[crossEnd] - crossSign*itemStyle[crossSize]
}
if(align === "center") {
itemStyle[crossStart] = crossBase + crossSign*(lineCrossSize - itemStyle[crossSize])/2
itemStyle[crossEnd] = itemStyle[crossStart] + crossSign*itemStyle[crossSize]
}
if(align === "stretch") {
itemStyle[crossStart] = crossBase
itemStyle[crossEnd] = crossBase + crossSign*lineCrossSize // 此处与视频不同
itemStyle[crossSize] = crossSign*lineCrossSize
}
}
crossBase += crossSign*(lineCrossSize+step)
console.log(items)
})
}
2 浏览器工作原理——渲染
2.1 绘制单个元素
- 绘制需要依赖一个图形环境,出于轻量化依赖少的考虑,课程采用npm的images库
- 绘制在一个viewport上进行
- 与绘制相关的属性,background-color,border,background-image等
2.2 绘制dom树
- 递归调用子元素的绘制方法即可完成整个dom树的绘制
- 实际浏览器中,文字绘制是难点,需要依赖字体库,课程中忽略
- 实际浏览器中,还会对一些图层做compositing,课程中也予以忽略
2.1~2.2 相关代码
const images = require("images")
function render(viewport, element) {
if(element.style) {
let img = images(element.style.width, element.style.height)
let color = element.style["background-color"]
if(color) {
let pattern = /rgb\((\d+), (\d+), (\d+)\)/
color.match(pattern)
img.fill(Number(RegExp.$1), Number(RegExp.$2), Number(RegExp.$3))
viewport.draw(img, element.style.left||0, element.style.top||0)
}
}
if(element.children) {
for(let child of element.children) {
render(viewport, child)
}
}
}
3 最终渲染结果
利用我们编写的toy-browser程序,下面html文档的最终渲染结果为
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="./foo.js"></script>
<title>Document</title>
<style>
body {
background-color: black;
}
.container {
width: 500px;
height: 300px;
display: flex;
background-color: rgb(255, 255, 255);
align-items: center;
}
.pText {
width: 200px;
}
p.text {
display: none;
}
p.text#name {
font-size: 20px;
color: red;
background-color: blue;
}
body div #myImg {
flex: 1;
height: 200px;
background-color: rgb(255, 0, 0);
}
body div #hisImg2 {
flex: 2;
height: 300px;
background-color: rgb(0, 255, 0);
}
/* .classImg {
margin: 10px;
}
.myClass {
border: 2px;
} */
</style>
</head>
<body>
<div class="container">
<div class="pText">
<p class="text">Hello world</p>
<p class="text" id="name">My name is blateyang</p>
</div>
<div id="myImg" class="classImg myClass"></div>
<div id="hisImg2" class="classImg myClass"></div>
</div>
</body>
</html>
4 toy-browser总结
通过三周的学习和实践,我们对浏览器的工作原理有了较为清晰深入地认识。知道了如何利用有限状态机对HTML文档进行词法和语法分析将其转换成一颗DOM树,CSS规则又是在何时被添加到DOM节点中以及如何与节点进行匹配的,如何根据flex属性对DOM节点进行弹性布局生成带位置信息的DOM树,如何将带位置信息和样式信息的DOM树渲染成网页位图。当然上述流程是对浏览器工作流程的一个简化,实际的浏览器工作流程还包括在发送请求时对请求的分析处理(是否跨域、是否发送预检请求)、生成网页位图后因执行js代码引发的重绘和回流等其它工作,布局排版的实现也仅仅是实现了基本的flex布局,还有很多可以扩展完善的地方,比如在flex布局中增加对margin、padding、border等属性的支持。本文的主要目的是通过实现一个简易的toy-browser,加深对浏览器工作原理的认识和理解,有兴趣的朋友可以在此基础上继续完善。
5 本节代码github地址
如需查看完整代码,请移步github
PS:如果你对本文有任何疑问,可以留言评论或私信,我看到了会尽量回复。如果对你有所帮助,欢迎点赞转发_