Render trees 渲染树

当浏览器呈现内容时,它不仅绘制HTML中定义的元素,还必须根据页面大小(元素流)计算在哪里绘制这些元素。例如,下面的Bootstrap HTML将在调整浏览器窗口大小时将元素放置在不同的位置。

  1. <div class="jumbotron text-center">
  2. <h1>Responsive layout</h1>
  3. </div>
  4. <div class="container">
  5. <div class="row">
  6. <div class="col-sm-6 col-xs-12 btn btn-default">
  7. Column 1
  8. </div>
  9. <div class="col-sm-6 col-xs-12 btn btn-default">
  10. Column 2
  11. </div>
  12. </div>
  13. </div>

浮图秀图片_blazor-university.com_20220109232525.gif
每当HTML元素的属性更改(widthheightpaddingmargin等)时,浏览器必须在呈现元素之前重排页面上的元素。更新浏览器的文档对象模型(DOM)可能非常占用CPU,因此速度很慢,特别是在执行大量更新时。

The Virtual DOM 虚拟DOM

其他客户端工具,如React和Angular,通过同时实现虚拟DOM和增量DOM方法来绕过此问题。
虚拟DOM是组成HTML页面的元素的内存表示形式。该数据创建HTML元素树,就好像它们是由HTML标记页指定的一样。Blazor组件通过名为BuildRenderTree的虚拟方法在其Razor视图中创建此虚拟DOM。例如,标准Pages/Index.razor页面的BuildRenderTree如下所示。

  1. protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
  2. {
  3. builder.AddMarkupContent(0, "<h1>Hello, world!</h1>\r\n\r\nWelcome to your new app.\r\n\r\n");
  4. builder.OpenComponent<MyFirstBlazorApp.Client.Shared.SurveyPrompt>(1);
  5. builder.AddAttribute(2, "Title", "How is Blazor working for you?");
  6. builder.CloseComponent();
  7. }

构建表示要呈现的视图的数据树有两个显著的好处:

  1. 在复杂的更新过程中,这些虚拟HTML元素的属性值可以在代码中多次更新,而无需浏览器重新呈现和重排其视图,直到该过程完成。
  2. 可以通过比较两棵树并构建一棵新树来创建渲染树,这是两者之间的不同之处。这允许我们使用增量DOM方法。

    The Incremental DOM

    增量DOM是一种将更新浏览器视图中的元素所需的工作量降至最低的技术。
    能够创建比较树使我们能够使用更新DOM所需的尽可能少的更改来表示对视图的更改。这节省了更改显示时的时间(因此用户体验更好),而且在服务器端Blazor应用程序中,这意味着网络上的字节数更少-使Blazor应用程序在速度较慢的网络或非常偏远的位置更易于使用。

    Example 1 – Adding a new list item

    假设我们的用户正在使用一个显示项目列表的Blazor应用程序。他们点击一个按钮将一个新项目添加到列表中—该列表自动被赋予文本“3”。
    Render 1:浏览器中视图的当前虚拟DOM由一个包含两个项目的列表组成。
    image.png
    Render 1:应用程序向列表中添加一个新项目。Blazor在一个新的虚拟DOM中表示了这一点。
    image.png
    Render 1:以下差异树被确定为所需更改次数最少的树。在本例中,一个新的

  3. 和一个新的文本元素“3”。
    image.png

    Example 2 – Changing display text

    用户看到列表“1”、“2”、“3”,并决定他们更喜欢看到数字。他们单击另一个按钮,该按钮将每个列表项的文本更改为其在列表中的索引。
    Render 2:浏览器中视图的当前虚拟DOM由一个包含三个项目的列表组成。
    image.png
    Render 2:应用程序更改列表中所有项目的文本。同样,Blazor在一个新的虚拟DOM中表示了这一点。
    image.png
    Render 2:确定以下差异树是所需更改次数最少的。在这种情况下,现有文本元素只有两处更改。
    image.png
    然后使用差异呈现树来更新浏览器中的实际HTML DOM。

    Incremental RenderTree proof 证明增量渲染树

    为了证明Blazor更新了现有的浏览器DOM元素,我们将创建一些JavaScript来获取Blazor生成的元素,并以Blazor不知道的方式更新它们。然后,我们将让Blazor更新其视图,并观察到JavaScript更改没有丢失。
    创建默认应用程序,然后在/Pages/Index.razor中进行以下更改:

  4. 添加一个具有一些初始值的List<string>成员。

  5. 添加一些标记以呈现该列表的值。
  6. 添加一个按钮,单击该按钮将调用C#方法来更新列表中的值。 ```csharp @page “/“

