继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

基于观察者模式的事件驱动React:属性钻取的简洁替代方案

慕容森
关注TA
已关注
手记 407
粉丝 183
获赞 650

在现代 React 开发中,管理状态和组件通信可能变得非常有挑战性,尤其是在应用变得越来越复杂时。虽然 Redux 和 Context API 这样的工具已经变得流行起来,但它们却常常引入了额外的样板代码和复杂性。有没有一种更简单、更优雅的方式来处理组件通信,而不需要依赖这些工具呢?介绍一种使用 观察者模式的事件驱动 React

属性钻取和状态管理带来的问题

在 React 中,当你需要通过多个组件层次传递数据或回调函数给深层嵌套的子组件时,就会发生 props 传递。这会让你的代码更难维护,也更容易出错。虽然像 Redux 和 Context API 这样的库可以帮助你集中管理状态,但它们也各自有自己的权衡。

  • Redux 需要写很多样板代码,并且对于小型应用来说可能有点多余。
  • Context API 相比之下更简单,但如果不小心使用,可能会导致不必要的重新渲染。

你可能觉得有很多其他的 Redux 替代方案,例如 Zustand,但它们都是外部库,并且我们尽量避免在没有任何帮助的情况下进行 prop 传递 嘿嘿

如果我们能以一种更松耦合的方式实现组件间的通信,而不依赖这些工具会怎样?

观察者模式:轻量级的解决方案

观察者模式(Observer 模式)是一种设计模式,其中对象(主题对象)维护其依赖者,并在状态变化时通知它们。这种模式非常适合用来创建事件总线——一个中心通信渠道,组件可以在这里发布和订阅事件。

通过使用事件总线,组件可以直接进行通信,无需传递 props,也无需依赖全局状态管理。这种方法能够促进组件之间的松耦合,使你的代码更加模块化,同时也更易于维护。

我们来画一个简图

    +--------------------+
    |    事件总线系统    |
    |--------------------|
    | - on(事件名称, cb) |
    | - off(事件名称, cb)|
    | - emit(事件名称, 数据对象)|
    +--------------------+
             ^   ^  
             |   |  
             |   |  
    +---------+  +-----------------+
    |  按钮组件       |  | 消息显示组件 |
    |----------|  |----------------|
    | - emit() |  | - 监听()       |
    +---------+  +-----------------+

来看看我们有:

主题(事件总线):在那里有如下的方法:on()off()emit()

组件(观察者),框代表的是 React 组件(ButtonMessageDisplay)。按钮会发出一个事件

事件流:从Button组件到消息总线(例如,emit('buttonClicked')),接着到MessageDisplay组件(例如,on('buttonClicked'))。

在 React 中实现事件中心

让我们用观察者模式来创建一个简单的事件流。下面是如何做:

1. 创建事件总线

首先,我们将定义一个简单的事件总线类。

class EventBus {  
  constructor() {  
    this.listeners = {};  
  }  

  on(event, callback) {  
    if (!this.listeners[event]) {  
      this.listeners[event] = [];  
    }  
    this.listeners[event].push(callback);  
  }  

  off(event, callback) {  
    if (this.listeners[event]) {  
      this.listeners[event] = this.listeners[event].filter(  
        (listener) => listener !== callback  
      );  
    }  
  }  

  emit(event, data) {  
    if (this.listeners[event]) {  
      this.listeners[event].forEach((listener) => listener(data));  
    }  
  }  
}  

const eventBus = new EventBus();  
export default eventBus;  

EventBus(事件总线)允许组件进行事件的订阅(on)、取消订阅(off)和触发(emit)。

2. 在组件里使用事件总线

现在我们已经有了事件总线,让我们来看看如何在 React 组件里使用它。

出版活动

一个组件可以通过 emit 方法来触发事件。

    import React from 'react';  
    import eventBus from './eventBus';  

    const Button = () => {  
      const handleClick = () => {  
        eventBus.emit('buttonClicked', { message: '按钮已被点击!' });  
      };  

      return <button onClick={handleClick}>点击</button>;  
    };  

    export default Button;
订阅事件通知

还可以有其他组件通过调用 on 方法来监听事件。

import React, { useEffect, useState } from 'react';  
从 './eventBus' 导入 eventBus;  

const MessageDisplay = () => {  
  const [message, setMessage] = useState('');  

  useEffect(() => {  
    const handleButtonClick = (data) => {  
      setMessage(data.message);  
    };  

    eventBus.on('buttonClicked', handleButtonClick);  

    return () => {  
      eventBus.off('buttonClicked', handleButtonClick);  
    };  
  }, []);  

  return <div>{message}</div>;  
};  

export default MessageDisplay;

在这个示例里,当按钮被点击时,MessageDisplay 组件更新状态来显示由 Button 组件触发的消息。

测试事件流

事件总线是我们架构的核心,所以我们需要验证它是否按预期运行。我们将为 EventBus 类编写单元测试,以确保其能正确处理订阅、退订和事件发射。

