基于观察者模式实现跨组件通信--react项目为例(含通用代码)

634 阅读3分钟

基于观察者模式实现跨组件通信,规避重复渲染问题

问题来源

在实际开发中会有如下图常见结构,父组件A中包含了左右布局子组件L,R。点击L中某列表项唤起R面板。一般原理是在父组件A中维护了一个state状态isShow,然后L组件点击更改setIsShow,R组件依赖isShow是否为true进行展示。如下图。

show.png

这里会有个问题,从视觉逻辑上,点击以后原本只是右面板组件发生了改变,而左面板也跟着重新渲染,导致左面板滚动状态回到了初始位置。

需求是:右边变化,左边不执行重新渲染。

问题剖析

从diff算法角度,因为父组件的state状态发生更改,就会触发父组件的重新渲染,所以里面的子组件L和R也会重新渲染。理论上说使用React.memo把L组件包住,能解决这个问题。从根本上说就是组件粒度设计的问题。值得注意的是,如果子组件包裹在父组件中声明,React.memo就不起作用。但是把子组件抽离出来声明,React.memo就能生效。因为每次父组件重新渲染,在里面定义的函数(包括函数式组件)的引用地址都不同了,所以子组件就会被重新定义和创建渲染。如果硬要在父组件里面定义子组件,那就用useCallback包住,说不定可以,我没试过。

关于组件粒度设计,当组件逻辑复杂的时候,其实是一个增加心智负担的事情。那么有什么方法可以不考虑复杂的组件间状态依赖关系,直接通信而不牵扯到其他组件状态的改变呢?这时候观察者模式的作用就来了。

观察者模式 事件总线类设计

observation mode.png

直接上代码 百分百能跑

类定义,导出事件总线单例。

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也有人提了这个问题。强行下载也不起作用。

欢迎讨论指正。