一次Toast组件引发的思考

1,459 阅读3分钟

背景

最近组内有一个新项目,需要用的Toast这样一个组件。心里想,这样的组件,还不是分分钟就搞定呀。然后一头砸进去了开始写。

如何做

toast 的展示与否,跟展示什么我都交给父组件去做,Toast本身只管展示就可以了,内部需要任何别的逻辑 第一版代码如下

import React from 'react';
import styles from './style.module.css';


const Toast = ({ content, showToast }) => {
  if (!showToast) {
    return null;
  }

  return <div className={styles.container}>{content}</div>;
};

export default Toast;

然后再调用的地方,发现如果我需要使用Toast,那么我使用的地方都得引入一下,这显然很不友好。 然后我就将控制Toast的展示与否交由最外层的App组件去做, 如果那里需要使用Toast,那么就将handleShowToast 传递下去就好了 代码如下

import React, { useState } from 'react';
import Toast from '@components/Toast';

const App = (props) => {
    const [showToast, setToastVisible] = useState(false);
    const [content, setToastContent] = useState('');
    const handleShowToast = (toastContent, delay) => {
        setToastVisible(true);
        setToastContent(toastContent);
        const timer = setTimeOut(() => {
            setToastVisible(false);
            setToastContent('');
        }, delay)
    }
    
    return (
        <div>
            <Toast showToast={showToast} content={content}/>
            <div>
                ...
            </div>
        </div>
    );
};

export default App;

但是这样用起来还是不舒服,如果页面多了,那岂不是每一处页面都需要这么处理,如果层级比较深,那岂不是要一层一层的传递下去?

综上所碰到的问题,我思考了一下我想要的Toast组件的样子

  1. 只需页面引入一次即可
  2. Toast 本身的逻辑自己处理,不需要调用方去管
  3. Toast 需要暴露一些 API,让业务方去调用

整理完上面的问题,想起了观察者模式的应用,我是不是可以,在 Toast 本身去订阅几个事件,然后当我想要处理跟Toast 相关逻辑的时候我再 emit 相关事件,事情是不是就变的简单了呢?

首先我需要一个事件系统,这个事件系统需要满足以下功能

  1. on 方法去订阅事件
  2. off 去解除订阅
  3. emit 方法去触发事件
  4. 有一个 list 去存储所有相关事件。 最终实现的 event 如下
interface EventD {
  list: any;
}

export default class Event implements EventD {
  list = new Map();

  on(event: string, callback: any) {
    if (!this.list.has(event)) {
      this.list.set(event, []);
    }
    this.list.get(event).push(callback);
    return this;
  }

  off(event: string) {
    this.list.delete(event);
    return this;
  }

  emit(event: string, ...args: any) {
    if (this.list.has(event)) {
      this.list.get(event).forEach((callback: any) => setTimeout(() => callback(...args), 0));
    }
  }
}

有了事件系统,我们再去改造以下我们的Toast

import React, { useEffect, useState } from 'react';

import Event from '@lib/event';
import styles from './style.module.css';

const event = new Event();

const Toast = () => {
  const [showToast, setToastVisible] = useState(false);
  const [content, setToastContent] = useState('');

  useEffect(() => {
    event.on('showToast', (toastContent: string, delay: number = 2000) => {
      setToastContent(toastContent);
      setToastVisible(true);
      const timer = setTimeout(() => {
        clearTimeout(timer);
        setToastVisible(false);
      }, delay);
    });
    return () => {
      event.off('showToast');
    };
  });

  if (!showToast) {
    return null;
  }

  return <div className={styles.container}>{content}</div>;
};

export default Toast;

export const showToast = (content: string, delay?: number) => event.emit('showToast', content, delay);

这样我们就可以在APP组件内引用一次即可,如果需要展示 toast 的时候,只需要调用一下 Toast 组件暴露的 showToast 方法即可。

总结

  1. 类似 Toast 这样需要事件系统的组件还有很多,我们都可以往这样的思维方式上面去改造
  2. 在写 React 的时候不要总局限于,父子间的一层一层的传递