创建一个可以用函数调用显示的 React 组件(如 react-toastify

IT技术 reactjs
2021-05-15 02:56:52

通常,当创建一个我们想要有条件地渲染的可重用 React 组件时,我们要么给它一个 prop 来告诉它是否渲染自己:

function TheComponent(props) {
    return(
        props.isVisible?<div>...</div>:null
    );
}

或者只是有条件地从外部渲染整个组件:

function App() {
    //...
    return (
            isVisible ? <TheComponent /> : null
        );
}

或者,如果我们想制作一个可以在应用程序中的任何地方显示/隐藏的组件——比如toast 通知——我们可以包装在一个提供者中并制作一个自定义钩子来访问它的上下文;这将让我们在 provider 内部的任何地方显示/隐藏它,只需调用一个函数:

const App = () => (
  <ToastProvider>
    <OtherStuff />
  </ToastProvider>
);

const OtherStuff = () => {
  const { showToast } = useToast();
  showToast();
  return ...;
};

然而,有一个非常酷的包react-toastify,我似乎无法理解它是如何实现的。你所要做的就是<ToastContainer />在你的应用程序某个地方放置一个,然后从其他任何地方,你可以:

import { toast } from "react-toastify";
toast.info("this will show the component with a message");

由于这个函数可以在提供者之外调用,我真的不明白它是如何控制树中其他地方的组件状态的。我试过查看它的代码,但作为一个 React 初学者,这有点超出我的理解。我喜欢完全独立的组件的想法,您可以将其固定在应用程序的某个位置,并通过从任何地方调用函数来调用。没有提供者/包装器或任何东西:只是一个函数调用,然后弹出。

有人可以帮助阐明从根本上说这样的组件如何工作吗?提供者外部的函数如何控制另一个组件内部的状态?

1个回答

在扫视react,toastify代码,你可以看到它使用的事件发射模式。<ToastContainer /> 事件侦听得到出动(或发射)当你调用toast.info当它收到一个事件时,它会更新其内部状态(大概)以显示消息。


TLDR:他们通过eventManager进行间接通信,它公开了 1) 调度事件和 2) 为这些事件注册侦听器的方法。


它类似于 onclick 处理程序在 DOM 中的工作方式。

这是基本模式的一个非常基本的实现:它只是在每次单击按钮时向文档附加一个 div。(这不是 React 或 toastify 特定的。但它展示了核心思想。)

请注意,按钮的单击处理程序对发生的事情一无所知。它不附加 div。它只是通过EventBus下面描述实例发出一个事件

EventBus类提供了一个on注册一个侦听方法。它们通常被称为 addEventListener 或 addListener,或者它们具有特定于事件的名称,如 onClick、onChange 等,但它们都做相同的基本事情:注册一个要调用的函数以响应事件。(这个类本质上是 react-toastify 的eventManager 的一个愚蠢的实现。)

on方法将提供的处理程序添加到内部数组。然后,当一个事件被触发(通过emit)时,它只是遍历数组,调用每个事件并传入事件信息。

const container = document.getElementById('demo');
const button = document.querySelector('button');

class EventBus {
  handlers = [];
  
  on (handler) {
    this.handlers.push(handler);
  }
  
  emit (event) {
    this.handlers.forEach(h => h(event));
  }
}

const emitter = new EventBus();

emitter.on((event) => {
  container.innerHTML += `<div>${event}</div>`;
})

button.addEventListener('click', () => emitter.emit('Button Clicked'));
<button>Emit</button>
<div id="demo"></div>

通过此设置,您可以添加额外的侦听器来执行其他操作,而无需知道事件的来源(单击按钮)。下面的演示与上面的相同,只是它添加了一个额外的处理程序来切换“黑暗”模式。

再次注意,按钮不知道暗模式,暗模式处理程序也不知道按钮,他们都不知道附加的 div。他们是完全解耦的。

这基本上是如何ToastContainertoast.info.

const container = document.getElementById('demo');
const button = document.querySelector('button');

class EventBus {
  handlers = [];
  
  on (handler) {
    this.handlers.push(handler);
  }
  
  emit (event) {
    this.handlers.forEach(h => h(event));
  }
}

const emitter = new EventBus();

emitter.on((event) => {
  container.innerHTML += `<div>${event}</div>`;
})

button.addEventListener('click', () => emitter.emit('Button Clicked'));

// add an additional handler
emitter.on(event => demo.classList.toggle('dark'));
.dark {
  background: black;
  color: white;
}
<button>Emit</button>
<div id="demo"></div>