对React中组件通信的一些小思考

2,648 阅读4分钟

前言

最近一直都在思考,如何写React能最舒服,从最初的繁琐的ReduxContext,同级组件间的数据交流逐渐的简单了起来,但有时候也会写多余的代码,让我又在想,如何才能让组件间的简单的交流更纯粹

状态提升

最近的项目在做后台管理,简单的页面经常会需要这样的情况,简单页面就是一个查询表单,一个结果的表格

const Page = () => {
  return (
    <Container>
      <SearchForm />
      <ResultTable />
    </Container>
  );
};

功能也很简单,查询表单输入条件,表格显示结果,这是组件的单向通信,即表单条件=>查询表格,但是因为是同级组件,不得不状态提升

状态提升后,变成了这样

const Page = () => {
  const [params,setParams] = useState();
  return (
    <Container>
      <SearchForm setParams={setParams}/>
      <ResultTable params={params}/>
    </Container>
  );
};

这样是可行的,但是却有额外的东西出现,如果使用ReduxContext这样的数据流方式,可以不用传额外的参数,但是却有了更繁琐的步骤

触发式的组件刷新

这个名称是我胡编的,我想表达的意思就是,<SearchForm>组件说<ResultTable>组件你得重新请求数据了,<ResultTable>就会重新请求数据

最先有这个想法,是因为使用了swr这个库,表格组件一定会请求一个接口,如果这个请求是使用swr发起的,在任意组件,甚至不在组件里,都可以使用类似于trigger('/api/tableList')这样的方式来让表格组件刷新

那原生怎么做,其实很简单,发布订阅模式就行了,改写组件

const SearchForm = () => {
  // 触发research事件
  emitter.emit('research', { time: 'now' });
  // ...
};

const ResultTable = () => {
  // 当research事件被触发时
  emitter.on('research', ({ time }) => getList(time));
  // ...
};

这样的方式,就像我想的触发式的组件刷新一样,任何地方触发research,结果表格都会显示最新的结果

其实这样的方式经常有使用,比如Redux就是类似这样的观察者模式实现的,connect的组件订阅了store的更新,每次store更新都会通知这些组件来刷新获取新的state

emitter是如何实现的

这样的库有一大堆,我找到的是preact大佬写的mitt,源码非常短,也非常简单,直接贴出代码分析一下

// event的类型
export type EventType = string | symbol;

// Handler可以接收一个可选的event参数,但是不返回任何值
export type Handler = (event?: any) => void;
// 通配符的Handler
export type WildcardHandler= (type: EventType, event?: any) => void

// 当前已经注册的Handler数组
export type EventHandlerList = Array<Handler>;
export type WildCardEventHandlerList = Array<WildcardHandler>;

// 一个event type的map,每一个event type都有相对应的handler list
export type EventHandlerMap = Map<EventType, EventHandlerList | WildCardEventHandlerList>;

export interface Emitter {
    on(type: EventType, handler: Handler): void;
    on(type: '*', handler: WildcardHandler): void;

    off(type: EventType, handler: Handler): void;
    off(type: '*', handler: WildcardHandler): void;

    emit<T = any>(type: EventType, event?: T): void;
    emit(type: '*', event?: any): void;
}

/** Mitt: Tiny (~200b) functional event emitter / pubsub.
 *  @name mitt
 *  @returns {Mitt}
 */
export default function mitt(all?: EventHandlerMap): Emitter {
    all = all || new Map();

    return {

        /**
         * 为给定的event类型注册一个event handler.
         * @param {string|symbol} type 监听的event类型,通配符"*"监听所有
         * @param {Function} handler 响应给定event事件的函数
         * @memberOf mitt
         */
        on(type: EventType, handler: Handler) {
            // 获取map中对应的type的handler数组
            const handlers = all.get(type);
            // 如果有就添加新的进去
            const added = handlers && handlers.push(handler);
            // 没有就添加新的handler数组
            if (!added) {
                all.set(type, [handler]);
            }
        },

        /**
         * 移除给定事件的一个event handler
         * @param {string|symbol} type 要移除的handler的event类型,或者"*"
         * @param {Function} handler 移除的handler函数
         * @memberOf mitt
         */
        off(type: EventType, handler: Handler) {
            const handlers = all.get(type);
            if (handlers) {
                // >>> 0 一定是非负,正数不变,-1变成4294967295
                handlers.splice(handlers.indexOf(handler) >>> 0, 1);
            }
        },

        /**
         * 触发给定event类型的所有handler函数
         * 如果类型存在,则会在执行完所有类型匹配的handler后再执行”*”的handler
         *
         * Note: 手动触发“*”是不支持的
         *
         * @param {string|symbol} type 需要调用的event type
         * @param {Any} [evt] 任何类型的值,推荐object类型,会传递给每一个handler处理函数
         * @memberOf mitt
         */
        emit(type: EventType, evt: any) {
            // slice浅拷贝一次,否则移除会出现问题 https://github.com/developit/mitt/issues/65
            // map而不是foreach是因为少4个字母……
            ((all.get(type) || []) as EventHandlerList).slice().map((handler) => { handler(evt); });
            ((all.get('*') || []) as WildCardEventHandlerList).slice().map((handler) => { handler(type, evt); });
        }
    };
}

这个库因为太简单了,所以只有200字节,功能也比较少,但是刚好够用

就像redux-thunk只有十来行代码但是有14.9k Star一样,凡是大佬,都很牛皮

总结

感觉自己一直都遵循着规则,所以很老套,这次的小思考让我觉得其实有很多可变的东西,而且也很方便

但是我觉得pubsub来快速的通信,还是得适当的使用,可以作为一个快捷的后备选项,我又想起了很早以前写flutter,因为不会用任何的状态管理库,所以就选择了event_bus这种pubsub的库,后来页面太多,太乱了

太多的组件传递信息,还是得选一个好用的状态管理库,最近发现一个不错的库pullstate,有机会试一试