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的内容。
