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中的无状态组件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我的2021年终总结:初为人父,从头再来
- 下一篇: 巧用ActionFilter的AOP特性