Blazor 的基本原理探索

news2025/2/27 5:26:09

背景

  • 为了提升开发效率,关键是对js不够熟悉,所以要使用C#进行全栈的开发,使用了mudblazor和radzen blazor,以及可能会用到其他的blazor组件,所有很有必要对blazor有个比较全面的不求甚解,其基本原理以及blazor组件的背后的逻辑是什么做个探究。使用范围的原因,只限于部分Blazor Server的内容。

Blazor 服务端

渲染过程

    浏览器 -->> 服务器: 建立 WebSocket 连接
    服务器 -->> 浏览器: 发送首页 HTML 代码
    loop 连接未断开
        Note left of 浏览器: 浏览器JS捕获用户输入事件
        浏览器 -->> 服务器: 通知服务器发生了该事件
        Note right of 服务器: 服务器 .Net 处理事件
        服务器-->>浏览器: 发送有变动的 HTML 代码
        Note left of 浏览器: 浏览器JS渲染变动的 HTML 代码
    end

备注:1. WebSocket 连接采用 SignalR 来建立,如果浏览器不支持 WebSocket,SignalR 会采用其他技术建立。(SignalR是重点,可自行搜索一下)

  1. 浏览器捕获用户输入是使用 Javascript进行捕获的
  2. 服务器处理客户端事件完成后,会生成新的 HTML 结构,然后将这个结构与老的结构进行对比,得到有变动的 HTML 代码
  3. Blazor 服务端渲染版采用在服务器端维护一个虚拟 DOM 树来实现上述操作
  4. “通知服务器发生了该事件”这一步里,从原理上来说类似于 WebForm 的 PostBack 机制,不同点在于,Blazor 只告诉服务器是哪个 DOM 节点发生了什么事件,这个传输量是极小的。

Blazor 路由渲染过程

当我们通过 NavigationManager 去改变路由地址时,大概流程如下

st=>start: 服务器启动
rt=>operation: 初始化 Router 组件,Router 内部注册 LocationChanged 事件
op1=>operation: LocationChanged 事件中根据路由查找对应的组件,默认触发首页组件
queue=>operation: 加入渲染队列
render=>operation: 一直进行渲染及比对,直到队列中所有的组件全部渲染完
diff=>operation: 将比对的差异结果更新至浏览器
e=>end: 等待下一次路由改变,继续触发 LocationChanged 事件
st->rt->op1->queue->render->diff->e

Blazor组件

组件是用户界面(UI)的自包含部分,具有支持动态行为的处理逻辑。 组件可以在项目之间嵌套、重用、共享,并在MVC和Razor Pages应用中使用。
在Razor组件文件中,组件是使用c#和HTML标记的组合来实现的,扩展名为. Razor。

从编程的角度来看,组件只是一个实现了IComponent接口的类。 仅此而已。 当它被附加到RenderTree (Renderer用来构建和更新的组件树)上时,它就有了生命。 UI IComponent接口是“Renderer”用来与组件通信和接收组件通信的接口

Renderer和Render Tree

  • Renderer和Render Tree位于WASM中的客户端应用程序或者Blazor Server中的SignalR Hub会话中,也就是说,每个连接的客户端应用程序都有一个渲染器。
  • UI——在DOM[文档对象模型]中由HTML代码定义的——在应用程序中被表示为一个RenderTree,并由Renderer管理。 把RenderTree想象成一个树,每个分支都有一个或多个组件。 每个组件都是一个c#类,实现了IComponent接口。 Renderer有一个RenderQueue,它运行代码来更新UI。 组件提交RenderFragments给Renderer运行以更新RenderTree和UI。 Renderer使用一个不同的进程(原文是diffing process)来检测由RenderTree更新引起的DOM中的变化,并将这些变化传递给客户端代码,在浏览器DOM中实现并更新显示的页面。
  • 所有的组件都是普通的实现了IComponent接口的类。IComponent接口的定义如下:

public interface IComponent
{
    void Attach(RenderHandle renderHandle);
    Task SetParametersAsync(ParameterView parameters);
}

我看到这个的第一反应是“什么? 漏掉了一些东西。 那些事件和初始化方法在哪里?” 你读过的每一篇文章都在谈论组件和OnInitialized,… 别让他们把你弄糊涂了。 这些都是ComponentBase的一部分,即IComponent的开箱即用的Blazor实现。 ComponentBase没有定义组件。 你将在下面看到一个简单得多的实现。

