布局一直是一个线性过程,可以独立处理闭合标签(例如<p></p>)和空标签(例如<link/>)。但网页节点是树结构,因此边框和背景在视觉上相互嵌套。本文介绍浏览器怎样将元素树转换为页面视觉元素的布局对象树。经过这个过程,网页将呈现得更加丰富多彩。
Layout树
浏览器通过处理闭合标签和空标签生成一棵HTML文档树。HTML树中的元素不会计算其宽度、高度、背景颜色等信息。
在浏览器中,布局是指生成一棵layout树,其节点是layout对象,每个节点都与一个HTML元素关联,每个节点都有大小和位置。浏览器遍历HTML树生成layout树,然后计算每个layout对象的大小和位置,最后将每个layout对象绘制到屏幕上。
浏览器在构建layout树的过程中会创建一个名为BlockLayout的新类,它将表示layout树中的一个节点。与之前的Element类一样,多个layout对象形成一棵树。因此每个节点都有children和parent列表。还添加了一个previous字段。用它来计算尺寸和位置。
class BlockLayout: def __init__(self, node, parent, previous): self.node = node self.parent = parent self.previous = previous self.children = []
在layout方法中,循环遍历子节点,为每个子节点创建layout对象。
class BlockLayout: def layout(self): previous = None for child in self.node.children: next = BlockLayout(child, self,previous) self.children.append(next) previous = next
这段代码很复杂,因为它涉及两棵树。node和child是HTML树的一部分;而self、previous和next是layout树的一部分。这两棵树的结构相似,所以很容易混淆。上面的代码从node.children中读取子对象(在HTML树中)并写入self.children(在布局树中)。完成了从HTML树到layout树的构造。
接下来循环遍历self.children并调用child.layout方法来递归地构建整棵树:
def layout(self): # ... for child in self.children: child.layout()
稍后我们再讨论递归。
BlockLayout构造函数需要一个父节点,这个父节点是根节点,也是文档本身,我们称之为DocumentLayout:
每个HTML节点对应一个layout对象,通过递归调用layout方法来构建一个layout树,根节点对应一个额外的layout对象。看起来是这样的:
在本例中,有四个BlockLayout对象,如图所示的绿色线框,每个元素对应一个。根目录下还有一个DocumentLayout对象。
接下来浏览器要计算每个layout对象的大小和位置。并且不同的HTML元素的布局不同。因此需要不同种类的layout对象!
web开发人员无法访问layout树,因此尚未对其进行标准化,而且不同浏览器的layout树结构也不同。连名字都不匹配!Chrome称之为layout树,Safari称之为render树,Firefox称之为frame树。
Layout 模式
像<body>和<header>这样的元素包含垂直堆叠的块。但是像<p>和<h1>这样的元素包含文本,并将文本水平排列成行。简单地说,有两种布局模式,即两种元素相对于其子元素的布局方式:块布局和内联布局。
BlockLayout类是块布局。InlineLayout是内联布局(InlineLayout就是以前文章中的Layout类)。
将Layout类重命名为InlineLayout,并将其构造函数重命名为Layout。添加一个与BlockLayout类似的新构造函数:
class InlineLayout: def __init__(self, node, parent, previous): self.node = node self.parent = parent self.previous = previous self.children = []
在新的layout方法中,将参数tree替换为node:
class InlineLayout: def layout(self): # ... self.line = [] self.recurse(self.node) self.flush()
在layout和flush函数中初始化cursor_x和cursor_y:
浏览器需要为每个元素使用正确的布局对象:包含文本的使用InlineLayout,包含块元素的(如<div>inner)使用BlockLayout。
以下是块元素列表:
如果上面列表中的元素包含子元素,就使用BlockLayout,否则使用InlineLayout。将该逻辑应用于layout_mode函数中:
此函数还确保文本节点使用内联布局,而空元素使用块布局。接下来调用layout_mode来确定每个元素使用哪个布局模式:
layout树的根节点上有一个DocumentLayout,在内部节点上有BlockLayouts,在叶子节点上有InlineLayouts:
构建layout树后,接下来就可以继续计算树中layout对象的大小和位置。
在CSS中,布局模式由display属性设置。
大小和位置
默认情况下,layout对象是贪婪的,会占据它们所能占据的所有水平空间。所以它们的宽度就是其父对象的宽度:
self.width = self.parent.width
每个layout对象从其父对象的左边缘开始:
self.x = self.parent.x
layout对象的垂直位置取决于其先前同级对象的位置和高度。如果没有兄弟节点,则从父节点的上边缘开始:
if self.previous: self.y = self.previous.y + self.previous.heightelse: self.y = self.parent.y
在递归调用每个child的layout方法之前,首先计算父对象的width、x和y。而父对象的高度应为其子对象的高度之和:
self.height = sum([child.height for child in self.children])
BlockLayout的高度取决于其子级的高度,必须在递归后计算高度。
InlineLayout以相同的方式计算width、x和y,但高度取决于其y坐标。
class InlineLayout: def layout(self): # ... self.height = self.cursor_y - self.y
同样,在布局文本之前,必须计算宽度、x和y,但之后必须计算高度。这都是关于依赖顺序的。
DocumentLayout也需要一些布局代码,不过因为文档总是在同一个位置开始,所以非常简单:
class DocumentLayout: def layout(self): # ... self.width = WIDTH - 2*HSTEP self.x = HSTEP self.y = VSTEP child.layout() self.height = child.height + 2*VSTEP
请注意,在DocumentLayout中,内容边缘有内边距,左右内边距是HSTEP,上下内边距是VSTEP。
对于所有三种类型的layout对象,layout方法中的步骤和顺序应相同:
调用layout时,它首先为每个子元素创建一个layout对象。
然后,从parent和previous layout对象中读取并计算width、x坐标和y坐标。
接下来,递归调用child.layout() 方法对子节点进行布局。
最后,读取child.height并计算height。
为了满足大小和位置之间的依赖关系,必须遵守此顺序。
使用基于树的布局
既然布局对象具有大小和位置信息,浏览器就会使用这些信息来呈现页面本身。首先,浏览器在load方法中运行layout方法:
class Browser: def load(self, url): headers, body = request(url) self.nodes = HTMLParser(body).parse() self.document = DocumentLayout(self.nodes) self.document.layout()
回想一下,浏览器首先收集display列表,然后调用paint绘制列表中的内容来绘制网页。对于基于树的布局,我们通过递归layout树来收集display列表。
下面在DocumentLayout中添加paint函数:
class DocumentLayout: def paint(self, display_list): self.children[0].paint(display_list)
对于BlockLayout,在每个child上调用paint:
class BlockLayout: def paint(self, display_list): for child in self.children: child.paint(display_list)
对于InlineLayout,由于已经在其display_list变量中存储了要绘制的内容,因此将self.display_list追加到display_list中即可:
class InlineLayout: def paint(self, display_list): display_list.extend(self.display_list)
现在浏览器可以使用paint收集自己的display_list变量:
class Browser: def load(self, url): # ... self.display_list = [] self.document.paint(self.display_list) self.draw()
背景
浏览器经常使用layout树,一个简单且视觉上引人注目的用例是绘制背景。
背景是矩形,要绘制背景,首先是将矩形放在显示列表中。从概念上讲,显示列表包含两种类型的命令:
现在InlineLayout必须将DrawText对象添加到显示列表中:
class InlineLayout: def paint(self, display_list): for x, y, word, font in self.display_list: display_list.append(DrawText(x, y,word, font))
也可以为背景添加DrawRect命令。让我们为pre标签(用于代码示例)添加一个灰色背景:
上面这段代码出现在添加DrawText对象的循环之前:背景必须绘制在源代码块内文本的下方,因此在文本之前。
完成显示列表后,创建execute方法,在该方法中调用create_text():
注意,execute将滚动量scroll作为一个参数;这样,每个图形命令都会自行进行相关的坐标转换。DrawRect对create_rectangle执行相同的操作:
class DrawRect: def execute(self, scroll, canvas): canvas.create_rectangle( self.left, self.top - scroll, self.right, self.bottom - scroll, width=0, fill=self.color, )
默认情况下,create_rectangle会绘制一个单像素的黑色边框,这是我们不想要的背景,所以请确保width=0。
总结
本文介绍了浏览器的布局引擎如何对网页进行布局,总结有以下几点:
布局现在是基于树的,并生成layout树
树中的每个节点有两种不同的布局模式
布局计算每个layout对象的大小和位置
显示列表包含通用命令
给layout对象应用背景
小程序名字修改的技巧规则
我们都知道名字的意义,名称作为陌生人最先的认知,在物质喧嚣的时代,如何从众多名称中脱颖而出给陌人生留下一个良好且深刻的印象,这至关重要。随着小程序开发越来越多,运营者在给小程序...
小程序商城怎么运营?
小程序商城在当今电商领域日益受到瞩目,成功运营这样一个平台对于每个经营者而言都至关重要。那么,我们该如何着手呢?一、确立品牌方向首先,我们要清晰地定义自己的品牌在市场中的位置。...
自建商城运营秘籍,吸引顾客有妙招!
新建网站的运营与维护之道一、明确核心产品的市场定位要让新建的商城网站在竞争激烈的市场中脱颖而出,关键在于精准地定位核心产品。选择具有市场潜力的热销产品,并突出其独特之处,是吸引...
小程序商城推广完全指南
随着小程序商城的日益兴起,如何在竞争激烈的市场中脱颖而出成为了关键。小程序商城的推广方式多种多样,以下是一些有效的策略:1.公众号与小程序的结合:商家可以将小程序与公众号绑定,...
推广引流方法有哪些,裂变营销什么意思
推广引流方法有哪些,裂变营销什么意思除了各公域平台,另一个比较重要的引流场景,就是在微信中。一方面做信社交性强,对于身边用友的链接更紧密,微信上也会以群、公众号的形式聚集一群有...
延伸阅读
本文来自投稿,不代表本人立场,如若转载,请注明出处:http://lnbdc.com/article/8595.html