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,无论组件参数是否发生变化。
但是通过对无状态组件的性能进行粗略测试,发现由于无状态组件没有生命周期的方法和状态,总体上具有更好的性能。此外,相较于重写生命周期的组件,更加直观。无状态组件更加适用于纯进行数据数据展示的组件。
以上仅为本人的拙见,如有错误,敬请谅解和纠正。https://github.com/zxyao145/BlazorTricks/tree/main/01-Stateless
Blazor中的无状态组件相关推荐
- class根据状态 vue_搞懂并学会运用 Vue 中的无状态组件
啥是应用程序状态,为什么咱们需要它? 状态管理通常在较小的项目并不需要,但是当涉及到更大的范围时,如企业级的应用大部分需要它了.简单的说,状态是一个包含应用程序使用的最新值的对象.但是,如果咱们从结构 ...
- 八十、React中的容器组件和无状态组件
2020/11/20. 周五.今天又是奋斗的一天. @Author:Runsen React,也有了自己去构建一些应用的信心,那会是一种非常棒的感觉. 容器组件和无状态组件 React类组件是在Jav ...
- [react] 描述下在react中无状态组件和有状态组件的区别是什么?
[react] 描述下在react中无状态组件和有状态组件的区别是什么? 1,无状态组件主要用来定义模板,接收来自父组件props传递过来的数据,使用{props.xxx}的表达式把props塞到模板 ...
- [react] 在react中无状态组件有什么运用场景
[react] 在react中无状态组件有什么运用场景 适用于逻辑简单的纯展示的场景,如资料卡片等 个人简介 我是歌谣,欢迎和大家一起交流前后端知识.放弃很容易, 但坚持一定很酷.欢迎大家一起讨论 主 ...
- react中的无状态函数式组件
无状态函数式组件,顾名思义,无状态,也就是你无法使用State,也无法使用组件的生命周期方法,这就决定了函数组件都是展示性组件,接收Props,渲染DOM,而不关注其他逻辑. 其实无状态函数式组件也是 ...
- 区分有状态和无状态组件
有状态组件和无状态组件是 React 中两种不同的组件类型,它们在处理数据和实现逻辑的方式上有所不同. 有状态组件(Stateful Components): 有状态组件是指具有内部状态(state) ...
- 有状态组件和无状态组件
所谓的有状态组件:指的是用类创建的组件. 所谓的无状态组件:指的是用函数创建的组件. 通过这个名称,可以发现函数组件与类组件的区别就是在状态上,那么状态指的就是数据. 所以说函数组件,没有自己的状态, ...
- PHP无状态对象,(PHP)基于Token的身份验证中对无状态的理解
假设我们设计的Token储存的信息为: 用户名.发行时间.过期时间.签名 在用户登录成功后,我们获取到用户的用户名.此时的时间戳,并将它们和我们设置的过期时间拼接在一起,组成一个字符串,假设为: $i ...
- 如何在 Kubernetes 中对无状态应用进行分批发布
在 Kubernetes 中针对各种工作负载,提供了多种控制器,其中 Deployment 为官方推荐,被用于管理无状态应用的 API 对象.本文将结合 Deployment 的特性,与常见的发布策略 ...
最新文章
- 2014年最值得关注的六大趋势
- java检索字符串并提取_eclipse 项目中搜索资源(类方法,文件名,文件中的字符串)...
- 一身漏洞狂奔24年!人人都用的WiFi被曝重大漏洞,随时成为监控你的工具
- 【pmcaff】 怎么招聘产品助理?如何成为称职的产品助理?
- h5页面长按保存图片
- 越过0到1的坎,卖好车开启1到10的路有难题?
- Datawhale-零基础入门NLP-新闻文本分类Task04
- 架构设计 | 缓存管理模式,监控和内存回收策略
- DNS 教育訓練教程
- 疫情风向标?苹果宣布将暂时关闭大中华区以外的所有苹果零售店!
- 程序员必备的10大健康装备!
- 关于UI三色配色的心得
- 水波纹 android,Android特效之水波纹的实现
- 批量替换Excel超级链接
- Maxima在线性代数的应用
- poj-1625 Censored![ac自动机+dp+高精度]
- 【NDN基础】Networking Named Content 问题汇总
- 宝塔 cpanel_cPanel / WHM许可更改
- 分析周杰伦超话爬虫思路
- SOCKET的 10035错误