Hello, world!

Welcome to your new app.

    @foreach(string item in Items) {
  1. @item
  2. }

@code { List Items = new List { “One”, “Two”, “3” };

  1. void ChangeData()
  2. {
  3. Items[0] = "1";
  4. Items[1] = "2";
  5. Items.Add("4");
  6. }

}

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/103169/1641743483955-a4a6277e-f713-4d67-a90c-b2d67ac2fe5d.png#clientId=u1e4bf7c0-5331-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u7dd71de3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=392&originWidth=1024&originalType=url&ratio=1&rotation=0&showTitle=true&size=240276&status=done&style=none&taskId=u2fd81a4d-8b3c-41b9-b6d6-c90cc8fffab&title=%E6%98%BE%E7%A4%BA%E5%88%97%E8%A1%A8%E5%80%BC%E7%9A%84%E4%B8%BB%E9%A1%B5 "显示列表值的主页")<br />现在我们有了一些Blazor生成的元素,我们需要使用一些JavaScript来更改这些元素。编辑**/wwwroot/index.html**,在开始的`<body>`元素内添加一个按钮,在结束的`</body>`元素上方添加对jQuery的引用和一些脚本来更新现有的`<li>`元素。
  2. ```csharp
  3. <!DOCTYPE html>
  4. <html>
  5. <head>
  6. <meta charset="utf-8" />
  7. <meta name="viewport" content="width=device-width" />
  8. <title>MyFirstBlazorApp</title>
  9. <base href="/" />
  10. <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
  11. <link href="css/site.css" rel="stylesheet" />
  12. </head>
  13. <body>
  14. <button id="setValues">Set values</button>
  15. <app>Loading...</app>
  16. <script src="_framework/blazor.webassembly.js"></script>
  17. <script src="https://code.jquery.com/jquery-3.4.1.min.js"
  18. integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  19. crossorigin="anonymous"></script>
  20. <script>
  21. $(function () {
  22. $('#setValues').click(function () {
  23. $('li').each(function () {
  24. var $elem = $(this);
  25. $elem.attr('originalValue', $elem.text());
  26. });
  27. });
  28. });
  29. </script>
  30. </body>
  31. </html>
  • 第12行在HTML中添加一个按钮。
  • 第17行引用jQuery。
  • 第21行添加了一个脚本,该脚本查找所有<li>元素,获取它们的当前text,然后将其分配给一个名为OriginalValue的新属性。

运行应用程序,右键单击页面上的第一个

  • 元素并检查它。最初,元素将如下所示:

    1. <ol>
    2. <li>One</li>
    3. <li>Two</li>
    4. <li>3</li>
    5. </ol>

    接下来,单击页面顶部的 Set values 按钮,这将执行JavaScript向每个<li>添加一个新属性,以记录它所帮助的原始文本。
    image.png
    现在,您应该会看到浏览器的元素检查器中的元素更改为以下内容:

    1. <ol>
    2. <li originalValue="One">One</li>
    3. <li originalValue="Two">Two</li>
    4. <li originalValue="3">3</li>
    5. </ol>

    最后,单击更改数据按钮。这将执行C#代码来更改列表<string>中的值,然后Blazor将重新呈现其视图。现在检查元素应显示以下内容:

    1. <ol>
    2. <li originalValue="One">1</li>
    3. <li originalValue="Two">2</li>
    4. <li originalValue="3">3</li>
    5. <li>4</li>
    6. </ol>
    1. 文本为“One”的项目的文本更改为“1”。
    2. 文本为“2”的项目已将其文本更改为“2”。
    3. 案文为“3”的项目保持不变。
    4. 添加文本为“4”的新项目已添加。

    我们可以看到现有的元素被重用了,因为元素的OriginalValue属性不是由Blazor生成的,但是它们仍然存在。Blazor新创建的新元素没有originalValue属性。

    👍Optimising using @key 使用@Key进行优化

    提示:对于运行时在循环中生成的组件,请始终使用@Key。

    前面的示例运行良好,因为Blazor能够轻松地将虚拟DOM元素与Brower的DOM中的正确元素相匹配,当元素成功匹配时,使用更少的更改就可以更容易地更新它们。
    然而,当元素重新排列时,这就变得更加困难。以用户身份证列表为例。
    image.png
    使用增量RenderTree Proof作为起点,编辑/Pages/Index.razor并输入以下标记。

    1. @page "/"
    2. <h1>Hello, world!</h1>
    3. Welcome to your new app.
    4. <button @onclick=@ChangeData>Change data</button>
    5. <style>
    6. .card-img-top {
    7. width: 150px;
    8. height: 150px;
    9. }
    10. </style>
    11. <ol>
    12. @foreach (Person person in People)
    13. {
    14. <li class="card">
    15. <img class="card-img-top" src="https://randomuser.me/api/portraits/men/@(person.ID).jpg" />
    16. <div class="card-body">
    17. <h5 class="card-title">
    18. @person.GivenName @person.FamilyName
    19. </h5>
    20. <p class="card-text">
    21. @person.GivenName @person.FamilyName has the id @person.ID
    22. </p>
    23. </div>
    24. </li>
    25. }
    26. </ol>
    27. @code {
    28. List<Person> People = new List<Person>
    29. {
    30. new Person(1, "Peter", "Morris"),
    31. new Person(2, "Bob", "Monkhouse"),
    32. new Person(3, "Frank", "Sinatra"),
    33. new Person(4, "David", "Banner")
    34. };
    35. void ChangeData()
    36. {
    37. var person = People[0];
    38. People.RemoveAt(0);
    39. People.Add(person);
    40. }
    41. class Person
    42. {
    43. public int ID { get; set; }
    44. public string GivenName { get; set; }
    45. public string FamilyName { get; set; }
    46. public Person(int id, string givenName, string familyName)
    47. {
    48. ID = id;
    49. GivenName = givenName;
    50. FamilyName = familyName;
    51. }
    52. }
    53. }

    该页面本质上与我们以前显示简单整数列表时相同,但现在有以下更改。

    • 第46行:定义了名为 Person 的新类。注意: 这个新类通常放在它自己的源文件中,但是为了简单起见,在这个例子中是按行排列的。
    • 第32行:定义一个名为 People 的私有成员,并添加两个项以在视图中显示。
    • 第40行:删除列表开头的人,并将他们添加到列表的结尾。
    • 第15行:将人员列表显示为 Bootstrap 提示卡。

    现在运行应用程序。单击Set Values按钮并检查列表中的元素,我们将看到如下所示:

    1. <ol>
    2. <li class="card" originalValue=" Peter Morris Peter Morris has the id 1">
    3. ...Html for Peter Morris
    4. </li>
    5. <li class="card" originalValue=" Bob Monkhouse Bob Monkhouse has the id 2">
    6. ...Html for Bob Monkhouse
    7. </li>
    8. <li class="card" originalValue=" Frank Sinatra Frank Sinatra has the id 3">
    9. ...Html for Frank Sinatra
    10. </li>
    11. <li class="card" originalValue=" David Banner David Banner has the id 4">
    12. ...Html for David Banner
    13. </li>
    14. </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>
    
    1. 运行应用程序。
    2. 单击设置值按钮。
    3. 单击更改数据按钮。
    4. 右键单击列表中的第一项并检查该元素。

    现在,我们看到的不是包含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的内容。