zoukankan      html  css  js  c++  java
  • 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的。

    7. 总结

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

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

    1. 没有生命周期

    2. 无法主动刷新

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

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

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

    以上仅为本人的拙见,如有错误,敬请谅解和纠正。

    代码:BlazorTricks/01-Stateless (github.com)

  • 相关阅读:
    [转]Xcode4.5.1破解iOS免证书开发真机调试与ipa发布
    [转]QT多线程异步调用
    [转]Clone Object as instance in OgreMax
    [转]Texture atlas extension to the RTSS
    [转]QT中线程调用GUI主线程控件的问题
    c语言打印菱形解析
    今天开始第一次win32汇编之旅 先搭建编程环境吧
    MSHFlexGrid控件
    用1602模拟电子钟功能
    Combobox控件使用
  • 原文地址:https://www.cnblogs.com/zxyao/p/15713349.html
Copyright © 2011-2022 走看看