Blazor中的无状态组件

声明:本文将RenderFragment称之为组件DOM树或者是组件DOM节点,将*.razor称之为组件。

1. 什么是无状态组件

如果了解React,那就应该清楚,React中存在着一种组件,它只接收属性,并进行渲染,没有自己的状态,也没有所谓的生命周期。写法大致如下:

var component = (props: IPerson)=>{return <div>{prop.name}: {prop.age}</div>;
}

无状态组件非常适用于仅做数据的展示的DOM树最底层——或者说是最下层——组件。

2. Blazor的无状态组件形式

Blazor也可以生命无状态组件,最常见的用法大概如下:

...@code {RenderFragment<Person> DisplayPerson = props => @<div class="person-info"><span class="author">@props.Name</span>: <span class="text">@props.Age</span></div>;
}

其实,RenderFragment就是Blazor在UI中真正需要渲染的组件DOM树。Blazor的渲染并不是直接渲染组件,而是渲染的组件编译生成的RenderFragment,执行渲染的入口,就是在renderHandle.Render(renderFragment)函数。而renderHandle则只是对renderer进行的一层封装,内部逻辑为:renderer.AddToRenderQueue(_componentId, renderFragment);_renderHandle内部私有的_renderer,对于WebAssembly来说,具体就是指WebAssemblyRenderer,它将会在webAssemblyHost.RunAsync()进行创建。

以上方式,固然能够声明一个Blazor的无状态组件,但是这种标签式的写法是有限制的,只能写在*.razor文件的@code代码块中。如果写在*.cs文件中就比较复杂,形式大概如下:

RenderFragment<Person> DisplayPerson = props => (__builder2) =>{__builder2.OpenElement(7, "div");__builder2.AddAttribute(8, "class", "person-info");__builder2.OpenElement(9, "span");__builder2.AddAttribute(10, "class", "author");__builder2.AddContent(11, props.Name);__builder2.CloseElement();__builder2.AddContent(12, ": ");__builder2.OpenElement(13, "span");__builder2.AddAttribute(14, "class", "text");__builder2.AddContent(15, props.Age);__builder2.CloseElement();__builder2.CloseElement();};

这段代码是.NET自动生成的,如果你使用.NET6,需要使用一下命令:

dotnet build /p:EmitCompilerGeneratedFiles=true

或者,在项目文件中加入一下配置:

<PropertyGroup><EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles></PropertyGroup>

然后就能在

"obj\Debug\net6.0\generated\Microsoft.NET.Sdk.Razor.SourceGenerators\Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator"文件夹下看到文件的生成(.NET5 应该是在 "obj/Debug/net6.0/RazorDeclaration")。

事实上,这和React是类似的,JSX也是ReactReact.createElement()的语法糖。但是,不管怎么样,语法糖就是香,而且能够直观看到HTML的DOM的大致样式(因为看不到组件的DOM)。那么,有没有一种更加优雅的方式,能够实现无状态组件,减少组件的生命周期的调用?答案是有的。

3. 面向接口编程的Blazor

当我们创建一个*.razor Blazor组件的时候,组件会默认继承抽象类ComponentBase,Blazor组件所谓的生命周期方法OnInitializedOnAfterRender等等,都是定义在这个抽象类中的。但是,Blazor在进行渲染的时候,组件的基类是ComponentBase并不是强制要求的,只需要实现IComponent接口即可。关于这一点,我并没有找到具体的源码在哪,只是从Blazor挂载的根节点的源码中看到的:

/// <summary>
/// Defines a mapping between a root <see cref="IComponent"/> and a DOM element selector.
/// </summary>
public readonly struct RootComponentMapping
{/// <summary>/// Creates a new instance of <see cref="RootComponentMapping"/> with the provided <paramref name="componentType"/>/// and <paramref name="selector"/>./// </summary>
+    /// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>/// <param name="selector">The DOM element selector or component registration id for the component.</param>public RootComponentMapping([DynamicallyAccessedMembers(Component)] Type componentType, string selector){if (componentType is null){throw new ArgumentNullException(nameof(componentType));}+        if (!typeof(IComponent).IsAssignableFrom(componentType)){throw new ArgumentException($"The type '{componentType.Name}' must implement {nameof(IComponent)} to be used as a root component.",nameof(componentType));}// ...}
}

那么,是不在只要Blazor的组件实现了IComponent接口即可?答案是:不是的。因为除了要实现IComponent接口,还有一个隐形的要求是需要有一个虚函数BuildRenderTree

