我们在进行可视化的交互时,经常会遇到卡顿/抖动、不流畅的情况,例如:
- 连续拖缩放图表、拖拽画布时,会发生卡顿
- 拖拽连续图例过滤数据时,图表响应变慢
- 浏览器窗口拖拽变化时,图表卡死
- 甚至鼠标在图表上移动时,tooltip 不流畅
这里很重要的一个原因是这个交互触发频率太快,导致一些操作频繁进行,这时候我们就需要思考如何对同一个交互中频繁进行触发的操作进行限流。
交互语法概览
G2 的交互语法,是将交互拆解成多个环节,每个环节由触发和反馈组成。只要你能将交互用自然语言的方式描述出来,就可以使用 G2 的交互语法进行组合搭建出交互行为。在这里我们再一起温习下 G2 交互语法中对于交互环节的定义,更详细的内容可以阅读可视化交互语法。
G2 将每一个交互环节拆解成以下步骤:
- showEnable 示能:表示交互可以进行;
- start 开始:交互开始;
- processing 持续:交互持续;
- end 结束:交互结束;
- rollback 回滚:取消交互,恢复到原始状态;
对交互语法使用不太了解的用户可以阅读我们前面写过的几篇文章:《可视化的交互语法》、《交互语法之鼠标高亮n》、《交互语法之框选高亮》等
本篇是在交互语法使用基础上优化交互的体验,方案优雅而又高效。
debounce 和 throttle
在前端领域说到限流就要提起 debounce 和 throttle:
- debounce 是延迟执行,直到不再触发
- throttle 是保证在一定时间内必定执行一次
这两个函数社区讨论了好多年,也有很多优雅的实现,这里不占看介绍,感兴趣的可以阅读这篇文章:《Debouncing and Throttling Explained Through Examples》
这里仅介绍一下这两个函数的用法和参数:
- debounce(callback, {wait, immediate}) 延迟 callback 函数的执行,直到不再触发
- wait: 等待的时间,超过这个时间就触发,如果在这个时间内则继续延迟函数执行
- immediate:是触发后马上执行,还是等待 wait 的时间后执行
- throttle(callback, {wait, leading, trailing}) 保证在 wait 的时间内必定执行一次
- wait:等待时间,在这个时间内触发一次
- leading:触发后马上执行,如果执行后还有触发则等待 wait 的时间,如果不再有触发则仅触发这一次
- trailing: 触发停止后是否再执行一次,这个参数是保证在最后一次触发后再执行一次
我们可以把这两个函数应用到我们的交互语法中,先从单个交互的优化开始。
优化缩放图表
以缩放图表为例,我们来看一下 throttle 的实现,以伪码的方式来看一下使用 throttle 和不使用之间的差别。
我们先列出缩放交互的语言描述和交互语法:
语言描述:
- 鼠标滚轮向下,图表放大
- 鼠标滚轮向上,图表变小
交互语法:
registerInteraction('view-zoom', {
start: [
{
trigger: 'plot:mousewheel', isEnable(context) {
return isWheelDown(context.event);
}, action: 'scale-zoom:zoomOut'
},
{
trigger: 'plot:mousewheel', isEnable(context) {
return !isWheelDown(context.event);
}, action: 'scale-zoom:zoomIn'
}
]
});
我们对 scale-zoom Action 进行改造即可,这个 Action 有两个方法:
- zoomIn: 缩小图表
- zoomOut: 放大图表
未使用
class ScaleZoom extends Action {
zoomIn() {
this.zoomScale(-0.05);
} // 缩小 5%
zoomOut() {
this.zoomScale(0.05);
}
/**
* 对比当前缩放的倍数
*/
zoomScale(scale) {
// do zoom
}
}
使用 throttle 后
class ScaleZoom extends Action {
zoomIn = throttle(() => {
this.zoomScale(-0.05);
}, {leading: true, trailing: false}); // 缩小 5%
zoomOutthrottle(() => {
this.zoomScale(0.05);
}, {leading: true, trailing: false});
/**
* 对比当前缩放的倍数
*/
zoomScale(scale) {
// do zoom
}
}
所有交互都支持
通过上面的实现,我们看到在单个 Action 改造方法时需要用 debounce 或者 throttle 函数进行封装,同时函数的参数都需要写死,如果我们要在拖拽移动图表上也增加 throttle 的支持那么就需要重新实现一遍,何不把 debounce 和 throttle 实现成交互语法中的机制,用户可以在任意交互,任意环节配置,实现思路非常简单:
- 每个交互环节支持用户配置 throttle 或者 debounce
- 初始化交互时,如果发现 throttle 或者 debounce ,对触发事件的回调函数进行封装即可
/**
* debounce 的配置
*/
export interface DebounceOption {
/**
* 等待时间
*/
wait: number;
/**
* 是否马上执行
*/
immediate?: boolean;
}
/**
* throttle 的配置
*/
export interface ThrottleOption {
/**
* 等待时间
*/
wait: number;
/**
* 马上就执行
*/
leading?: boolean;
/**
* 执行完毕后再执行一次
*/
trailing?: boolean;
}
这一实现对原先的所有交互不造成影响,同时又可以附加限流配置项
新的语法支持
还是以缩放图表为例,我们增加 throttle 的支持:
- 鼠标滚轮向下,每 50ms 触发一次图表的放大
- 鼠标滚轮向上,每 50ms 触发一次图表的缩小
registerInteraction('view-zoom', {
start: [
{
trigger: 'plot:mousewheel', isEnable(context) {
return isWheelDown(context.event);
}, action: 'scale-zoom:zoomOut',
throttle: { wait: 50, leading: true, trailing: false }
},
{
trigger: 'plot:mousewheel', isEnable(context) {
return !isWheelDown(context.event);
}, action: 'scale-zoom:zoomIn',
throttle: { wait: 50, leading: true, trailing: false }
}
]
});
- leading: true 表示触发后马上执行
- trailing: false 表示 50ms 后如果没有新的触发不再执行
对比一下文章开始的同样两个交互,加了 throttle 后就流畅多了
更多
总结
在实现完成交互的限流后,我们排查所有的交互,发现有大量的交互需要增加限流的配置项,例如:
- 画布的缩放、拖拽,窗口大小改变引起的图表大小变化
- 连续过滤数据
- 在页面上连续滑动显示 tooltip 时
- 框选进行图表元素的高亮时
- 多图表联动(联动显示 tooltip、联动框选、联动过滤)
而这些交互每个的限流配置都不相同,是使用 debounce 还是 throttle,延迟 16ms 还是 100ms,是触发时马上执行还是延迟后执行,各有不同,需要用户自己慢慢品味,慢慢调整,经过精心的调配,交互的流程性会更好。
再次见证交互语法的强大性,欢迎大家使用!