EventBus 示例测试集

    import EventBus from './eventBus';  

    describe('EventBus', () => {  
      let eventBus;  

      beforeEach(() => {  
        eventBus = new EventBus();  
      });  

      it('应订阅一个事件,并在事件触发时调用监听器', () => {  
        const listener = jest.fn();  
        eventBus.on('testEvent', listener);  

        eventBus.emit('testEvent', { message: 'Hello, world!' });  

        expect(listener).已调用与({ message: 'Hello, world!' })匹配;  
      });  

      it('应取消订阅一个事件,此时不应调用监听器', () => {  
        const listener = jest.fn();  
        eventBus.on('testEvent', listener);  
        eventBus.off('testEvent', listener);  

        eventBus.emit('testEvent', { message: 'Hello, world!' });  

        expect(listener).not.toHaveBeenCalled();  
      });  

      it('应处理同一个事件的多个监听', () => {  
        const listener1 = jest.fn();  
        const listener2 = jest.fn();  
        eventBus.on('testEvent', listener1);  
        eventBus.on('testEvent', listener2);  

        eventBus.emit('testEvent', { message: 'Hello, world!' });  

        expect(listener1).已调用与({ message: 'Hello, world!' })匹配;  
        expect(listener2).已调用与({ message: 'Hello, world!' })匹配;  
      });  

      it('在没有监听器的情况下触发事件不应抛出错误', () => {  
        期望(() => {  
          eventBus.emit('testEvent', { message: 'Hello, world!' });  
        }).未抛出异常;  
      });  
    });
如何测试使用事件总线的那些组件

我们将使用React Testing Library和Jest来测试这些发布或订阅事件的组件,确保它们能正确处理事件中心的交互。

测试 Button 组件的示例

Button 组件被点击时会触发一个事件。我们将测试该事件是否正确触发。

    import React from 'react';  
    import { render, fireEvent } from '@testing-library/react';  
    import Button from './Button';  
    import eventBus from './eventBus';  

    jest.mock('./eventBus'); // 模拟了事件总线  

    describe('Button', () => {  
      it('点击按钮时应该触发一个事件', () => {  
        const { getByText } = render(<Button />);  
        const button = getByText('点我');  

        fireEvent.click(button);  

        expect(eventBus.emit).toHaveBeenCalledWith('buttonClicked', {  
          message: '按钮被点了一下!',  
        });  
      });  
    });
例子:测试 MessageDisplay 组件

MessageDisplay 组件订阅了一个事件;它会在事件触发时更新其状态。我们将测试该组件是否正确处理事件。

    import React from 'react';  
    import { render, act } from '@testing-library/react';  
    import MessageDisplay from './MessageDisplay';  
    import eventBus from './eventBus';  

    jest.mock('./eventBus'); // 模拟事件总线  

    describe('MessageDisplay', () => {  
      it('接收到事件时应更新消息', () => {  
        const { getByText } = render(<MessageDisplay />);  

        // 模拟事件触发  
        act(() => {  
          eventBus.listeners['buttonClicked'][0](https://medium.com/?message=按钮被点击了!);  
        });  

        期望(getByText('按钮被点击了!')).toBeInTheDocument();  
      });  

      it('在卸载时应清理事件监听器', () => {  
        const { unmount } = render(<MessageDisplay />);  

        unmount();  

        期望(eventBus.off).被调用时传入('buttonClicked', 期望.任何(Function));  
      });  
    });
关键的测试方法
  1. 模拟事件总线:使用 jest.mock 模拟事件总线,并在测试中控制其行为。
  2. 测试事件发出:验证组件是否正确发出了事件,并带有正确的数据。
  3. 测试事件订阅:确保组件对事件作出正确反应。
  4. 测试清理工作:检查组件卸载时事件监听器是否正确移除,以避免内存泄露。
测试工具
  • Jest :用于单元测试和模拟。
  • React Testing Library :以模拟用户交互的方式测试React组件。
  • 模拟函数(mock 函数) :使用 jest.fn() 来模拟事件处理器并验证其行为。
使用消息总线的好处有哪些?
  1. 解耦组件:组件不再需要了解彼此。它们通过事件总线进行通信,促进职责分离。
  2. 无 Props 传递:你可以避免在多个组件层次中传递 props。
  3. 轻量级:与 Redux 或 Context 不同,事件总线易于实现且不需要额外依赖。
  4. 可扩展:随着应用的扩展,你可以增加更多事件和监听器,而不会显著增加复杂性。
瑞典何时使用事件总线

(Note: Following the expert suggestions, I've adjusted the translation to "何时使用事件总线" to accurately reflect the source text and maintain a more direct and colloquial tone. Mentioning "瑞典" was removed as it was not present in the source text. The final output is adjusted to "何时使用事件总线" as per the expert recommendations.)

Corrected:

瑞典何时使用事件总线

Correction:

何时使用事件总线

虽然事件总线是一个强大的工具,但它并不是适用于所有情况的解决方案。以下是一些它表现优异的场景:

  • 小型到中型应用:对于较小的应用程序,“事件总线”可能是一个更简单的选择。
  • 跨组件通信:当不同组件需要跨越组件树进行通信时。
  • 解耦逻辑:当你希望组件保持独立且易于重用时。

不过,对于大型应用来说,如果状态管理需求复杂,Redux 或 Context 可能仍然是更好的选择。

结论部分

观察者模式和事件驱动架构为 React 中处理组件间的通信提供了一种干净优雅的方式。通过使用事件中心,你可以消除 props 穿层传递,减少冗余代码,并保持组件的独立性和易维护性。

虽然它不能替代所有状态管理场景,但它是你的 React 工具箱里有价值的工具。在你的下一个项目里试一试,看看它如何让你的代码更简洁!

如果你喜欢我做的事情,请通过Ko-fi支持一下我!

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP