Blazor中的无状态组件
聲明:本文將RenderFragment稱之為組件DOM樹或者是組件DOM節(jié)點(diǎn),將*.razor稱之為組件。
1. 什么是無狀態(tài)組件
如果了解React,那就應(yīng)該清楚,React中存在著一種組件,它只接收屬性,并進(jìn)行渲染,沒有自己的狀態(tài),也沒有所謂的生命周期。寫法大致如下:
var?component?=?(props:?IPerson)=>{return?<div>{prop.name}:?{prop.age}</div>; }無狀態(tài)組件非常適用于僅做數(shù)據(jù)的展示的DOM樹最底層——或者說是最下層——組件。
2. Blazor的無狀態(tài)組件形式
Blazor也可以生命無狀態(tài)組件,最常見的用法大概如下:
...@code?{RenderFragment<Person>?DisplayPerson?=?props?=>?@<div?class="person-info"><span?class="author">@props.Name</span>:?<span?class="text">@props.Age</span></div>; }其實(shí),RenderFragment就是Blazor在UI中真正需要渲染的組件DOM樹。Blazor的渲染并不是直接渲染組件,而是渲染的組件編譯生成的RenderFragment,執(zhí)行渲染的入口,就是在renderHandle.Render(renderFragment)函數(shù)。而renderHandle則只是對renderer進(jìn)行的一層封裝,內(nèi)部邏輯為:renderer.AddToRenderQueue(_componentId, renderFragment);。_renderHandle內(nèi)部私有的_renderer,對于WebAssembly來說,具體就是指WebAssemblyRenderer,它將會在webAssemblyHost.RunAsync()進(jìn)行創(chuàng)建。
以上方式,固然能夠聲明一個(gè)Blazor的無狀態(tài)組件,但是這種標(biāo)簽式的寫法是有限制的,只能寫在*.razor文件的@code代碼塊中。如果寫在*.cs文件中就比較復(fù)雜,形式大概如下:
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或者,在項(xiàng)目文件中加入一下配置:
<PropertyGroup><EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles></PropertyGroup>然后就能在
"obj\Debug\net6.0\generated\Microsoft.NET.Sdk.Razor.SourceGenerators\Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator"文件夾下看到文件的生成(.NET5 應(yīng)該是在 "obj/Debug/net6.0/RazorDeclaration")。
事實(shí)上,這和React是類似的,JSX也是ReactReact.createElement()的語法糖。但是,不管怎么樣,語法糖就是香,而且能夠直觀看到HTML的DOM的大致樣式(因?yàn)榭床坏浇M件的DOM)。那么,有沒有一種更加優(yōu)雅的方式,能夠?qū)崿F(xiàn)無狀態(tài)組件,減少組件的生命周期的調(diào)用?答案是有的。
3. 面向接口編程的Blazor
當(dāng)我們創(chuàng)建一個(gè)*.razor?Blazor組件的時(shí)候,組件會默認(rèn)繼承抽象類ComponentBase,Blazor組件所謂的生命周期方法OnInitialized、OnAfterRender等等,都是定義在這個(gè)抽象類中的。但是,Blazor在進(jìn)行渲染的時(shí)候,組件的基類是ComponentBase并不是強(qiáng)制要求的,只需要實(shí)現(xiàn)IComponent接口即可。關(guān)于這一點(diǎn),我并沒有找到具體的源碼在哪,只是從Blazor掛載的根節(jié)點(diǎn)的源碼中看到的:
///?<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的組件實(shí)現(xiàn)了IComponent接口即可?答案是:不是的。因?yàn)槌艘獙?shí)現(xiàn)IComponent接口,還有一個(gè)隱形的要求是需要有一個(gè)虛函數(shù)BuildRenderTree:
protected?virtual?void?BuildRenderTree(RenderTreeBuilder?builder);這是因?yàn)?#xff0c;Blazor在編譯后文件中,會默認(rèn)重寫這個(gè)函數(shù),并在該函數(shù)中創(chuàng)建一個(gè)具體DOM渲染節(jié)點(diǎn)RenderFragment。RenderFragment是一個(gè)委托,其聲明如下:
public?delegate?void?RenderFragment(RenderTreeBuilder?builder)BuildRenderTree的作用就相當(dāng)于是給這個(gè)委托賦值。
4. 自定義StatelessComponentBase
既然只要組件類實(shí)現(xiàn)IComponent接口即可,那么我們可以實(shí)現(xiàn)一個(gè)StatelessComponentBase : IComponent,只要我們以后創(chuàng)建的組件繼承這個(gè)基類,即可實(shí)現(xiàn)無狀態(tài)組件。IComponent接口的聲明非常簡單,其大致作用見注釋。
public?interface?IComponent {///?<summary>///?用于掛載RenderHandle,以便組件能夠進(jìn)行渲染///?</summary>///?<param?name="renderHandle"></param>void?Attach(RenderHandle?renderHandle);///?<summary>///?用于設(shè)置組件的參數(shù)(Parameter)///?</summary>///?<param?name="parameters"></param>///?<returns></returns>Task?SetParametersAsync(ParameterView?parameters); }沒有生命周期的無狀態(tài)組件基類:
public?class?StatelessComponentBase?:?IComponent {private?RenderHandle?_renderHandle;private?RenderFragment?renderFragment;public?StatelessComponentBase(){//?設(shè)置組件DOM樹(的創(chuàng)建方式)renderFragment?=?BuildRenderTree;}public?void?Attach(RenderHandle?renderHandle){_renderHandle?=?renderHandle;}public?Task?SetParametersAsync(ParameterView?parameters){//?綁定props參數(shù)到具體的組件(為[Parameter]設(shè)置值)parameters.SetParameterProperties(this);//?渲染組件_renderHandle.Render(renderFragment);return?Task.CompletedTask;}protected?virtual?void?BuildRenderTree(RenderTreeBuilder?builder){} }在StatelessComponentBase的SetParametersAsync中,通過parameters.SetParameterProperties(this);為子組件進(jìn)行中的組件參數(shù)進(jìn)行賦值(這是ParameterView類中自帶的),然后即執(zhí)行_renderHandle.Render(renderFragment),將組件的DOM內(nèi)容渲染到HTML中。
繼承自StatelessComponentBase的組件,沒有生命周期、無法主動刷新、無法響應(yīng)事件(需要繼承IHandleEvent),并且在每次接收組件參數(shù)([Parameter])的時(shí)候都會更新UI,無論組件參數(shù)是否發(fā)生變化。無狀態(tài)組件既然有這么多不足,我們?yōu)槭裁催€需要使用它呢?主要原因是:沒有生命周期的方法和狀態(tài),無狀態(tài)組件在理論上應(yīng)具有更好的性能。
5. 使用StatelessComponentBase
Blazor模板默認(rèn)帶了個(gè)Counter.razor組件,現(xiàn)在,我們將count展示的部分抽離為一個(gè)單獨(dú)DisplayCount無狀態(tài)組件,其形式如下:
@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添加一個(gè)生命周期函數(shù)AfterRender,并在渲染后調(diào)用,則現(xiàn)在其結(jié)構(gòu)如下(注意SetParametersAsync現(xiàn)在是個(gè)虛函數(shù)):
public?class?StatelessComponentBase?:?IComponent {private?RenderHandle?_renderHandle;private?RenderFragment?renderFragment;public?StatelessComponentBase(){//?設(shè)置組件DOM樹(的創(chuàng)建方式)renderFragment?=?BuildRenderTree;}public?void?Attach(RenderHandle?renderHandle){_renderHandle?=?renderHandle;}+????public?virtual?Task?SetParametersAsync(ParameterView?parameters){//?綁定props參數(shù)到具體的組件(為[Parameter]設(shè)置值)parameters.SetParameterProperties(this);//?渲染組件_renderHandle.Render(renderFragment); +????????AfterRender();return?Task.CompletedTask;}protected?virtual?void?BuildRenderTree(RenderTreeBuilder?builder){}protected?virtual?void?AfterRender(){} }修改無狀態(tài)組件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();} }創(chuàng)建有狀態(tài)組件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);} }兩者的區(qū)別在于繼承的父類、生命周期函數(shù)和輸出的日志不同。
有趣的是,DisplayCount和DisplayCountFull組件的位置的更換,在第一次渲染的時(shí)候,會得到兩個(gè)完全不一樣的結(jié)果,哪個(gè)在前,哪個(gè)的耗時(shí)更短,但是DisplayCount在前的時(shí)候,兩者整體耗時(shí)之和是最小的。關(guān)于這點(diǎn),我還沒有找到原因是什么。但是無論那種情況,之后隨著count的變化,DisplayCount的耗時(shí)是小于DisplayCountFull的。
7. 總結(jié)
本文粗略的探究了Blazor的組件的本質(zhì)——組件僅僅是對RenderFragment組件DOM樹的包裝和語法糖。通過聲明RenderFragment變量,即可進(jìn)行無狀態(tài)的Blazor的組件渲染。此外,組件不需要繼承ComponentBase類,只需要實(shí)現(xiàn)IComponent接口并具備一個(gè)protected virtual void BuildRenderTree(RenderTreeBuilder builder)抽象函數(shù)即可。
同時(shí),本文提出了Blazor的無狀態(tài)組件的實(shí)現(xiàn)方式?jīng)],相較于直接聲明RenderFragment更加優(yōu)雅。盡管無狀態(tài)組件有很多缺點(diǎn):
沒有生命周期
無法主動刷新
無法響應(yīng)事件(需要繼承IHandleEvent),
每次接收組件參數(shù)([Parameter])的時(shí)候都會更新UI,無論組件參數(shù)是否發(fā)生變化。
但是通過對無狀態(tài)組件的性能進(jìn)行粗略測試,發(fā)現(xiàn)由于無狀態(tài)組件沒有生命周期的方法和狀態(tài),總體上具有更好的性能。此外,相較于重寫生命周期的組件,更加直觀。無狀態(tài)組件更加適用于純進(jìn)行數(shù)據(jù)數(shù)據(jù)展示的組件。
以上僅為本人的拙見,如有錯(cuò)誤,敬請諒解和糾正。https://github.com/zxyao145/BlazorTricks/tree/main/01-Stateless
總結(jié)
以上是生活随笔為你收集整理的Blazor中的无状态组件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我的2021年终总结:初为人父,从头再来
- 下一篇: 巧用ActionFilter的AOP特性