可视化组件的状态管理
状态的分类
交互状态
参考王渊《交互状态设计-探索 ECharts 与 ZRender 的设计模式》
在特定的交互行为下才会触发的样式(状态)
这里的交互指的并不一定是常规的鼠标交互行为,其代表的是我们将一种事物变更为另一种状态的行为,例如一个列表中文本的搜索、筛选高亮,交互则指的是搜索和筛选的这两种用户行为。
状态的设计
五种常用状态:
NORMAL、SELECTED、UNSELECTED、EMPHASIS、BLUR
状态的管理
将状态的变更和状态的应用分离开。
React、Vue、ZRender 的状态管理
React
小状态:state
大状态:Redux
Vue
小状态:state
大状态:Vuex/Pinia
ZRender
setProxy 的设计
如何用状态表达对象的系统特征
由于节点(Node)的抽象粒度较粗,而其关联数据的特征信息很多,对于数据分析场景来说,其实有很高的信息表达复杂度。于是出现了这样一个需求:通过一个开关可让用户控制是否需要按节点类型分组着色的效果。说明白一点就是,在按交互行为类型对状态类型进行抽象设计的情况下,一个 Node 实例对象在 normal 状态下可能会因为开关的状态不同而表现不同,该如何应对此类场景?
其实也有两个方案,一是对开关的状态进行状态类型抽象,这样就可以实现 normal 和 switchOpen 两个状态叠加应用并更新对象,但状态的叠加还会产生其它问题,例如状态叠加的顺序。不过,查看 ZRender 的源码,其实可以看到对多状态的叠加是支持的,但在实际使用过程中,叠加的多个状态定义也会产生一些问题。
从按交互行为对状态类型进行抽象的角度来看,这是全部 Node 对象共有的交互特性,也可看作是 Node 个体特征,而要将全部 Node 分组进行着色实际上是 Node 的系统特征的表达。所以,第二种方案就是将交互行为分为两类:用户行为(个体行为)和系统行为。在状态类型抽象设计时,具体的状态类型表达的是用户行为,而对应的状态对象定义可以动态计算,其依赖于系统行为状态。这样,就避免了状态叠加的复杂性和问题,利用命令式编码的灵活性来方便的应对此类复杂场景。实际上,这也是之前为何选择了动态计算状态定义的方案,而 ZRender 中 stateProxy() 的设计应该也是考虑了此类场景。
–王渊
设计要点
0、不要 2 个交互操作一个状态(气泡移入移出的例子)
1、保持单向数据流,不要形成环状依赖(即通过读取当前页面上的元素的状态,来计算下一次显示内容的状态)
2、对状态进行整理,行为树 +状态机,每个行为对应一个【视图】,每个视图中,针对每个元素设置其【状态】
3、每次渲染,都需要执行 2 个操作:
(1)回收:清空当前所有绘图元素的状态
(2)赋值:赋予需要绘制的元素状态
4、参考王渊龙虎榜的例子,ZRender 的 state 管理状态的叠加
状态的层级分类
状态可以分为全局状态(视图维度,比如顶部视图)和组件状态(对象维度)
状态的数据结构定义
1、建议不要简单的弄一个 1、2、3、4,最好定一个结构,包含标记、描述信息等等
2、建议定义一个统一的接口来处理状态行为(这里指大状态,即交互行为),支持前置/后置钩子:
1 | |
(TODO)状态机与行为树
状态机和行为树PresleyGo 的博客-CSDN 博客行为树和状态机区别
FSM(状态机)、HFSM(分层状态机)、BT(行为树)的区别 - 程序员大本营
从这个角度来看,我们的动态图表似乎根本用不着状态机和行为树,因为太简单了。
3D 场景因为子元素比较多,且每个元素状态可能不一样,可能会用得上分层有限状态机(HFSM)。
行为树:ZRender 的动画机制
状态机:ECharts 或者我们自己的业务
状态机
两个核心元素:状态 + 切换状态的条件
网状结构,复杂后就成为一团乱麻。
状态与状态之间的过渡通过事件的触发来形成(用户的交互动作也属于事件),是一种“事件触发型”AI,就是只有事件的触发才会发生引起状态的变化。
FSM:有限状态机
HFSM:分层有限状态机
FSM 当状态太多的时候,不好维护,于是将状态分类,抽离出来,将同类型的状态做为一个状态机,然后再做一个大的状态机,来维护这些子状态机。
这样大大的降低了状态机的复杂度。
如果感觉还是过于复杂,就继续分层。
我们的视图、关键帧,就是分层状态机。
决策树(Decision Tree)
行为树(Behavior Tree- The Next-Gen AI)
这是当前的主流。
树状结构,相对于状态机的网状结构来说耦合性更低。
行为树的子节点执行时需要返回,返回类型有:False,True,Running
行为树把行为逻辑和状态数据剥离
关键词:tasks(任务),action(行为),composite(复合),conditional(条件),decorator(修饰符)
比如 NPC 要跳跃,那么前提条件可以是:已完成上一个动作,HP 大于 0,下一个行为状态设置为跳跃。把这些条件看作状态数据,把跳跃的 Animation 看做行为,那么当要切换行为的时候,就不用去管上一个状态是什么行为、和上一个状态是什么关系。构建出来的行为树,只需要判断当前的状态数据是否满足跳跃的条件,满足就执行行为,不满足就返回 false。
(精)可视化编程原则-基于状态的动画控制
参考这个需求的方案设计案例。
实际上也是一个分层状态/关键帧属性和子状态/图元属性的拆解过程。每个关键帧就是一个分层状态,每个分层状态涵盖了里面各个子图元的小状态。
针对动态 Y 轴这个需求,我们在每个分层状态/关键帧中,需要设置的是如下几个子图元的状态:
1、设置裁剪矩形(clip)的宽度,实现从左到右的动画效果
2、设置折线的 polyline 的 point 的 y 坐标值,实现 Y 轴范围变更时,折线下降的动画效果
3、设置 Y 轴的文本标签的数值,实现动态 Y 轴
本质上都是操纵 ZRender 修改小图元的属性;通过 ZRender 的自动插帧,实现属性的过渡动画。
PS:这个机制可以成为动画类需求的通用解决方案。
ZRender 如何处理 state
可以通过 ZRender 源码的elementOperation.html这个示例测试 State。
注意:状态切换,核心都是切换 API,而交互是更上一层的事情。
参考 Element.ts 的_applyStateObj()方法(处理 state 数据)和_transitionState()方法(执行过渡动画)
animateTo 和 state 没关系
举个例子,比如鼠标 Hover 到图中的某类数据的时候,其他类别的图表需要进行淡化处理。这里可跟用户输入的 Option 不一样,每个组件其实内部都是有自己的 UI 相关的状态,所以这里图标淡化或者是饼图扇区变大这种 UI 状态相关的操作都是由组件本身的 State 去控制的,最终也是通过 ZRender 去渲染。这里有个简单公式:
Final Option = User Option + UserData + UI State。
其中只要任何一个项变化,都会触发整个 EChart 实例进行渲染,其中:
- UserOption 可以通过 setOption 去改变
- UserData 可以因为 Legend 点击隐藏某一项 data 而改变,也可以因为 setOption 改变
- UI State 可以因为用户进行某种 UI 操作改变了 UI 状态,比如饼图弹出、Hover 高亮折线图等
Zrender 的 State 是什么?
State is a collection of properties.
比如名为emphasis的 state,其内容类似这样:
1 | |
Element 中对于 State 的处理
我们看下 ZRender 的 Element 类中和状态相关的属性和方法(注意看中文注释):
1 | |
State 与 Animation 的关系
State 是 Animation 的上层应用:State 会触发 Animation(调用_updateAnimationTargets()),Animation 不会触发 State。
因此在 elementOperation.html 中,运行 Animation 相关的测试不会触发useState()
ECharts 与 State
ECharts 是基于状态的配置化图表库,其 style、emphasis 等等,本质上都是状态。
ECharts 中的 states 是行为和样式的集合。
我们打印 echart 的实例对象,可以查看到其_zr 属性(存放页面上所有 ZRender 图形元素实例的属性)的内容如下:

