--- title: G6 状态管理的最佳实践 order: 5 --- ⚠️ 注意: 多值状态、互斥状态、设置子图形状态样式、修改状态样式配置在 V3.4 后支持。 ### 背景 元素(节点/边)的状态(state)用于反馈用户交互、数据变化。通过状态,可以将「交互/数据变化」与视图中「元素的样式变化」快速关联。最常见的例子:鼠标进入节点,该节点为 hover 状态,并被高亮;离开节点,该节点为非 hover 状态,并复原样式。 乍看之下,状态似乎很简单,但从目前业务痛点与经验来看,“状态”有许多隐藏的需求和复杂性。简单的实现方式,会造成易用性低下的问题。 ### 挑战 要解决业务中常见的状态问题,并提供清晰明了、简单易用的方案必须要考虑以下问题: - **简单快速地设置目标状态**:当一个节点或一条边上已经设置了大量不同状态后,要再设置一个新状态时,能够快速将之前所有状态清除; - **状态多值**,即一个状态量存在多个不同的值,如节点代表人,有“健康”、“疑似”、“确诊”、“死亡”四种状态; - **状态间互斥**:“健康”、“疑似”、“确诊”、“死亡”四种状态中,“死亡”与其他三种就是互斥的,不可能同时存在“健康”和“死亡”两种状态; - **节点或边中所有元素的状态更新**:如一个由文本和圆组成的节点,状态变化时不仅能设置圆的样式,也可以设置文本的样式; - **更新状态样式**,当更新节点的样式时,能够同时更新状态的样式。 ### 方案 为了解决以上问题,我们将 G6 的状态管理分为以下几层: - 定义状态:统一的定义方式; - 设置状态:`setItemState` 方法; - 更新状态:`updateItem` 支持更新状态; - 取消状态:`clearItemStates` 方法。 ### 定义状态 #### 全局状态 G6 中定义全局状态是在实例化 Graph 时通过 `nodeStateStyles` 和 `edgeStateStyles` 来定义。 ```javascript const graph = new G6.Graph({ container, width, height, nodeStateStyles: { hover: { fill: 'red', 'keyShape-name': { fill: 'red', }, }, }, edgeStateStyles: {}, }); ``` 当设置 [keyShape](/zh/docs/manual/middle/elements/shape/shape-keyshape/#keyshape) 的状态时,可以直接在 `nodeStateStyles` 或 `edgeStateStyles` 中定义样式,或者将 keyShape 的 `name` 属性值作为 key 定义样式,我们建议使用后者。 #### 节点/边状态 除过全局状态外,G6 也支持针对不同节点定义不同的状态,使用 `graph.node(fn)` / `graph.edge(fn)` 或在数据中设置 `stateStyles` 即可。 ```javascript graph.node((node) => { return { ...node, stateStyles: {}, }; }); const data = { nodes: [ { id: 'node', stateStyles: {}, }, ], }; ``` #### 子图形状态样式 在 G6 中,通常一个节点在绘制层面上,有一个图形分组,该图形分组包含多个图形组成的。G6 3.3 之前的版本,仅支持设置 keyShape 的状态,如果要设置其他部分元素的状态,就需要在自定义元素时复写 `setState` 方法,这意味着状态样式的管理完全需要自定义,用户写起来将会非常复杂。G6 3.4 版本中,我们支持了定义子元素的状态,从此再也不用去复写 `setState` 方法了。 子元素状态也支持两种设置方式,为了演示方便,在这里我们只演示在在全局状态中定义子元素状态。 ```javascript const graph = new G6.Graph({ container, width, height, nodeStateStyles: { selected: { 'sub-element': { fill: 'green', }, 'text-element': { stroke: 'red', }, }, }, edgeStateStyles: {}, }); ``` 在「定义状态」部分,我们建议在 `nodeStateStyles`/`edgeStateStyles` 中定义状态时,使用 keyShape 的 `name` 属性作为 key 值来定义状态样式,因为我们在定义子元素的状态时也采用同样的方式,结构上比较统一。 如上示例代码所示,我们定义了节点中 `name` 属性值为 `'sub-element'` 和 `'text-element'` 两部分的样式,当我们通过 `graph.setItemState(item, 'selected', true)` 设置指定 item 的状态时,子元素 `'sub-element'` 和 `'text-element'` 的样式也会同步更新。 ```javascript // 执行下面语句以后,name 为 sub-element 和 name 为 text-element // 的元素填充色和描边色都会改变 graph.setItemState(item, 'selected', true); ``` 另外,G6 也支持在使用 `updateItem` 更新节点或边的时候定义状态。 ⚠️ 注意: 子图形状态样式仅限于指定节点/边的图形分组下平铺的图形,不支持嵌套图形分组下的图形。内置节点/边的图形分组内的图形均为平铺,在自定义节点时需要注意该规则。 ### 设置状态 G6 中状态支持多值和二值两种情况。 > 二值:值只能为 `true` / `false`,无其他选择,即要么有这个状态,要么没有;多值:如节点代表人,有“健康”、“疑似”、“确诊”、“死亡”四种状态。 #### 二值状态 二值状态一般常用于交互过程中,如 hover、selected 等状态,当节点被选中时,节点应用 selected 状态的样式,取消选中时,去掉 selected 状态的样式。 在 G6 中,使用 `graph.setItemState(item, 'selected', true)` 来设置二值状态。 ```javascript const graph = new G6.Graph({ //... nodeStateStyles: { selected: { fill: 'red', }, }, }); graph.setItemState(item, 'selected', true); ``` #### 多值状态 除过像 hover、selected 这种交互状态外,还会存在很多的业务状态,如节点代表人,有“健康”、“疑似”、“确诊”、“死亡”四种状态,此时使用 `true`/`false` 的形式就不能满足,需要支持单个状态可以有多个值的情况。 ```javascript const graph = new Graph({ // ... 其他配置 // 节点在不同状态下的样式 nodeStateStyles: { // 实现 bodyState 的【多值】【互斥】 'bodyState:healthy': { // keyShape 该状态下的样式, 可以使用三种方式指定: fill: 'green', }, 'bodyState:suspect': {}, 'bodyState:ill': {}, }, }); graph.setItemState(item, 'bodyState', 'healthy'); ``` #### 互斥状态 上面的多值状态,也很好地解决了状态互斥的问题,还以上面的 `bodyState` 状态为例,该状态共有 `healthy`、`dead`、`ill` 等值。 ```javascript //【互斥】 graph.setItemState(item, 'bodyState', 'healthy'); // 执行下面这句话 bodyState 将会被改变成 dead, // item.hasState('bodyState:healthy') 为 false graph.setItemState(item, 'bodyState', 'dead'); ``` 执行上面的两句后, item 只会有 `bodyState` 状态的 `dead` 一个值,而二值状态不能解决这个问题。 ```javascript graph.setItemState(item, 'select', true); graph.setItemState(item, 'active', true); ``` 执行上面的两句设置二值状态的语句后,item 具有 select 和 active 所有的属性值,不能满足互斥需求。 ### 修改状态样式配置 G6 3.3 及 以下的版本中,不支持修改状态样式的配置。`updateItem` 方法只能更新 keyShape 的默认样式。从 G6 3.4 版本开始,`updateItem` 支持更新 item 中所有子图形的默认样式和状态样式。 #### 更新默认样式 `updateItem` 可用于更新 keyShape 以及其他子图形的默认样式。使用 `updateItem` 更新子图形样式时,只需要在 `style` 中以子图形的 `name` 属性作为为 key 即可。 ```javascript // 更新 item,除过更新 keyShape 外,还更新 name 值为 node-text 的图形 graph.updateItem(item, { style: { fill: 'green', stroke: 'green', opacity: 0.5, 'node-text': { stroke: 'yellow', }, }, }); ``` #### 更新状态样式 `updateItem` 也支持更新 keyShape 与其他子图形的状态样式,使用 `stateStyles` 属性。 ```javascript graph.updateItem(item, { style: { stroke: 'green', 'node-text': { stroke: 'yellow', }, }, stateStyles: { hover: { opacity: 0.1, 'node-text': { stroke: 'blue', }, }, }, }); graph.setItemState(item, 'hover', true); ``` 使用 `updateItem` 更新状态的样式时,会存在两种情况: - 使用 `updateItem` 更新时,item 已有指定状态,即 `item.hasState('hover') === true`,此时状态值对应的属性会立即生效; - 使用 `updateItem` 更新时,item 没有指定的状态,即 `item.hasState('hover') === false`,更新以后,当用户执行  `graph.setItemState(item, 'hover', true)` 后,hover 状态的 `stroke` 属性值为 `updateItem` 时设置的值。 ### 取消状态 在 G6 中,我们建议使用 `graph.clearItemStates`  来取消 `graph.setItemState` 设置的状态,`graph.clearItemStates` 支持一次取消单个或多个状态。 ```javascript graph.setItemState(item, 'bodyState', 'healthy'); graph.setItemState(item, 'selected', true); graph.setItemState(item, 'active', true); // 取消单个状态 graph.clearItemStates(item, 'selected'); graph.clearItemStates(item, ['selected']); // 取消多个状态 graph.clearItemStates(item, ['bodyState:healthy', 'selected', 'active']); ``` 以上就是 G6 中状态的定义、设置和取消的全过程,很清晰明了,但总感觉缺少了点什么,没错,想必聪明的你已经发现了,缺少了更新子元素及和 `updateItem` 配合使用的方案。不要着急,接着放下看。 ### 状态优先级 G6 中提供了 `hasState` 方法用于判断元素是否有某种状态。但具体哪个状态的优先级高,哪个状态值应该覆盖其他的类似问题我们就没有再做任何限制,完全由业务用户控制,实现这种控制也非常简单,如一般情况下,鼠标 hover 到某个节点后,该节点会高亮,但希望当该节点处于 active 状态时,鼠标 hover 上去后也不要覆盖 active 的状态,即 active 优先级高于 hover。 ```javascript // 设置节点处于 active 状态 graph.setItemState(item, 'active', true); // 鼠标 hover const hasActived = item.hasState('active'); // 当节点没有 active 时才设置 hover 状态 if (!hasActived) { graph.setItemState(item, 'hover', true); } ```