Render trees 渲染树
当浏览器呈现内容时,它不仅绘制HTML中定义的元素,还必须根据页面大小(元素流)计算在哪里绘制这些元素。例如,下面的Bootstrap HTML将在调整浏览器窗口大小时将元素放置在不同的位置。
<div class="jumbotron text-center">
<h1>Responsive layout</h1>
</div>
<div class="container">
<div class="row">
<div class="col-sm-6 col-xs-12 btn btn-default">
Column 1
</div>
<div class="col-sm-6 col-xs-12 btn btn-default">
Column 2
</div>
</div>
</div>
每当HTML元素的属性更改(width
、height
、padding
、margin
等)时,浏览器必须在呈现元素之前重排页面上的元素。更新浏览器的文档对象模型(DOM)可能非常占用CPU,因此速度很慢,特别是在执行大量更新时。
The Virtual DOM 虚拟DOM
其他客户端工具,如React和Angular,通过同时实现虚拟DOM和增量DOM方法来绕过此问题。
虚拟DOM是组成HTML页面的元素的内存表示形式。该数据创建HTML元素树,就好像它们是由HTML标记页指定的一样。Blazor组件通过名为BuildRenderTree
的虚拟方法在其Razor视图中创建此虚拟DOM。例如,标准Pages/Index.razor页面的BuildRenderTree
如下所示。
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
builder.AddMarkupContent(0, "<h1>Hello, world!</h1>\r\n\r\nWelcome to your new app.\r\n\r\n");
builder.OpenComponent<MyFirstBlazorApp.Client.Shared.SurveyPrompt>(1);
builder.AddAttribute(2, "Title", "How is Blazor working for you?");
builder.CloseComponent();
}
构建表示要呈现的视图的数据树有两个显著的好处:
- 在复杂的更新过程中,这些虚拟HTML元素的属性值可以在代码中多次更新,而无需浏览器重新呈现和重排其视图,直到该过程完成。
可以通过比较两棵树并构建一棵新树来创建渲染树,这是两者之间的不同之处。这允许我们使用增量DOM方法。
The Incremental DOM
增量DOM是一种将更新浏览器视图中的元素所需的工作量降至最低的技术。
能够创建比较树使我们能够使用更新DOM所需的尽可能少的更改来表示对视图的更改。这节省了更改显示时的时间(因此用户体验更好),而且在服务器端Blazor应用程序中,这意味着网络上的字节数更少-使Blazor应用程序在速度较慢的网络或非常偏远的位置更易于使用。Example 1 – Adding a new list item
假设我们的用户正在使用一个显示项目列表的Blazor应用程序。他们点击一个按钮将一个新项目添加到列表中—该列表自动被赋予文本“3”。
Render 1:浏览器中视图的当前虚拟DOM由一个包含两个项目的列表组成。
Render 1:应用程序向列表中添加一个新项目。Blazor在一个新的虚拟DOM中表示了这一点。
Render 1:以下差异树被确定为所需更改次数最少的树。在本例中,一个新的- 和一个新的文本元素“3”。
Example 2 – Changing display text
用户看到列表“1”、“2”、“3”,并决定他们更喜欢看到数字。他们单击另一个按钮,该按钮将每个列表项的文本更改为其在列表中的索引。
Render 2:浏览器中视图的当前虚拟DOM由一个包含三个项目的列表组成。
Render 2:应用程序更改列表中所有项目的文本。同样,Blazor在一个新的虚拟DOM中表示了这一点。
Render 2:确定以下差异树是所需更改次数最少的。在这种情况下,现有文本元素只有两处更改。
然后使用差异呈现树来更新浏览器中的实际HTML DOM。Incremental RenderTree proof 证明增量渲染树
为了证明Blazor更新了现有的浏览器DOM元素,我们将创建一些JavaScript来获取Blazor生成的元素,并以Blazor不知道的方式更新它们。然后,我们将让Blazor更新其视图,并观察到JavaScript更改没有丢失。
创建默认应用程序,然后在/Pages/Index.razor中进行以下更改: 添加一个具有一些初始值的
List<string>
成员。- 添加一些标记以呈现该列表的值。
- 添加一个按钮,单击该按钮将调用C#方法来更新列表中的值。 ```csharp @page “/“
Hello, world!
Welcome to your new app.
-
@foreach(string item in Items)
{
- @item }
@code {
List
void ChangeData()
{
Items[0] = "1";
Items[1] = "2";
Items.Add("4");
}
}
<br />现在我们有了一些Blazor生成的元素,我们需要使用一些JavaScript来更改这些元素。编辑**/wwwroot/index.html**,在开始的`<body>`元素内添加一个按钮,在结束的`</body>`元素上方添加对jQuery的引用和一些脚本来更新现有的`<li>`元素。
```csharp
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>MyFirstBlazorApp</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<button id="setValues">Set values</button>
<app>Loading...</app>
<script src="_framework/blazor.webassembly.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<script>
$(function () {
$('#setValues').click(function () {
$('li').each(function () {
var $elem = $(this);
$elem.attr('originalValue', $elem.text());
});
});
});
</script>
</body>
</html>
- 第12行在HTML中添加一个按钮。
- 第17行引用jQuery。
- 第21行添加了一个脚本,该脚本查找所有
<li>
元素,获取它们的当前text
,然后将其分配给一个名为OriginalValue
的新属性。
运行应用程序,右键单击页面上的第一个
<ol>
<li>One</li>
<li>Two</li>
<li>3</li>
</ol>
接下来,单击页面顶部的 Set values 按钮,这将执行JavaScript向每个<li>
添加一个新属性,以记录它所帮助的原始文本。
现在,您应该会看到浏览器的元素检查器中的元素更改为以下内容:
<ol>
<li originalValue="One">One</li>
<li originalValue="Two">Two</li>
<li originalValue="3">3</li>
</ol>
最后,单击更改数据按钮。这将执行C#代码来更改列表<string>
中的值,然后Blazor将重新呈现其视图。现在检查元素应显示以下内容:
<ol>
<li originalValue="One">1</li>
<li originalValue="Two">2</li>
<li originalValue="3">3</li>
<li>4</li>
</ol>
- 文本为“One”的项目的文本更改为“1”。
- 文本为“2”的项目已将其文本更改为“2”。
- 案文为“3”的项目保持不变。
- 添加文本为“4”的新项目已添加。
我们可以看到现有的元素被重用了,因为元素的OriginalValue
属性不是由Blazor生成的,但是它们仍然存在。Blazor新创建的新元素没有originalValue
属性。
👍Optimising using @key 使用@Key进行优化
提示:对于运行时在循环中生成的组件,请始终使用@Key。
前面的示例运行良好,因为Blazor能够轻松地将虚拟DOM元素与Brower的DOM中的正确元素相匹配,当元素成功匹配时,使用更少的更改就可以更容易地更新它们。
然而,当元素重新排列时,这就变得更加困难。以用户身份证列表为例。
使用增量RenderTree Proof作为起点,编辑/Pages/Index.razor并输入以下标记。
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
<button @onclick=@ChangeData>Change data</button>
<style>
.card-img-top {
width: 150px;
height: 150px;
}
</style>
<ol>
@foreach (Person person in People)
{
<li class="card">
<img class="card-img-top" src="https://randomuser.me/api/portraits/men/@(person.ID).jpg" />
<div class="card-body">
<h5 class="card-title">
@person.GivenName @person.FamilyName
</h5>
<p class="card-text">
@person.GivenName @person.FamilyName has the id @person.ID
</p>
</div>
</li>
}
</ol>
@code {
List<Person> People = new List<Person>
{
new Person(1, "Peter", "Morris"),
new Person(2, "Bob", "Monkhouse"),
new Person(3, "Frank", "Sinatra"),
new Person(4, "David", "Banner")
};
void ChangeData()
{
var person = People[0];
People.RemoveAt(0);
People.Add(person);
}
class Person
{
public int ID { get; set; }
public string GivenName { get; set; }
public string FamilyName { get; set; }
public Person(int id, string givenName, string familyName)
{
ID = id;
GivenName = givenName;
FamilyName = familyName;
}
}
}
该页面本质上与我们以前显示简单整数列表时相同,但现在有以下更改。
- 第46行:定义了名为 Person 的新类。注意: 这个新类通常放在它自己的源文件中,但是为了简单起见,在这个例子中是按行排列的。
- 第32行:定义一个名为 People 的私有成员,并添加两个项以在视图中显示。
- 第40行:删除列表开头的人,并将他们添加到列表的结尾。
- 第15行:将人员列表显示为 Bootstrap 提示卡。
现在运行应用程序。单击Set Values按钮并检查列表中的元素,我们将看到如下所示:
<ol>
<li class="card" originalValue=" Peter Morris Peter Morris has the id 1">
...Html for Peter Morris
</li>
<li class="card" originalValue=" Bob Monkhouse Bob Monkhouse has the id 2">
...Html for Bob Monkhouse
</li>
<li class="card" originalValue=" Frank Sinatra Frank Sinatra has the id 3">
...Html for Frank Sinatra
</li>
<li class="card" originalValue=" David Banner David Banner has the id 4">
...Html for David Banner
</li>
</ol>
但是,当我们单击Change data按钮,然后再次检查元素时,我们会看到,尽管数据中的元素是相同的(只是重新排序),但HTML中的所有元素都已更新。这从OriginalValue表示以前由该元素持有的人这一事实可以明显看出,这意味着为了显示正确的HTML标记,必须更新许多子元素。
<ol>
<li class="card" originalValue=" Peter Morris Peter Morris has the id 1">
...Html for Bob Monkhouse
</li>
<li class="card" originalValue=" Bob Monkhouse Bob Monkhouse has the id 2">
...Html for Frank Sinatra
</li>
<li class="card" originalValue=" Frank Sinatra Frank Sinatra has the id 3">
...Html for David Banner
</li>
<li class="card" originalValue=" David Banner David Banner has the id 4">
...Html for Peter Morris
</li>
</ol>
这些更改的增量如下所示:
- Element 1
1.jpg => 2.jpg
Peter Morris => Bob Monkhouse - Element 2
2.jpg => 3.jpg
Bob Monkhouse => Frank Sinatra - Element 3
3.jpg => 4.jpg
Frank Sinatra => David Banner - Element 4
4.jpg => 1.jpg
David Banner => Peter Morris
总共有三个HTML元素针对每个人进行了更改。如果Blazor能检测到元素何时重新排列就更好了。这样,当重新排列数据时,从虚拟DOM到浏览器DOM的增量更改也将是一个简单的重新排列。
Identifying elements with @key
这正是@key
指令的作用所在。编辑第17行,更改<li class="card">
并添加密钥,如下所示:
<li class="card" @key=person>
- 运行应用程序。
- 单击设置值按钮。
- 单击更改数据按钮。
- 右键单击列表中的第一项并检查该元素。
现在,我们看到的不是包含Person 2内部HTML的Element 1,而是内部HTML完全保持不变,并且<li>
元素只是被重新排列。
<ol>
<li class="card" originalValue=" Bob Monkhouse Bob Monkhouse has the id 2">
...Html for Frank Sinatra
</li>
<li class="card" originalValue=" Frank Sinatra Frank Sinatra has the id 3">
...Html for David Banner
</li>
<li class="card" originalValue=" David Banner David Banner has the id 4">
...Html for Peter Morris
</li>
<li class="card" originalValue=" Peter Morris Peter Morris has the id 1">
...Html for Bob Monkhouse
</li>
</ol>
显然,当您希望重新排列数据或从列表末尾以外的任何位置添加/删除任何项时,在任何时候呈现列表中的项时使用@key
指令都是有利的。@key
使用的值可以是任何类型的对象。我们可以使用Person
实例本身,或者,如果列表中的实例发生更改,则可以使用类似Person.ID
的内容。