你是否曾经好奇过浏览器是如何渲染网页的?本文将通过 30 张图将带你了解浏览器渲染进程的内部工作机制。
渲染进程负责处理标签页中的所有内容。
在渲染进程中,主线程处理大部分发送给用户的代码。如果使用 Web Worker 或 Service Worker,部分 JavaScript 会由工作线程处理。另外,合成器线程和光栅化线程也在渲染进程中运行,确保网页高效、流畅地渲染。
渲染进程的核心任务是将 HTML、CSS 和 JavaScript 转换为用户可交互的网页。
一、解析(Parse)
一旦浏览器收到第一个数据分块,它就可以开始解析收到的信息。“解析”是浏览器将通过网络接收的数据转换为 DOM 和 CSSOM 的步骤,通过渲染器在屏幕上将它们绘制成页面。
虽然 DOM 是浏览器标记的内部表示,但是它也被暴露出来,可以通过 JavaScript 中的各种 API 进行操作。
构建 DOM 树
第一步是处理 HTML 标记并构造 DOM 树。HTML 解析涉及到符号化和树的构造。HTML 标记包括开始和结束标记,以及属性名和值。如果文档格式良好,则解析它会简单而快速。解析器将标记化的输入解析到文档中,构建 DOM 树。
树结构类似于现实生活中的“树”,每个点称为节点,相连的节点称为父子节点。DOM 树构建过程示意图:
构建 DOM 树的输入内容是一个 HTML 文件,然后经过 HTML 解析器解析,最终输出树状结构的 DOM。 通过【开发者工具】 ⇒ 【控制台】,输入 document
回车后可查看完整的 DOM 树结构。
可以了解到,DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保持在内存中树状结构, 可以通过 JavaScript 来查询和修改内容。而要让 DOM 节点拥有正确的样式,需要样式计算。
子资源加载
当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析。当遇到一个 CSS 文件时,解析也可以继续进行,但是对于 <script>
标签(特别是没有 async
或者 defer
属性的)会阻塞渲染并停止 HTML 的解析。尽管浏览器的预加载扫描器加速了这个过程,但过多的脚本仍然是一个重要的瓶颈。
预加载扫描器
浏览器构建 DOM 树时,这个过程占用了主线程。同时,预加载扫描器会解析可用的内容并请求高优先级的资源,如 CSS、JavaScript 和 web 字体。多亏了预加载扫描器,我们不必等到解析器找到对外部资源的引用时才去请求。它将在后台检索资源,而当主 HTML 解析器解析到要请求的资源时,它们可能已经下载中了,或者已经被下载。预加载扫描器提供的优化减少了阻塞。
构建 CSSOM 树
HTML 加载 CSS 的三种方式:
解析CSS文件
由于浏览器也无法解析纯文本的 CSS 样式,所以当渲染引擎接收到CSS文本时, 会将 CSS 文本转换为浏览器可以理解的结构 StyleSheets,也就是 CSSOM ,并提供了查询和修改功能。
通过控制台 document.styleSheets
可查看结构:
构建 CSSOM 非常快,并且在当前的开发工具中没有以独特的颜色显示。相反,开发人员工具中的“重新计算样式”显示解析 CSS、构建 CSSOM 树和递归计算计算样式所需的总时间。在 web 性能优化方面,它是可轻易实现的,因为创建 CSSOM 的总时间通常小于一次 DNS 查询所需的时间。
提示浏览器你希望如何加载资源
JavaScript 可能会阻止解析
当HTML解析器遇到**<script>
标签时,会暂停文档解析,直到JavaScript代码加载、解析和执行完毕。这是因为JavaScript可以使用document.write()
**等方法改变DOM结构,因此必须在继续解析HTML文档前执行JavaScript。
如果你想知道在 JavaScript 执行过程中会发生什么,V8 团队会就此展开讨论和博文。
为了顺利加载资源,网络开发者可以通过多种方式向浏览器发送提示。以下是简要总结上述几种提示浏览器如何加载资源的最常见方法:
async
和defer
属性
async
- 异步加载:浏览器在解析HTML文档的同时并行加载脚本。
- 立即执行:一旦脚本加载完成,便立即执行,不会等待HTML文档的解析完成。
- 非顺序执行:多个带有**
async
**属性的脚本会根据它们加载完成的顺序执行,而不是它们在文档中的顺序。 优先外部脚本:更适合独立的、没有依赖其他脚本或DOM内容的脚本,例如第三方广告、数据收集或分析脚本。
<script src="script.js" async></script>
defer
:- 异步加载:浏览器在解析HTML文档的同时并行加载脚本。
- 延迟执行:脚本文件会在 HTML 文档完全解析和
DOMContentLoaded
事件执行之前执行。 - 顺序执行:多个带有**
defer
**属性的脚本会按它们在文档中的出现顺序执行。 DOM 依赖:适合需要在完整的DOM加载后执行的脚本,例如依赖于DOM元素的初始化代码。
<!---->
<script src="script.js" defer></script>
<link rel="preload">
提前加载资源,以提高页面加载性能。常用于关键资源,比如脚本、样式表、字体等。
<link rel="preload" href="style.css" as="style"> <link rel="preload" href="script.js" as="script">
- 预加载:浏览器在预先加载资源,但不执行。资源会被放在浏览器缓存中,等到需要的时候立即可用。
- 不影响执行时机:预加载的资源只是提前下载,并不改变它们原本的执行时机。需要在合适的地方使用相同资源的引用来执行或应用。
JavaScript 模块
使用
<script type="module">
,使JavaScript模块化,模块默认异步加载,不阻塞HTML解析。<script type="module" src="script.js"></script>
dns-prefetch
和preconnect
dns-prefetch
: 提前解析 DNS,提高后续请求速度。<link rel="dns-prefetch" href="//example.com">
preconnect
: 提前与目标服务器建立连接,减少请求延迟。<link rel="preconnect" href="<https://example.com>">
- 懒加载(Lazy Loading)
延迟加载图片和其他不可见的元素,减少初始加载时间。
<img src="image.jpg" loading="lazy" alt="example image">
通过这些常见的方法,开发者可以有效地优化资源加载,提高页面性能和用户体验。
二、样式计算
拥有 DOM 并不足以知道页面的显示效果,因为我们可以在 CSS 中设置页面元素的样式。主线程解析 CSS 并确定每个 DOM 节点的计算样式。这是关于根据 CSS 选择器将哪种样式应用于每个元素的信息。您可以在开发者工具的 computed
部分查看此信息。
CSS 继承与层叠
浏览器会根据CSS规则计算每个元素的样式属性值,包括继承自父元素的属性值和层叠样式表(如用户代理样式表、作者样式表和用户样式表)的属性值。
CSS 继承
CSS继承就是每个DOM节点都包含了父节点的样式。例如:
body { font-size: 20px }
p {color:blue;}
span {display: none}
div {font-weight: bold;color:red}
div p {color:green;}
可了解到,例如 body 节点的font-size
的属性,body 节点下的所以子节点都继承了。
另可通过“开发者工具”->Element,查看“style”标签:
通过分析,我们可以选择对应第一区域对应的元素,可查询改元素的样式(对应区域 2 中); 并且可通过区域 3 可查看对应样式的来源心情,其中,UserAgent样式,是浏览器提供的一组默认样式,如果不修改任何样式,默认使用的是UserAgent样式。
CSS 层叠
层叠是 CSS 的一个基本特征,它定义了如何合并来自多个源的属性值的算法。 CSS的全称“层叠样式表”即强调了这点。
可通过“开发者工具” ⇒ Element 标签,“Computed
”查看最后的计算样式:
样式计算阶段的目的就是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中遵守 CSS 的继承和层叠两个规则。这个阶段最终输出的内容是每个 DOM 节点的样式, 并被保存在 Computd Style
的结构内。
标准化样式表中的属性值
CSS文本中存在很多属性值,例如1em、blue、bold等属性值不容易被渲染引擎识别,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个 过程就是属性值标准化。
标准化属性值:
CSS 文本中的很多属性值,如2em
、blue
、bold
不容易渲染引擎理解,因此需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
我们看到,经过属性值标准化后,2em
被解析成32px
,blue
被解析成rgb(0, 0, 255)
,bold
被解析成700
。
构建渲染树(Render Tree)
浏览器会根据DOM树和计算好的样式属性值构建渲染树(也称为布局树或框模型树)。渲染树是DOM树的一个副本,但它只包含可见元素,并且每个元素都有一个对应的盒模型(包括宽度、高度、边距、边框等)。
三、布局(回流)
现在,渲染程序进程知道文档的结构以及每个节点的样式,但这不足以渲染页面。假设你正尝试通过电话向朋友描述一幅画。“有一个大的红色圆圈和一个小的蓝色方块”是不够的信息,不足以让你的朋友知道这幅画究竟是什么样子。
布局是在渲染树上运行布局以计算每个节点的几何体。
Chrome 在布局阶段需完成两个任务:布局 和 重排。
- 布局 是首次确定渲染树中所有节点的尺寸和位置,以及确定页面上每个对象的大小和位置的过程。
- 重排 是后续过程中对页面的任意部分或整个文档的大小和位置的重新计算。
布局 Layout(创建布局树)
图:主线程遍历经过计算的样式并生成布局树的 DOM 树
其中 DOM 树中还包含很多不可见的元素,例如 head
标签,display: none
属性的元素等。所以在显示之前,需要额外构建一颗只包含可见元素的布局树。
为了构建布局树,浏览器需要:
- 遍历 DOM 树:浏览器从 DOM 树的根节点开始,遍历整个树,找到所有可见元素。
- 过滤不可见元素:浏览器过滤掉不可见元素,如
display: none
的元素、visibility: hidden
的元素等。 - 创建布局树节点:浏览器为每个可见元素创建一个布局树节点,这个节点包含了元素的基本信息,如元素的 ID、类名、样式等。
- 建立父子关系:浏览器建立布局树节点之间的父子关系,确保每个元素的布局信息与其父元素和子元素的布局信息保持一致。
在创建完整的布局树后,需要计算布局树中节点的准确位置。
布局计算(重排 Reflow)
布局计算是确定元素几何形状的过程。
HTML采用流式布局模型,其基本原则是按照元素在文档流中的顺序,自左向右、自上而下排列。此外,还有一些特殊布局方式,如通过**position
** 属性进行定位布局和通过 float
实现的浮动布局。
主线程会遍历DOM树和计算出的样式,创建布局树,其中包含元素的坐标(x 和 y)及边界框大小等信息。虽然布局树的结构可能与DOM树类似,但它仅包含与页面上可见内容相关的元素。如果某个元素应用了**display: none
,它将不会出现在布局树中(不过, visibility: hidden
的元素仍会存在于布局树中)。同样,伪元素(例如p::before { content: "Hi!" }
**)会包含在布局树中,即使它们不在DOM中。
图:因换行变化而移动段落的 Box 布局
图:因换行变化而移动段落的 Box 布局
布局是一项复杂的任务,即使是最简单的页面布局(如从上到下的块级流)也需要考虑字体大小和换行位置,因为这些会影响段落的大小和形状,并进而影响下一个段落的位置。
CSS 可以使元素悬浮、裁剪溢出内容以及更改书写方向,因此布局阶段非常繁重。
CSS 可以使元素悬浮、裁剪溢出内容以及更改书写方向,因此布局阶段非常繁重。
阶段小结
我们了解到了渲染流程的前三个阶段为:DOM 生成、样式计算和布局:
- 浏览器不能直接解析 HTML 数据,所以第一步需要将其转换为 DOM 树结构;
- 生成 DOM 树后,接着根据 CSS 样式表,计算 DOM 树所以节点的样式;
- 最后根据计算 DOM 元素的布局信息,保存在布局树中。
布局树和渲染树的关系
布局树(Layout Tree)和渲染树(Render Tree)是浏览器渲染页面的两个重要数据结构,它们之间有着密切的关系。
以下是布局树和渲染树的关系图:
DOM树
|
|-- 布局树(Layout Tree)
| |
| |-- 元素的布局信息(位置、大小等)
|
|-- 渲染树(Render Tree)
|
|-- 元素的渲染信息(样式、颜色、字体等)
|-- 渲染器(Renderer)
四、绘制(重绘)
图:一个人坐在画布前,手里拿着画笔,不确定应该先画圆圈,还是先画方形
拥有 DOM、样式和布局仍然不足以渲染页面。假设你正尝试复制一幅画。你已经知道元素的大小、形状和位置,但仍需判断它们的绘制顺序。
例如,系统可能会为某些元素设置 z-index
,在这种情况下,按 HTML 中编写的元素的顺序进行绘制会导致呈现错误。
图:页面元素按 HTML 标记顺序显示,由于未考虑 Z-index 值,导致呈现的图片有误
在此绘制步骤中,主线程会遍历布局树来创建绘制记录。绘制记录是绘制过程的备注,例如“先提供背景,然后是文本,最后是矩形”。如果你使用 JavaScript 在 <canvas>
元素上绘制了内容,那么你可能已经熟悉此过程。
图:主线程遍历布局树并生成绘制记录
创建绘制记录
在这个阶段,浏览器通过遍历布局树来生成一个具有绘制顺序的绘制记录。这一过程类似于在绘画过程中记录每个绘制动作的顺序,确保图像按正确的顺序显示。
绘制记录的组成:
- 背景: 首先绘制元素的背景。
- 内容: 然后绘制内容,例如文本、图像等。
- 边框: 最后绘制边框或阴影。
这些步骤确保了视觉效果的层次正确。浏览器生成的绘制记录包含绘制每个元素及其内容的详细指令。
[
{type: "background", rect: {...}, color: "#fff"},
{type: "text", position: {...}, text: "Hello World"},
{type: "border", rect: {...}, style: "solid"}
]
构建分层树(Layer Tree)
为了确定哪些元素需要位于哪些层,主线程会遍历布局树来创建分层树。浏览器的页面实际上被分层了很多图层,这些图层叠加后合成最终的页面。
图:遍历布局树生成层树的主线程
布局树中的每个元素不一定都对应一个分层。层是为了优化重绘性能和处理复杂的层叠上下文而引入的。
图层树允许浏览器将页面内容分解为更小、可管理的单位,以提高渲染性能和减少重绘的开销。
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
提升为单独图层的条件:
- 层叠上下文属性: 具有层叠上下文属性的元素(例如
position: fixed
,z-index
属性等)会被提升为单独的图层。
剪裁内容: 需要裁剪内容的元素(例如带有滚动条的元素
overflow: auto
或scroll
)也会被提升为单独的图层。例如所示,文字所显示的区域超过了200*200的显示范围时就产生了剪裁,渲染引擎会把剪裁文字内容 的一部分用于显示在div区域。出现这种剪裁情况下,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会 被提升为单独的层。
<style> div { width: 200; height: 200; overflow:auto; background: gray; } </style> <body> <div > <p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p> <p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p> <p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> </div> </body>
被裁剪的内容所在单独图层的示意图
生成绘制列表
在基于布局树完成分层树的构建后,渲染引擎会对图层树中的每个图层进行绘制,渲染引擎会把每个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
drawBackground({...}, "#fff");
drawText({...}, "Hello World");
drawBorder({...}, "solid");
绘制列表:
绘制列表中的指令其实就是让其执行一个简单的绘制操作,而每个原生的背景、边框等都需要单独 的指令绘制,通过几条绘制指令来实现绘制一个元素。
“开发者工具”->“Layers”可查看“document”层等的绘制列表过程:
更新渲染流水线的成本很高
在渲染流水线中,最重要的一点是,在每个步骤中,系统都会使用前一操作的结果创建新数据。例如,如果布局树中有一些变化,则需要为文档中受影响的部分重新生成绘制顺序。
如果你要为元素添加动画效果,浏览器必须在每一帧之间执行这些操作。我们的大多数显示屏每秒都会刷新屏幕 60 次 (60 fps);当你在每一帧屏幕上在屏幕上移动内容时,动画将对用户来说非常流畅。但是,如果动画缺少其中帧,则页面将显得“卡顿”。
图:时间轴上的动画帧
即使你的渲染操作能够与屏幕刷新保持同步,这些计算也会在主线程上运行,这意味着当应用运行 JavaScript 时,这些计算可能会被阻塞。
图:时间轴上的动画帧,但有一帧被 JavaScript 屏蔽
你可以将 JavaScript 操作分成几小块,并使用 requestAnimationFrame()
安排在每一帧运行。如需详细了解此主题,请参阅优化 JavaScript 执行。你还可以在 Web 工作器中运行 JavaScript,以避免阻塞主线程。
图:在具有动画帧的时间轴上运行的小型 JavaScript 块
五、合成
如何绘制页面?
现在,浏览器已经了解了文档的结构、每个元素的样式、页面的几何图形以及绘制顺序,接下来就要将这些信息转换为屏幕上的像素,这个过程称为光栅化(Rasterization)
。
在早期版本的 Chrome 中,处理这个问题的一种简单方法是直接光栅化视口内的部分。如果用户滚动页面,则移动光栅框架,并通过光栅化更多内容来填补缺失的部分。然而,现代浏览器通过运行一个名为 合成
的更复杂过程来优化这一操作。
什么是合成?
想象你在制作一本图画书,每一页上都有几个层次的图画,比如:背景、树木、人物和天空。你先分别绘制每一层,然后把它们叠加在一起,形成一个完整的场景。这就是浏览器在合成阶段所做的事情。
浏览器中的合成可将网页的各个部分分离成**图层(Tiling)
,分别将它们光栅化,然后在单独的线程合成器线程(Compositor Thread)
**中合成为网页。如果发生滚动,由于图层已光栅化,因此只需合成新帧即可。同样,可以通过移动层和合成新帧来实现动画效果。
主线程外的光栅和合成
绘制列表只是用来记录绘制顺序和绘制指令的列表,实际上绘制操作是由渲染进程中的合成线程和光栅化线程来完成的。主线程会先将这些信息提交到合成器线程。
当图层的绘制列表准备完成后,主线程会把该绘制列表提交给合成线程。
图块处理(Tiling) :
通常,网页页面非常大,而用户只能够看到其中的一部分,称为**视口(ViewPort)
**。
图层的大小可能覆盖整个页面,绘制所有图层内容的开销很大。因此,合成线程会将图层分割成多个小图块(Tiling),并将每个图块发送到光栅化线程处理。这些图块的大小通常为256x256
或512x512
像素。
图:创建图块位图并发送到 GPU 的光栅线程
执行光栅化
合成线程会按照视口附近的图块来优先生成位图(Bitmap)
,实际生成位图的操作是由光栅化线程来执行的。 所谓光栅化,是指将图块转换为位图。而图块是光栅化执行的最小单位。渲染进程维护了一个光栅化线程池
,所以的图块光栅化都是在线程池内执行的。
合成线程提交图块给光栅化线程池
利用GPU加速
通常,光栅化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫**快速光栅化
**或者 GPU光栅化
,渲染进程会把生成图块的指令发送给 GPU 进程,然后在 GPU进程 将图块的矢量数据快速转换为位图,并将生成的位图存储在GPU内存中。
你可以在开发者工具中使用“Layers”面板查看网站是如何划分为多个图层的。
生成合成帧(Composite Frame)
图块光栅化完成后,光栅化线程池将已经处理好的位图图块返回给合成线程。
合成线程根据页面的层次结构和图块信息,创建绘制四边形(Draw Quads)
,这些四边形包含图块在内存中的位置及它们在页面上的渲染位置。这一步就像把一个大拼图分块拼好,再组合成一张完整的图片。
合成线程确保每个图块都放在正确的位置,将所有绘制四边形(Draw Quads)
组合成一个完整的合成帧。这一合成帧代表了当前需要呈现给用户的完整页面。
然后,合成线程会通过进程间通信(IPC)
,将合成帧发送到浏览器进程中。
图:创建合成帧的合成器线程。先将帧发送到浏览器进程,然后再发送到 GPU
显示(Display)
浏览器进程接收到合成帧后,浏览器进程中的UI线程可能会结合浏览器自身的界面元素(比如滚动条、浏览器扩展等)与接收到的合成帧进行进一步合成。
最终的合成帧通过 IPC
发送到GPU进程,GPU进程 负责将这个合成帧在显示设备上显示出来。
GPU进程遵循显示器的垂直同步(通常每秒60次,即60Hz)将合成帧逐帧呈现到屏幕上,确保页面渲染的流畅和稳定。
滚动和动画的优化
若用户滚动页面,合成线程只需要更新新的滚动部分,而不必重新计算整个页面内容,这就像更换拼图的一部分来适应视图改变。
类似地,对于动画效果,合成线程只需要不断更新动画部分,使得动画流畅进行,而不会影响其他内容。
小结
浏览器渲染过程中的合成阶段从进程和线程视角总结如下:
- 光栅化线程池:并行处理大量图块,利用GPU加速生成位图。
- 合成线程:收集和组合图块,生成合成帧。
- 进程间通信:合成帧通过IPC发送到浏览器进程。
- 浏览器进程:结合浏览器界面合成完整页面。
- GPU进程:最终将合成帧显示在屏幕上,确保流畅的用户体验。
这整个过程通过优化线程和进程的协同工作,极大地提升了页面渲染的性能,使得滚动和动画效果更加流畅。
合成器线程不需要等待样式计算或 JavaScript 执行。因此,仅合成动画被认为是实现流畅性能的最佳选择。如果需要再次计算布局或绘制,则必须涉及主线程。
六、总结
完整的渲染流水线:
浏览器渲染流程的真实顺序
解析HTML和构建DOM树:
- 浏览器从网络上获取HTML文件,开始解析这个文件的内容,逐步构建DOM树(Document Object Model Tree)。
解析CSS和构建CSSOM树:
- 同时,浏览器也开始解析与HTML相关联的CSS文件,构建一个CSSOM(CSS Object Model Tree)。
合成DOM和CSSOM为渲染树:
- 浏览器将DOM树与CSSOM树合成,生成渲染树(Render Tree),使每个DOM节点都有相应的样式信息。
布局(Layout/Reflow) :
- 渲染树生成后,浏览器计算每个节点的几何位置和尺寸,这一过程叫做布局或者回流(Reflow)。
绘制(Painting) :
- 绘制记录(Paint Record) :浏览器通过遍历布局树,生成绘制记录。这些记录包括了绘制元素的顺序和详细指令。
- 绘制顺序处理:确定绘制顺序,例如处理**
z-index
**等样式属性,确保视觉上的正确顺序。
构建分层树(Layer Tree) :
确定层(Layers) :浏览器确定哪些元素需要创建独立的图层。例如:
- 拥有层叠上下文属性的元素(例如**
position: fixed
**, **z-index
**等)。 - 需要剪裁的元素(例如带有滚动条的元素**
overflow: auto
或scroll
**)。
- 拥有层叠上下文属性的元素(例如**
生成绘制列表(Paint List) :
- 合成后的每个图层会有自己的独立绘制列表。
- 绘制指令:包含绘制指令(例如绘制背景、内容、边框等),按照正确的顺序排列。
绘制到位图(Rasterization) :
- 生成图块(Tile) :为了提高性能,大的图层可能会被拆分成更小的图块。
- 光栅化(Rasterization) :将绘制指令转换为位图,准备交给GPU处理。这些位图被称为纹理(Textures)。
合成和渲染(Compositing and Rendering) :
- 合成器线程(Compositor Thread) :独立于主线程的合成器线程负责将不同图层的纹理合成到一起。
- GPU加速:使用GPU加速将这些纹理合成并渲染到最终的帧。
- 显示内容(Display Content) :合成后的图像最终呈现到用户的屏幕上。
参考文献
深入了解现代网络浏览器(第 3 部分) | Blog | Chrome for Developers
(宏观视角下的浏览器)渲染流程:HTML、CSS和JavaScript是如何变成页面的? | Web全栈技术笔记
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。