protected virtual void BuildRenderTree(RenderTreeBuilder builder);

这是因为,Blazor在编译后文件中,会默认重写这个函数,并在该函数中创建一个具体DOM渲染节点RenderFragmentRenderFragment是一个委托,其声明如下:

public delegate void RenderFragment(RenderTreeBuilder builder)

BuildRenderTree的作用就相当于是给这个委托赋值。

4. 自定义StatelessComponentBase

既然只要组件类实现IComponent接口即可,那么我们可以实现一个StatelessComponentBase : IComponent,只要我们以后创建的组件继承这个基类,即可实现无状态组件。IComponent接口的声明非常简单,其大致作用见注释。

public interface IComponent
{/// <summary>/// 用于挂载RenderHandle,以便组件能够进行渲染/// </summary>/// <param name="renderHandle"></param>void Attach(RenderHandle renderHandle);/// <summary>/// 用于设置组件的参数(Parameter)/// </summary>/// <param name="parameters"></param>/// <returns></returns>Task SetParametersAsync(ParameterView parameters);
}

没有生命周期的无状态组件基类:

public class StatelessComponentBase : IComponent
{private RenderHandle _renderHandle;private RenderFragment renderFragment;public StatelessComponentBase(){// 设置组件DOM树(的创建方式)renderFragment = BuildRenderTree;}public void Attach(RenderHandle renderHandle){_renderHandle = renderHandle;}public Task SetParametersAsync(ParameterView parameters){// 绑定props参数到具体的组件(为[Parameter]设置值)parameters.SetParameterProperties(this);// 渲染组件_renderHandle.Render(renderFragment);return Task.CompletedTask;}protected virtual void BuildRenderTree(RenderTreeBuilder builder){}
}

StatelessComponentBaseSetParametersAsync中,通过parameters.SetParameterProperties(this);为子组件进行中的组件参数进行赋值(这是ParameterView类中自带的),然后即执行_renderHandle.Render(renderFragment),将组件的DOM内容渲染到HTML中。

继承自StatelessComponentBase的组件,没有生命周期、无法主动刷新、无法响应事件(需要继承IHandleEvent),并且在每次接收组件参数([Parameter])的时候都会更新UI,无论组件参数是否发生变化。无状态组件既然有这么多不足,我们为什么还需要使用它呢?主要原因是:没有生命周期的方法和状态,无状态组件在理论上应具有更好的性能。

5. 使用StatelessComponentBase

Blazor模板默认带了个Counter.razor组件,现在,我们将count展示的部分抽离为一个单独DisplayCount无状态组件,其形式如下:

@inherits StatelessComponentBase<h3>DisplayCount</h3>
<p role="status">Current count: @Count</p>@code {[Parameter]public int Count{ get; set; }
}

counter的形式如下:

@page "/counter"<PageTitle>Counter</PageTitle><h1>Counter</h1>+ <Stateless.Components.DisplayCount Count=@currentCount />
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>@code {private int currentCount = 0;private void IncrementCount(){currentCount++;}
}

6. 性能测试

StatelessComponentBase添加一个生命周期函数AfterRender,并在渲染后调用,则现在其结构如下(注意SetParametersAsync现在是个虚函数):

public class StatelessComponentBase : IComponent
{private RenderHandle _renderHandle;private RenderFragment renderFragment;public StatelessComponentBase(){// 设置组件DOM树(的创建方式)renderFragment = BuildRenderTree;}public void Attach(RenderHandle renderHandle){_renderHandle = renderHandle;}+    public virtual Task SetParametersAsync(ParameterView parameters){// 绑定props参数到具体的组件(为[Parameter]设置值)parameters.SetParameterProperties(this);// 渲染组件_renderHandle.Render(renderFragment);
+        AfterRender();return Task.CompletedTask;}protected virtual void BuildRenderTree(RenderTreeBuilder builder){}protected virtual void AfterRender(){}
}

修改无状态组件DisplayCount如下:

@inherits StatelessComponentBase<h3>DisplayCount</h3>
<p role="status">Current count: @Count</p>@code {[Parameter]public int Count{ get; set; }long start;public override Task SetParametersAsync(ParameterView parameters){start = DateTime.Now.Ticks;return base.SetParametersAsync(parameters);}protected override void AfterRender(){long end = DateTime.Now.Ticks;Console.WriteLine($"Stateless DisplayCount: {(end - start) / 1000}");base.AfterRender();}
}

创建有状态组件DisplayCountFull

<h3>DisplayCountFull</h3>
<p role="status">Current count: @Count</p>@code {[Parameter]public int Count { get; set; }long start;public override Task SetParametersAsync(ParameterView parameters){start = DateTime.Now.Ticks;return base.SetParametersAsync(parameters);}protected override void OnAfterRender(bool firstRender){long end = DateTime.Now.Ticks;Console.WriteLine($"DisplayCountFull: {(end - start) / 1000}");base.OnAfterRender(firstRender);}
}

两者的区别在于继承的父类、生命周期函数和输出的日志不同。

有趣的是,DisplayCountDisplayCountFull组件的位置的更换,在第一次渲染的时候,会得到两个完全不一样的结果,哪个在前,哪个的耗时更短,但是DisplayCount在前的时候,两者整体耗时之和是最小的。关于这点,我还没有找到原因是什么。但是无论那种情况,之后随着count的变化,DisplayCount的耗时是小于DisplayCountFull的。

ce6fc5bac7f0d530148d2f66c2fa57a6.png628ab4e66d9619a27b37836a8497801a.png

7. 总结

本文粗略的探究了Blazor的组件的本质——组件仅仅是对RenderFragment组件DOM树的包装和语法糖。通过声明RenderFragment变量,即可进行无状态的Blazor的组件渲染。此外,组件不需要继承ComponentBase类,只需要实现IComponent接口并具备一个protected virtual void BuildRenderTree(RenderTreeBuilder builder)抽象函数即可。

同时,本文提出了Blazor的无状态组件的实现方式没,相较于直接声明RenderFragment更加优雅。尽管无状态组件有很多缺点:

  1. 没有生命周期

  2. 无法主动刷新

  3. 无法响应事件(需要继承IHandleEvent),

  4. 每次接收组件参数([Parameter])的时候都会更新UI,无论组件参数是否发生变化。

但是通过对无状态组件的性能进行粗略测试,发现由于无状态组件没有生命周期的方法和状态,总体上具有更好的性能。此外,相较于重写生命周期的组件,更加直观。无状态组件更加适用于纯进行数据数据展示的组件。

以上仅为本人的拙见,如有错误,敬请谅解和纠正。https://github.com/zxyao145/BlazorTricks/tree/main/01-Stateless

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/293637.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一次性撤稿70篇!中国学者论文再现大规模撤稿 | 附全名单

全世界只有3.14 % 的人关注了爆炸吧知识2021年1月20日&#xff0c;英国皇家化学学会&#xff08;Royal Society of Chemistry&#xff0c;简称RSC&#xff09;表示&#xff0c;他们正在考虑撤回68篇可能出自“论文工厂”的文章&#xff0c;但当时并没有公布这68篇文章的详情。近…

二叉树前序、中序、后序遍历相互求法

今天来总结下二叉树前序、中序、后序遍历相互求法&#xff0c;即如果知道两个的遍历&#xff0c;如何求第三种遍历方法&#xff0c;比较笨的方法是画出来二叉树&#xff0c;然后根据各种遍历不同的特性来求&#xff0c;也可以编程求出&#xff0c;下面我们分别说明。 首先&…

搭建SVN服务

SVN版本管理工具管理着随时间改变的各种数据像普通文件服务器或者ftp服务器&#xff0c;但是&#xff0c;SVN会备份并记录每个文件每一次的修改记录Subversion官网&#xff1a;http://subversion.tigris.org/http://subversion.apache.org/svn客户端&#xff1a; http://tortoi…

MegaCli 监控Raid状态

MegaCli是一款管理维护硬件RAID软件&#xff0c;可以通过它来了解当前 raid卡的所有信息&#xff0c;包括 raid卡的型号&#xff0c;raid的阵列类型&#xff0c;raid 上各磁盘状态&#xff0c;等等。通常&#xff0c;我们对硬盘当前的状态不太好确定&#xff0c;一般通过机房人…

研究表明,胸大无脑是不存在的......

1 求车主心里阴影面积▼2 不管成不成功大叔是你坚实的后盾▼3 小岳岳的“全球分鹏”▼4 胸大无脑是不存在的&#xff01;▼美国芝加哥大学曾对1200名女性进行实验&#xff0c;胸部丰满女性在智商测试中的平均得分反而比胸部扁平的女性高出了10分。5 你也是个逻辑鬼才▼6 想…

每日一小练——按字典顺序列出全部子集

上得厅堂&#xff0c;下得厨房&#xff0c;写得代码&#xff0c;翻得围墙&#xff0c;欢迎来到睿不可挡的每日一小练&#xff01; 题目&#xff1a;按字典顺序列出全部子集 内容&#xff1a; 请写一个程序用字典顺序把一个{1,2,3,4,...,n}集合的全部子集找出来。 解答&#xff…

CSS 特殊性、继承与层叠

一、特殊性规则 选择器的特殊性由选择器本身的组件确定&#xff1b;特殊性由四个部分组成&#xff0c;其初始值为0&#xff0c;0&#xff0c;0&#xff0c;0。 1. 对于选择器中的每一个id&#xff0c;记0&#xff0c;1&#xff0c;0&#xff0c;0&#xff1b; 2. 对于选择…

解读WPF中的Xaml

1.Overview这篇文章主要分享从源代码角度解读wpf中xaml。由于源码查看起来错综复杂“随便找一个对象按下F12就是一个新的世界”&#xff0c;看源码的感觉就是在盗梦空间里来回穿梭&#xff1b;所以也是耗费很长的时间去阅读源码然后根据自己的理解编写文章和贴出部分关键源码。…

寒门博士分享读博经历成“抖音网红”惹争议,博士该这么“不正经”吗?

全世界只有3.14 % 的人关注了爆炸吧知识最近在抖音上&#xff0c;一个名叫“相宜”的主播火了。短短几个月时间内&#xff0c;她就涨粉940万。而和一般网红不同的是&#xff0c;相宜是一位刚毕业的博士。带火她的视频&#xff0c;是她自述博士毕业后的感想&#xff0c;目前已经…

Android Nine-patch

做了好多客户端软件了&#xff0c;突然发现里面有好多图片都是重复的&#xff0c;个别只是大小不一样&#xff0c;每次都使用大量图片&#xff0c;导致软件过大&#xff0c;项目总结的时候才发现Android已经提供了一种解决方案了&#xff0c;这就是NinePatchDrawable&#xff0…

稍微成型点的用WEBSOCKET实现的实时日志LOG输出

难的是还是就地用JS显示出来相关的发布进度。 还好&#xff0c;花了一下午实现了。 可以移植到项目中去罗。。。 websocket.py&#xff1a; import tornado.ioloop import tornado.web import tornado.websocket from tornado.ioloop import IOLoop from datetime import timed…

.NET6之MiniAPI(四):配置

配置文件&#xff0c;是一个每个应用服务程序常用的功能&#xff0c;从原来的终端应用时代&#xff0c;到现在的元宇宙时代&#xff0c;配置都是很悠然自得的存在。asp.net core提供了强大的配置文件访问机制&#xff0c;不管是MVC API还是MiniAPI&#xff0c;使用方式都是相同…

.NET 6新特性试用 | PeriodicTimer

前言在.NET中&#xff0c;已经存在了5个Timer类&#xff1a;System.Threading.TimerSystem.Timers.TimerSystem.Web.UI.TimerSystem.Windows.Forms.TimerSystem.Windows.Threading.DispatcherTimer不管以前这样设计的原因&#xff0c;现在.NET 6又为我们增加了一个新Timer&…

ChatForFun 公众号使用说明

使用方法 2016-07-16 DennisMi ChatForFun1&#xff0c;发送 #1 实现登陆&#xff0c;或者退出登陆 2&#xff0c;发送 #2 实现加入聊天&#xff0c;和退出聊天 3&#xff0c;聊天开始后&#xff0c;可以直接发送消息 4&#xff0c;如果需要退出登陆或者退出聊天&#xff0c;…

.NET 6新特性试用 | 总结:我最喜欢的5个特性

前言不知不觉&#xff0c;《.NET 6新特性试用》系列文章已经写了20多篇&#xff0c;而今天终于要告一段落了。如果你还没有看过&#xff0c;详细文章列表在这里&#xff1a;.NET 6新特性试用系列在这么多特性中&#xff0c;我最喜欢如下5个特性&#xff1a;1、最小Web API仅需三…

mikrotikROS系统的几种安装方法

这里简单介绍下几种ROS的安装方法,以及适用于哪些设备,这里我们先提供一个ROS6.0全系列版本的下载链接mikrotik-RouteOS-V6.0正式版下载 或者前往官方下载最新版常见的ROS硬件一般分为:X86架构(也是最常用的)mipsbe(欧米tik,部分RB系列,SXT,Groove等)mipsle(RBC系列,RB100,R…