Blazor Hub Session有一个Renderer,它为每个根组件运行RenderTree。 从技术上讲,你可以有多个(根组件),但我们将在本文中忽略这一点。 我们刨析一下上面这个接口定义中的一些细节:

Rederer提供了如下机制:

  1. 用于呈现IComponent实例的层次结构
  2. 为它们分发事件
  3. 当用户界面发生变更时进行通知

RenderHandle结构:允许组件与其渲染器(Renderer)交互。

再回到IComponent这个接口上来:

  1. Attach在Renderer将一个IComponent对象附加到RenderTree时被调用。 它传递给组件一个RenderHandle结构体。 组件使用这个RenderHandle来将RenderFragments放入Renderer的RenderQueue中。 我们很快会更详细地了解RenderFragment。
  2. SetParametersAsync被Renderer调用,调用的时机是当它第一次将组件附加到RenderTree,并且当它认为一个或多个组件的参数发生了改变。

注意,IComponent没有RenderTree的概念。 它通过调用SetParametersAsync来触发,并通过调用RenderHandle上的方法来传递更改。

下图是Blazor模板的渲染树(Render Tree)的可视化表示

组件渲染过程

● OnInitialized、OnInitializedAsync:仅在第一次实例化组件时,才会调用这些方法一次。注意,该方法调用时参数已经设置,但没有渲染。
● SetParametersAsync:该方法可以让您在设置参数之前做一些事
● OnParametersSetAsync、OnParametersSet:每一次参数设置完成之后都会调用
● OnAfterRender、OnAfterRenderAsync:在组件渲染完成之后触发
● ShouldRender:如果该方法返回 false,则组件在第一次渲染完成后不会执行二次渲染
● StateHasChanged:强制渲染当前组件,如果 ShouldRender 返回的是 false,则不会强制渲染
● BuildRenderTree: 该方法一般情况下我们用不到,它的作用是拼接 HTML 代码,由 VS 自动生成的代码去调用它

另有一个关键的结构体 EventCallBack,还有一个关键的委托RenderFragment,它俩非常重要,前者可能见得比较少,后者基本上都知道。

st=>start: 开始渲染
isfirst=>condition: 是否首次渲染
init=>operation: 调用 OnInitialized 方法
initAsync=>operation: 调用 OnInitializedAsync 方法
onSetParameter=>operation: 调用 OnParametersSet 方法
setParameter=>operation: 调用 SetParametersAsync 方法
stateHasChanged=>operation: 调用 StateHasChanged 方法
st->setParameter->isfirst->init->initAsync->onSetParameter
onSetParameter->stateHasChanged
isfirst(yes)->init
isfirst(no)->onSetParameter

需要注意的是这个流程中没有 OnAfterRender 方法的调用,这个将在下面讨论

StateHasChanged 方法
 

这个方法至关重要,就比如上图中最终只到了 StateHasChanged 方法,就没了下文,我们来看看这个方法里面有什么

st=>start: 开始
isfirst=>condition: 是否首次渲染
should=>condition: ShouldRender 为True?
queue=>operation: 进入渲染队列
render=>operation: 开始循环渲染队列的数据
after=>operation: 触发 OnAfterRender 方法
e=>end: 结束
st->isfirst
queue->render->after->e
isfirst(yes)->queue
isfirst(no)->should
should(yes)->queue
should(no)->e

至此,我们基本把一个组件的生命周期的那几个方法讨论完了,除了一些异步版本的,逻辑都差不多,没有写进来

渲染队列的工作:

st=>start: 开始渲染队列
queue=>condition: 队列还有组件?
read=>operation: 从队列获取组件
swap=>operation: 备份当前 DOM 树及清空
render=>operation: 调用组件的 RenderFragment 委托获取新的 DOM 树
diff=>operation: 与备份的树对比
append=>operation: 将对比结果存入列表
display=>operation: 将列表中的所有对比结果发送至浏览器
e=>end: 结束
st->queue
read->swap->render->diff->append->queue
queue(yes)->read
queue(no)->display->e

几点注意

  1. 渲染开始之前是将当前树赋值成了旧的树,然后再将当前树清空
  2. 组件的 RenderFragment 委托在大多数情况下就是组件的 ChildContent 属性的值,玩过的都知道几乎每个组件都有自己的 ChildContent。
  3. 同时 RenderFragment 也有可能是 ComponentBase类中的一个私有属性,详见下面的代码。当然也有可能是其他的,限于篇幅,不细说
  4. RenderFragment 委托输入的参数就是当前这颗树
  5. 如果您在组件中调用了子组件,并且这个子组件还有自己的内容,那么 VS 会生成调用这个组件的代码,并且为这个组件添加 ChildContent 属性,内容就是子组件自己的内容,详见代码

