发布订阅是设计模式中比较出名的一个了,只要不是刚入门的同学,多多少少也有对这个词有所耳闻。 学习过VUE框架的同学都应该知道其 MVVM 架构的实现原理就是数据劫持+发布订阅模式
什么是发布订阅呢?
百度百科说的太详细了太复杂了“发布/订阅”,翻译过来就是:发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。
啥意思呢?拿回 vue 架构作为例子,我们在使用 vue 的时候都知道,我们修改非对象的变量时,不论你有多少个地方用了这个变量,修改的变量都会实时重新渲染到 dom 上。这是因为在定义该变量的时候做数据劫持的时候就在 getter 和 setter 方法中使用了发布订阅模式,使其存在一对多的能力。想详细了解的小伙伴可以自行去查阅~
发布订阅看起来很高级,但是当项目中却忘了该在怎么样的场景去使用了。 今天刚好有一个 echarts 的需求可以使用,那就写下来吧~
需求情况:存在如上图的多个图表,但是点击不同的图表可能存在不同事件(如:点击1,2图表会在1,3中打上标记线,但是点击3却不触发标记线的修改)
按照业务优先的思想去实现这个功能的话,通常我们会直接在这些图表的父级组件上用 useState 存储起来当前的鼠标点击的位置,并将该位置派发给其他的子组件,每个子组件在根据该 state 变化后用该去调用什么方法。 但是考虑到扩展性,这里我选择了使用发布订阅模式。因为可以对每个图表注册对应的事件,让每个注册的事件被触发时自动去回调相应的行为。
发布订阅类
/*
* 定义一个事件发布与新增订阅者的帮助类
*/
import type { EChartsInstance } from "echarts-for-react"
export default class Listener {
events = {}
// 注册事件,事件被触发时候执行对应的回调
on(name: string, cb: () => void) {
if (this.events[name]) {
(this.events[name].push(cb))
} else {
(this.events[name] = [cb])
}
}
// 触发事件,并将对应的参数传入回调
trigger(name: string, ...args: any[]) {
if (this.events[name]) {
this.events[name].forEach((func: EChartsInstance) => {
func(...args)
})
}
}
}
export interface IListenerProps {
on: (name: string, cb: (...arg: any) => void) => void
trigger: (name: string, ...args: any) => void
}
定义好了这个帮助类,我们回到 echart 上处理图表对应的事件,因为我们点击图表,不仅仅在图例上才触发,而是空白处也会触发的。
监听空白处事件
在echart中,zrender 事件和 echarts 事件,zrender 事件与 echarts 事件不同。前者是当鼠标在任何地方都会被触发,而后者是只有当鼠标在图形元素上时才能被触发。事实上,echarts 事件是在 zrender 事件的基础上实现的,也就是说,当一个 zrender 事件在图形元素上被触发时,echarts 将触发一个 echarts 事件给开发者。
import ReactECharts from 'echarts-for-react';
import { useEffect, useRef, useState } from 'react';
import type { FC } from 'react';
import type { IListenerProps } from './lister';
interface IListener {
listener: IListenerProps
}
const LineChart: FC<IListener> = (props) => {
// 父类将帮助类实例 传到子组件中,方便注册事件。
const { listener } = props
const [option, setOption] = useState({})
const ref = useRef<any>(null)
const instance = ref?.current?.getEchartsInstance()
useEffect(() => {
setOption({
grid: { top: 8, right: 8, bottom: 24, left: 36 },
tooltip: {
trigger: 'axis',
showContent: false,
axisPointer: {
axis: 'x',
snap: true,
lineStyle: {
color: 'rgba(86,86,86,0.50)'
}
}
},
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
data: new Array(7).fill(0).map(() => 1000 * Math.random()),
type: 'line',
smooth: 0.6,
labelLine: {
show: false
},
lineStyle: {
type: 'solid',
color: '#ffdd00',
width: 5
},
},
],
})
}, [])
if (instance) {
const zr = instance?.getZr()
// 订阅事件
const marklineAction = (arg: object) => { instance.setOption(arg) }
listener.on('markline', marklineAction)
// 按下鼠标、移动鼠标
zr.on('click', (event: any) => {
const { offsetX, offsetY } = event
const pointInPixel = [offsetX, offsetY]
if (instance.containPixel('grid', pointInPixel)) {
const xIndex = instance.convertFromPixel({ seriesIndex: 0 }, pointInPixel)[0]
const params = {
series: [{
markLine: {
lineStyle: {
type: 'solid'
},
data: [{ xAxis: xIndex }]
}
}]
}
// 图例点击 发布给所有订阅者
zr.on('click', listener.trigger('markline', params))
}
});
}
return <ReactECharts
lazyUpdate={false}
option={option}
notMerge={false}
ref={(e) => { ref.current = e; }}
{...props}
/>
}
export default LineChart
此处是通过 ref 拿到图表的 eachrt 对象,通过调用对象上的 getEchartsInstance() 拿到实例,实例中能访问到 getZr() 方法,在该方法下调用on方法可以坚挺zrender对象的点击事件,拿到对应 offsetX 和 offsetY,通过 convertFromPixel、convertFromPixel、containPixel 对其转换成对应的坐标,通过setOption在图表上画上对应位置的标记线。
其实还可以注册其他的事件,这里就不再赘述了。 灵活变通一下工作中其实还是能常用的~