这里的 style 对应的就是当前渲染的状态。
ECharts 是如何处理 State 的?
详见 src/core/echarts.ts 文件的bindMouseEvent方法。
1 | |
这里的 handleGlobalMouseOverForHig()方法,最终其实就是调用各种切换状态的方法,比如 blurComponent 和 enterEmphasisWhenMouseOver,最终都是调用src/util/states.ts中的这个方法,只是参数不同而已:
1 | |
src/util/states.ts是一个很关键的文件,把这里的方法过一遍有助于在设计 State 的时候查漏补缺。
开发技巧
状态迁移前,记得清理当前状态(如果不清理状态,会导致叠加状态有问题,比如将上一次的 emphasis 的部分状态属性叠加到本次状态中了,而这些属性并不是你本次想要的)
通过上层(业务层)抽象来解决交互问题
业务层设计一个锁的机制,解决 click 和 hover 的交互问题
状态的特性
可叠加/合并:这就是为什么 Element 的 states 是一个数组的缘故
有优先级:比如 hover 和 click 交互,针对的状态有优先级,hover 不会覆盖前面 click 产生的状态
可以无限向上抽象:即 HFSM
我们 3D 业务开发框架的状态管理
照搬的ZRender的Element的 state 设计,useState()的逻辑一毛一样。
我画了个草图:

如何定义子类的具体状态属性?
通过 Element 类的_states 定义:
1 | |
如何动态计算状态属性?
通过 Element 类的_stateProxy 进行动态计算:
1 | |
具体计算逻辑由子类去实现。
如何在帧循环中减少不必要的状态处理?
在 controller 的 private __applyChangedStates(stage: Stage)方法中,通过 isDirty 来实现:
1 | |