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组件所谓的生命周期方法OnInitialized、OnAfterRender等等,都是定义在这个抽象类中的。但是,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渲染节点RenderFragment。RenderFragment是一个委托,其声明如下:
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)
{
}
}
在StatelessComponentBase的SetParametersAsync中,通过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);
}
}
两者的区别在于继承的父类、生命周期函数和输出的日志不同。
有趣的是,DisplayCount和DisplayCountFull组件的位置的更换,在第一次渲染的时候,会得到两个完全不一样的结果,哪个在前,哪个的耗时更短,但是DisplayCount在前的时候,两者整体耗时之和是最小的。关于这点,我还没有找到原因是什么。但是无论那种情况,之后随着count的变化,DisplayCount的耗时是小于DisplayCountFull的。


7. 总结
本文粗略的探究了Blazor的组件的本质——组件仅仅是对RenderFragment组件DOM树的包装和语法糖。通过声明RenderFragment变量,即可进行无状态的Blazor的组件渲染。此外,组件不需要继承ComponentBase类,只需要实现IComponent接口并具备一个protected virtual void BuildRenderTree(RenderTreeBuilder builder)抽象函数即可。
同时,本文提出了Blazor的无状态组件的实现方式没,相较于直接声明RenderFragment更加优雅。尽管无状态组件有很多缺点:
没有生命周期
无法主动刷新
无法响应事件(需要继承
IHandleEvent),每次接收组件参数([Parameter])的时候都会更新UI,无论组件参数是否发生变化。
但是通过对无状态组件的性能进行粗略测试,发现由于无状态组件没有生命周期的方法和状态,总体上具有更好的性能。此外,相较于重写生命周期的组件,更加直观。无状态组件更加适用于纯进行数据数据展示的组件。
以上仅为本人的拙见,如有错误,敬请谅解和纠正。
Blazor中的无状态组件的更多相关文章
- React 中的 Component、PureComponent、无状态组件 之间的比较
React 中的 Component.PureComponent.无状态组件之间的比较 table th:first-of-type { width: 150px; } 组件类型 说明 React.c ...
- React中的高阶组件,无状态组件,PureComponent
1. 高阶组件 React中的高阶组件是一个函数,不是一个组件. 函数的入参有一个React组件和一些参数,返回值是一个包装后的React组件.相当于将输入的React组件进行了一些增强.React的 ...
- react 中的无状态函数式组件
无状态函数式组件,顾名思义,无状态,也就是你无法使用State,也无法使用组件的生命周期方法,这就决定了函数组件都是展示性组件,接收Props,渲染DOM,而不关注其他逻辑. 其实无状态函数式组件也是 ...
- React系列文章:无状态组件生成真实DOM结点
在上一篇文章中,我们总结并模拟了JSX生成真实DOM结点的过程,今天接着来介绍一下无状态组件的生成过程. 先以下面一段简单的代码举例: const Greeting = function ({name ...
- Flutter入门之无状态组件
Flutter核心理念 flutter组件采用函数式响应框架构建,它的灵感来自于React.它设计的核心思想是组件外构建UI,简单解释一下就是组件鉴于它当前的配置和状态来描述它的视图应该是怎样的,当组 ...
- react的redux无状态组件
Provider功能主要为以下两点: 在原应用组件上包裹一层,使原来整个应用成为Provider的子组件 接收Redux的store作为props,通过context对象传递给子孙组件上的connec ...
- React: 无状态组件生成真实DOM结点
在上一篇文章中,我们总结并模拟了 JSX 生成真实 DOM 结点的过程,今天接着来介绍一下无状态组件的生成过程. 先以下面一段简单的代码举例: const Greeting = function ({ ...
- 37行代码构建无状态组件通信工具-让恼人的Vuex和Redux滚蛋吧!
状态管理的现状 很多前端开发者认为,Vuex和Redux是用来解决组件间状态通信问题的,所以大部分人仅仅是用于达到状态共享的目的.但是通常Redux是用于解决工程性问题的,用于分离业务与视图,让结构更 ...
- StatelessWidget 无状态组件 StatefulWidget 有状态组件 页面上绑定数据、改变页面数据
一.Flutter 中自定义有状态组件 在 Flutter 中自定义组件其实就是一个类,这个类需要继承 StatelessWidget/StatefulWidget. StatelessWidget ...
随机推荐
- SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节,继续使用 spock 测试我们自己封装的 WebClient 测试针对 r ...
- 小程序嵌套H5的方式和技巧(二)
文章接上文,小程序嵌套H5的方式和技巧(一) 四.刷新wev-view嵌套的H5页面 1)我们为什么要刷新wev-view嵌套的H5页面? 很多的业务场景都需要开发者每次打开页面都更新一下页面的数据. ...
- Pycharm整体缩进和减少缩进
整体缩进:鼠标拉选住代码块,按下tab键. 反向缩进:鼠标拉选住代码块,按下shift+tab键.
- 贪心/构造/DP 杂题选做Ⅱ
由于换了台电脑,而我的贪心 & 构造能力依然很拉跨,所以决定再开一个坑( 前传: 贪心/构造/DP 杂题选做 u1s1 我预感还有Ⅲ(欸,这不是我在多项式Ⅱ中说过的原话吗) 24. P5912 ...
- Codeforces 1458E - Nim Shortcuts(博弈论+BIT)
Codeforces 题目传送门 & 洛谷题目传送门 首先看到这样的题我们不妨从最特殊的情况入手,再逐渐推广到一般的情况.考虑如果没有特殊点的情况,我们将每个可能的局面看作一个点 \((a,b ...
- ceph简单了解
ceph简介 ceph是一个统一的分布式存储系统,设计初衷是提供较好的性能.可靠性和可扩展性. 目前已经得到众多云计算厂商的支持并被广泛应用.RedHat及OpenStack都可以与Ceph整合以支持 ...
- CRLF漏洞浅析
部分情况下,由于与客户端存在交互,会形成下面的情况 也就是重定向且Location字段可控 如果这个时候,可以向Location字段传点qqgg的东西 形成固定会话 但服务端应该不会存储,因为后端貌似 ...
- Gradle插件详解
参考[1]Gradle 插件 [2]修改 Gradle 插件(Plugins)的下载地址(repositories)
- Shell学习(三)——Shell条件控制和循环语句
参考博客: [1]Shell脚本的条件控制和循环语句 一.条件控制语句 1.if语句 1.1语法格式: if [ expression ] then Statement(s) to be execut ...
- 【Python】【Basic】【数据类型】运算符与深浅拷贝
运算符 1.算数运算: 2.比较运算: 3.赋值运算: 4.逻辑运算: 5.成员运算: 三元运算 三元运算(三目运算),是对简单的条件语句的缩写. # 书写格式 result = 值1 if 条件 ...