基于观察者模式实现跨组件通信,规避重复渲染问题
问题来源
在实际开发中会有如下图常见结构,父组件A中包含了左右布局子组件L,R。点击L中某列表项唤起R面板。一般原理是在父组件A中维护了一个state状态isShow,然后L组件点击更改setIsShow,R组件依赖isShow是否为true进行展示。如下图。
这里会有个问题,从视觉逻辑上,点击以后原本只是右面板组件发生了改变,而左面板也跟着重新渲染,导致左面板滚动状态回到了初始位置。
需求是:右边变化,左边不执行重新渲染。
问题剖析
从diff算法角度,因为父组件的state状态发生更改,就会触发父组件的重新渲染,所以里面的子组件L和R也会重新渲染。理论上说使用React.memo把L组件包住,能解决这个问题。从根本上说就是组件粒度设计的问题。值得注意的是,如果子组件包裹在父组件中声明,React.memo就不起作用。但是把子组件抽离出来声明,React.memo就能生效。因为每次父组件重新渲染,在里面定义的函数(包括函数式组件)的引用地址都不同了,所以子组件就会被重新定义和创建渲染。如果硬要在父组件里面定义子组件,那就用useCallback包住,说不定可以,我没试过。
关于组件粒度设计,当组件逻辑复杂的时候,其实是一个增加心智负担的事情。那么有什么方法可以不考虑复杂的组件间状态依赖关系,直接通信而不牵扯到其他组件状态的改变呢?这时候观察者模式的作用就来了。
观察者模式 事件总线类设计
直接上代码 百分百能跑
类定义,导出事件总线单例。
class EventBus {
subscribers = {};
subscribe = (event, callback) => {
if (!this.subscribers[event]) {
this.subscribers[event] = [];
}
this.subscribers[event].push(callback);
};
unsubscribe = (event, callback) => {
if (this.subscribers[event]) {
this.subscribers[event] = this.subscribers[event].filter(
(cb) => cb !== callback,
);
}
};
publish = (event: string, data?: any) => {
if (this.subscribers[event]) {
this.subscribers[event].forEach((callback) => {
callback(data);
});
}
};
}
export default new EventBus();
钩子函数,把事件总线注入到context
import { createContext, useContext } from 'react';
import EventBus from './index';
const EventBusContext = createContext(EventBus);
export const useEventBus = () => {
return useContext(EventBusContext);
};
使用 -- 订阅
useEffect(() => {
const handleEvent = () => {
// 修改组件state
setIsShow(true);
};
eventBus.subscribe('trigger', handleEvent);
return () => {
eventBus.unsubscribe('trigger', handleEvent);
};
}, [eventBus]);
使用 -- 发布
eventBus.publish('trigger')
总结
有了事件总线以后,也不要滥用。在交互逻辑比较深的时候,组件是否展示的状态可以自己维护。父组件调用的时候,啥也不用传给子组件,父组件只管布局逻辑,不管数据。也不知道这属于什么设计思路,但封装性和复用性会更强,组件之间的耦合度会更低。
额外尝试
一开始的角度是滚动条保存问题。一是手动记录上一次渲染滚动条所在位置并保存,二是找个什么库给组件包一下。
一的话,感觉太麻烦了,而且代码很难复用,直接放弃。
二的话,网络寻医问药了一圈多数是页面跳转返回滚动条位置的解决方案。一般都是keep-alive的逻辑。对于本文具体情况,找了两个库:react-scroll和react-scroll-restoration。
-
第一个star的人还比较多,api也多,但还是依靠记录元素进行定位,还要去获取具体元素,会有点麻烦,而且有小的滚动痕迹,不是想要的。想要的就是一动不动。
-
第二个直接react-route的版本不兼容,只支持4. 5. ,本项目用的umi4,直接干到6了,issue也有人提了这个问题。强行下载也不起作用。
欢迎讨论指正。