blazor 如何对比的呢?

st=>start: 开始对比
seq=>operation: 循环每帧
compare=>condition: 序列号是否一致?
isComponent=>condition: 该帧是否都为组件?
render=>operation: 渲染该组件
compareParameter=>condition: 两边组件的参数是否有变化?
skip=>operation: 跳过该帧
setParameter=>operation: 设置新组件的参数,进入该组件的生命周期流程
currentSkip=>operation: 机制过于复杂,不讨论
e=>end: 对比结束
endSeq=>operation: 结束循环
st->seq->compare
compare(yes)->isComponent
compare(no)->currentSkip
isComponent(yes)->render->compareParameter
isComponent(no)->currentSkip
compareParameter(yes)->setParameter->endSeq->e
compareParameter(no)->skip

Blazor的不足

浏览器产生任何事件都会发送到服务器端,想象一下你注册了一个 onmousemove 事件的话,还要不要活了?所以,大规模触发的事件尽量少注册,这里面的网络传输成本是很大的,而且也会给你的服务端造成很大的压力。

Blazor 应用变卡一般有以下几种情况,我们只讨论服务端应用的情况

  • 服务器端已经挂了,这种情况其实浏览器端会完全失去响应,除非你刷新
  • 你的代码有问题或你引用的库的代码有问题,导致进入死循环或循环次数非常多

出现了卡的情况,会非常头疼,但实际上大多数情况都是第二种中

结合所有流程图来看,Blazor 完成渲染才会发送至浏览器,那么完成渲染的标准就是渲染队列被清空,那如果一直无法清空呢?体现出来就是死循环,或者说发生了一次点击事件结果循环了十次,这明显不科学(你故意的例外),而渲染队列被加入新东西大多数情况下是因为调用了 StateHasChanged 并且 ShuoldRender 返回了 true,或者是因为使用了 EventCallBack,这些代码所在的地方你全都难以调试
因为这些代码不是你的代码,所以你的断点也没处打,目前的 Blazor 不会告诉你到底是哪个组件哪行代码引起的死循环。

大部分来自于网上,没有找到来处,特此声明。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1382827.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI-数学-高中-5.求函数解析式(4种方法)

原作者视频:函数】3函数解析式求法(易)_哔哩哔哩_bilibili 1.已知函数类型-待定系数法:先用待定系数法把一次或二次函数一般表达式写出来;再用“要变一起变”左右两边同时替换,计算出一般表达式的常数&…

(分享) 音乐软件Spotify-声破天8.9.4

​【应用名称】:Spotify-声破天 ​【适用平台】:#Android ​【软件标签】:#Spotify ​【应用版本】:8.8.96 → 8.9.4 ​【应用大小】:67MB ​【软件说明】:软件升级更新。iOS可配合qx小火箭类的工具对…

P1179 [NOIP2010 普及组] 数字统计————C++

目录 [NOIP2010 普及组] 数字统计题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 提示 解题思路Code1Code2运行结果 [NOIP2010 普及组] 数字统计 题目描述 请统计某个给定范围 [ L , R ] [L, R] [L,R] 的所有整数中,数字…

ivrobot乐高EV3 鲸鱼 能力风暴自制遥控手柄库文件和编程样例 使用指南

编程示例: 资源下载链接: https://download.csdn.net/download/abilix_tony/88739582 EV3 mindstorms能用基础版和高阶版(条形编程界面) EV3 classroom只能用基础版 (scratch模块形状编程界面) 请根据使…

基于Xilinx K7-410T的高速DAC之AD9129开发笔记(二)

引言:上一篇文章我们简单介绍了AD9129的基础知识,包括芯片的重要特性,外部接口相关的信号特性等。本篇我们重点介绍下项目中FPGA与AD9129互联的原理图设计,包括LVDS IO接口设计、时钟电路以、供电设计以及PCB设计。 LVDS数据接口设…

设计一个简单的规则引擎

👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家📕系列专栏:Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术🔥如果感觉博主的文章还不错的…

史诗级长文--朴素贝叶斯

引言 朴素贝叶斯算法是有监督的学习算法,解决的是分类问题,如客户是否流失、是否值得投资、信用等级评定等多分类问题。该算法的优点在于简单易懂、学习效率高、在某些领域的分类问题中能够与决策树、神经网络相媲美。但由于该算法以自变量之间的独立&am…

VC++中使用OpenCV读取图像、读取本地视频、读取摄像头并实时显示

VC中使用OpenCV读取图像、读取本地视频、读取摄像头并实时显示 最近闲着跟着油管博主murtazahassan,学习了一下LEARN OPENCV C in 4 HOURS | Including 3x Projects | Computer Vision,对应的Github源代码地址为:Learn-OpenCV-cpp-in-4-Hour…

Java重修第五天—面向对象3

通过学习本篇文章可以掌握如下知识 1、多态; 2、抽象类; 3、接口。 之前已经学过了继承,static等基础知识,这篇文章我们就开始深入了解面向对象多态、抽象类和接口的学习。 多态 多态是在继承/实现情况下的一种现象&#xf…

STM32 定时器输入捕获2——捕获高电平时长

由上图我们可以知道,高电平时间t2-t1。在代码中,可以记录此时t1的时间然后再记录t2的时间,t2-t1,就是我们所想要的答案。 但是,还有更简单一点点的,当到达t1的时候,我们把定时器清零&#xff0c…

Edge浏览器入门

关于作者: CSDN内容合伙人、技术专家, 从零开始做日活千万级APP,带领团队单日营收超千万。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、商业化变现、人工智能等,希望大家多多支持。 目录 一、导读二、概览…

【C++11】Lambda 表达式

Lambda表达式简介 Lambda 表达式是 C11 中语法之一Lambda 表达式把函数看作对象,把这个表达式当做对象使用Lambda 表达式可以赋值给变量,也可以当做参数传给真正的函数 Lambda表达式语法解析 [capture-list] (parameters) mutable -> return-type {…

【汇编要笑着学】汇编模块化编程 | call和ret调用指令 | jmp跳转指令 | inc自加指令

Ⅰ.汇编模块化编程 0x00 一个简单的例子 我们了解模块化编程前先给出一个例子,方便大家快速了解。 SECTION MBR vstart0x7c00 ; 起始地址编译在0x7c00mov ax,cs mov ds,ax mov es,axmov ss,axmov fs,axmov sp,0x7c00 ; 上面这些都没什…

低代码开发平台

低代码开发平台(LCDP)本身也是一种软件,它为开发者提供了一个创建应用软件的开发环境。看到“开发环境”几个字是不是很亲切?对于程序员而言,低代码开发平台的性质与IDEA、VS等代码IDE(集成开发环境&#x…

Python 自学(八) 之模块

目录 1. import语句导入模块 P206 2. from ... import 语句导入模块 P207 3. 模块的搜索目录 sys.path P209 4. 以主程序的形式执行 __name__ P212 5. python中的包 P213 1. import语句导入模块 P206 同一目录下&…

centos7下搭建ldap服务器

参考: 全网最全Centos7.9搭建LDAP服务器图形界面_ldap服务器搭建-CSDN博客 LDAP服务搭建 - 简书 https://www.cnblogs.com/netsa/p/16017326.html 安装 #安装ldap服务 yum install -y openldap-servers openldap-clients openldap openldap-devel compat-openl…

centos docker-compose安装教程-2024最新版 亲测可用

目录 长时间不安装,生疏了,再次记录下 1.下载 2.修改名称 3.提权 4.测试验证 长时间不安装,生疏了,再次记录下 1.下载 官网地址 docker-compose官网地址:https://docs.docker.com/compose/compose-file/compose-file-v3/ #进入目录 cd /usr/local/bin#下载 wg…

统信UOS操作系统上禁用IPv6

原文链接:统信UOS操作系统上禁用IPv6 hello,大家好啊!继之前我们讨论了如何在麒麟KYLINOS上禁用IPv6之后,今天我要给大家带来的是在统信UOS操作系统上禁用IPv6的方法。IPv6是最新的网络通信协议,但在某些特定的网络环境…

首次落地零担快运!商用车自动驾驶跑出交付加速度

即将迈入2024年,还活着的自动驾驶玩家,身上有两个显著标签:选对了细分赛道、会玩。 10月以来,Cruise宣布在美国德州奥斯汀、休斯顿、亚利桑那州凤凰城和加州旧金山全面停止所有自动驾驶出租车队运营服务,通用汽车计划…

解决:Windows 无法访问指定设备、路径或文件。你可能没有适当的权限访问改项目。

故障情况:双击打开我的电脑和文件夹均弹出图上提示。右击可以打开,任务栏WIN开始 打不开,时钟和通知都点不开,所有系统自带的软件统统无法打开,但第三方软件可以正常打开。 故障原因:钉钉DingTalk_v7.5.0.…