MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 自定义事件的实现与应用

2022-02-253.9k 阅读

React 自定义事件基础概念

在 React 应用开发中,我们经常会使用到各种内置事件,如点击事件 onClick、输入事件 onChange 等。然而,在某些复杂业务场景下,内置事件无法满足需求,这就需要我们自定义事件。

React 自定义事件本质上是一种在组件间通信和处理特定业务逻辑的机制。它允许我们在组件中定义和触发自定义的事件,并且可以传递相关的数据,从而实现更灵活的交互和逻辑控制。

自定义事件的应用场景

  1. 组件间复杂交互:例如,一个电商应用中,购物车组件和商品列表组件之间,当商品添加到购物车时,不仅要更新购物车的数量,还可能需要触发一个自定义事件通知其他组件(如导航栏显示购物车数量)。
  2. 特定业务逻辑处理:在表单验证场景下,当用户输入满足特定条件时,触发自定义事件,进行下一步操作,比如提交表单前的最终确认。
  3. 抽象复用逻辑:如果多个组件有相似的交互逻辑,但又不能简单地使用内置事件,通过自定义事件可以将这些逻辑抽象出来,提高代码复用性。

实现 React 自定义事件的方法

基于发布 - 订阅模式

  1. 原理:发布 - 订阅模式是实现自定义事件的常见方式。在这种模式中,有发布者(Publisher)和订阅者(Subscriber)。发布者负责触发事件,而订阅者在感兴趣的事件发生时接收通知并执行相应的处理函数。
  2. 代码示例
    • 首先,创建一个简单的发布 - 订阅工具类 EventEmitter
class EventEmitter {
    constructor() {
        this.events = {};
    }
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    }
    emit(eventName, ...args) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(callback => callback(...args));
        }
    }
}
  • 然后,在 React 组件中使用这个 EventEmitter
import React, { Component } from'react';

// 创建 EventEmitter 实例
const emitter = new EventEmitter();

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message: ''
        };
        // 订阅自定义事件
        emitter.on('customEvent', (data) => {
            this.setState({
                message: data
            });
        });
    }
    handleClick = () => {
        // 触发自定义事件
        emitter.emit('customEvent', '自定义事件被触发啦!');
    }
    render() {
        return (
            <div>
                <p>{this.state.message}</p>
                <button onClick={this.handleClick}>触发自定义事件</button>
            </div>
        );
    }
}

export default MyComponent;

在上述代码中,EventEmitter 类实现了基本的发布 - 订阅功能。MyComponent 组件在构造函数中订阅了 customEvent 事件,并在点击按钮时触发该事件,从而更新组件的状态。

使用 React 上下文(Context)

  1. 原理:React 上下文提供了一种在组件树中共享数据的方式。我们可以利用上下文来传递事件处理函数,从而实现自定义事件。通过将事件处理函数放在上下文对象中,不同层级的组件都可以访问并触发这些事件。
  2. 代码示例
    • 创建上下文对象:
import React from'react';

const CustomContext = React.createContext();

export default CustomContext;
  • 创建父组件,提供上下文数据:
import React, { Component } from'react';
import CustomContext from './CustomContext';

class ParentComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            value: ''
        };
        this.handleCustomEvent = this.handleCustomEvent.bind(this);
    }
    handleCustomEvent(data) {
        this.setState({
            value: data
        });
    }
    render() {
        const contextValue = {
            handleCustomEvent: this.handleCustomEvent
        };
        return (
            <CustomContext.Provider value={contextValue}>
                {this.props.children}
            </CustomContext.Provider>
        );
    }
}

export default ParentComponent;
  • 创建子组件,触发自定义事件:
import React, { Component } from'react';
import CustomContext from './CustomContext';

class ChildComponent extends Component {
    handleClick = () => {
        const { handleCustomEvent } = this.context;
        if (handleCustomEvent) {
            handleCustomEvent('子组件触发的自定义事件');
        }
    }
    render() {
        return (
            <div>
                <button onClick={this.handleClick}>触发上下文自定义事件</button>
            </div>
        );
    }
}

ChildComponent.contextType = CustomContext;

export default ChildComponent;
  • 在应用中使用:
import React from'react';
import ReactDOM from'react-dom';
import ParentComponent from './ParentComponent';
import ChildComponent from './ChildComponent';

const rootElement = document.getElementById('root');
ReactDOM.render(
    <ParentComponent>
        <ChildComponent />
    </ParentComponent>,
    rootElement
);

在这个示例中,ParentComponent 通过 Context.Provider 提供了 handleCustomEvent 函数,ChildComponent 通过 contextType 获取上下文并触发自定义事件,从而更新 ParentComponent 的状态。

基于自定义 DOM 事件(适用于与原生 DOM 交互场景)

  1. 原理:在 React 中,虽然我们通常使用合成事件,但在某些与原生 DOM 紧密交互的场景下,可以创建和触发自定义 DOM 事件。通过 document.createEvent 方法创建自定义事件对象,然后使用 dispatchEvent 方法触发事件。
  2. 代码示例
    • 创建一个包含原生 DOM 操作的 React 组件:
import React, { Component } from'react';

class CustomDOMEventComponent extends Component {
    componentDidMount() {
        const button = document.getElementById('custom-dom-button');
        button.addEventListener('customDOMClick', (event) => {
            console.log('自定义 DOM 事件被触发:', event.detail);
        });
    }
    handleClick = () => {
        const event = new CustomEvent('customDOMClick', {
            detail: {
                message: '这是自定义 DOM 事件的数据'
            }
        });
        const button = document.getElementById('custom-dom-button');
        button.dispatchEvent(event);
    }
    render() {
        return (
            <div>
                <button id="custom-dom-button" onClick={this.handleClick}>触发自定义 DOM 事件</button>
            </div>
        );
    }
}

export default CustomDOMEventComponent;

在上述代码中,CustomDOMEventComponent 组件在挂载后为按钮添加了 customDOMClick 事件的监听器。当按钮被点击时,创建并触发自定义 DOM 事件,同时传递了一些数据。

React 自定义事件的优化与注意事项

  1. 事件命名规范:为了避免命名冲突和提高代码可读性,自定义事件的命名应该遵循一定的规范。通常采用驼峰命名法,并且名称要能够清晰地表达事件的含义,比如 userLoggedInformSubmitted 等。
  2. 内存泄漏问题:在使用发布 - 订阅模式时,如果订阅者没有正确地取消订阅,可能会导致内存泄漏。例如,在组件卸载时,应该移除所有的事件监听器。
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message: ''
        };
        emitter.on('customEvent', (data) => {
            this.setState({
                message: data
            });
        });
    }
    componentWillUnmount() {
        emitter.off('customEvent');
    }
    handleClick = () => {
        emitter.emit('customEvent', '自定义事件被触发啦!');
    }
    render() {
        return (
            <div>
                <p>{this.state.message}</p>
                <button onClick={this.handleClick}>触发自定义事件</button>
            </div>
        );
    }
}

componentWillUnmount 生命周期方法中,调用 emitter.off('customEvent') 来移除事件监听器,防止内存泄漏。 3. 事件传递数据的安全性:当通过自定义事件传递数据时,要注意数据的安全性。避免传递敏感信息,并且对传递的数据进行必要的验证和过滤,防止恶意数据导致的安全漏洞。 4. 性能优化:在频繁触发自定义事件的场景下,要注意性能问题。例如,在发布 - 订阅模式中,如果有大量的订阅者,触发事件可能会带来性能开销。可以考虑采用批量处理的方式,减少事件触发的频率,或者使用更高效的算法来管理订阅者。

结合 React 生命周期与自定义事件

  1. 组件挂载时的事件订阅:在 componentDidMount 生命周期方法中订阅自定义事件是常见的做法。这样可以确保在组件渲染到 DOM 后开始监听事件,避免在组件还未准备好时触发事件导致错误。
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            data: ''
        };
    }
    componentDidMount() {
        emitter.on('newDataAvailable', (newData) => {
            this.setState({
                data: newData
            });
        });
    }
    componentWillUnmount() {
        emitter.off('newDataAvailable');
    }
    render() {
        return (
            <div>
                <p>{this.state.data}</p>
            </div>
        );
    }
}

在上述代码中,MyComponentcomponentDidMount 中订阅了 newDataAvailable 事件,当该事件触发时更新组件状态。 2. 组件更新时的事件处理:有时候,我们可能需要在组件更新时根据新的 props 或 state 来处理自定义事件。可以在 componentDidUpdate 生命周期方法中进行相关操作。

class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            status: 'initial'
        };
    }
    componentDidMount() {
        emitter.on('statusChanged', (newStatus) => {
            this.setState({
                status: newStatus
            });
        });
    }
    componentDidUpdate(prevProps, prevState) {
        if (prevState.status!== this.state.status) {
            // 根据新的状态处理其他逻辑,例如触发另一个自定义事件
            emitter.emit('statusUpdateHandled', this.state.status);
        }
    }
    componentWillUnmount() {
        emitter.off('statusChanged');
        emitter.off('statusUpdateHandled');
    }
    render() {
        return (
            <div>
                <p>当前状态: {this.state.status}</p>
            </div>
        );
    }
}

在这个例子中,当 statusChanged 事件触发导致组件状态更新后,componentDidUpdate 检查状态变化并触发 statusUpdateHandled 自定义事件。 3. 组件卸载时的事件清理:如前面提到的,在 componentWillUnmount 中清理事件订阅是非常重要的。这不仅可以防止内存泄漏,还能避免在组件已经不存在的情况下触发事件导致的运行时错误。

React 自定义事件与 Redux 或 MobX 等状态管理库的结合

  1. 与 Redux 结合:在 Redux 应用中,自定义事件可以作为触发 action 的一种方式。例如,当某个组件触发自定义事件时,可以通过 dispatch 方法来触发 Redux 的 action,从而更新全局状态。
import React, { Component } from'react';
import { connect } from'react-redux';
import { updateUserInfo } from './actions';

class UserComponent extends Component {
    constructor(props) {
        super(props);
        this.handleCustomEvent = this.handleCustomEvent.bind(this);
    }
    handleCustomEvent() {
        // 假设自定义事件传递了新的用户信息
        const newUserInfo = { name: '新名字', age: 25 };
        this.props.dispatch(updateUserInfo(newUserInfo));
    }
    render() {
        return (
            <div>
                <button onClick={this.handleCustomEvent}>触发更新用户信息的自定义事件</button>
            </div>
        );
    }
}

export default connect()(UserComponent);

在上述代码中,UserComponent 触发自定义事件后,通过 dispatch 触发 updateUserInfo action 来更新 Redux 中的用户信息。 2. 与 MobX 结合:MobX 使用 observable 和 action 来管理状态。自定义事件可以与 MobX 的 action 相结合。当自定义事件触发时,调用 MobX 的 action 来修改 observable 状态。

import React, { Component } from'react';
import { observer } from'mobx-react';
import store from './store';

class MyMobXComponent extends Component {
    constructor(props) {
        super(props);
        this.handleCustomEvent = this.handleCustomEvent.bind(this);
    }
    handleCustomEvent() {
        store.updateData('新数据');
    }
    render() {
        return (
            <div>
                <p>{store.data}</p>
                <button onClick={this.handleCustomEvent}>触发自定义事件更新 MobX 数据</button>
            </div>
        );
    }
}

export default observer(MyMobXComponent);

在这个例子中,MyMobXComponent 触发自定义事件后,调用 store 中的 updateData action(在 MobX 中定义为 action)来更新 store 中的 observable 数据。

高级应用:自定义事件在复杂组件库开发中的应用

  1. 组件库中的事件抽象:在开发复杂的 React 组件库时,自定义事件可以用于抽象组件间的交互逻辑。例如,一个表单组件库可能有多种类型的表单组件,如文本输入框、下拉框等。可以定义一些通用的自定义事件,如 inputChangedselectChanged 等,不同的表单组件触发这些自定义事件,上层应用可以统一订阅这些事件来处理表单数据的变化。
// 文本输入框组件
class TextInput extends Component {
    constructor(props) {
        super(props);
        this.state = {
            value: ''
        };
        this.handleChange = this.handleChange.bind(this);
    }
    handleChange(event) {
        this.setState({
            value: event.target.value
        });
        // 触发自定义事件
        emitter.emit('inputChanged', {
            type: 'text',
            value: event.target.value
        });
    }
    render() {
        return (
            <input
                type="text"
                value={this.state.value}
                onChange={this.handleChange}
            />
        );
    }
}

// 下拉框组件
class SelectInput extends Component {
    constructor(props) {
        super(props);
        this.state = {
            selectedValue: ''
        };
        this.handleChange = this.handleChange.bind(this);
    }
    handleChange(event) {
        this.setState({
            selectedValue: event.target.value
        });
        // 触发自定义事件
        emitter.emit('inputChanged', {
            type:'select',
            value: event.target.value
        });
    }
    render() {
        return (
            <select onChange={this.handleChange}>
                <option value="option1">选项 1</option>
                <option value="option2">选项 2</option>
            </select>
        );
    }
}

在应用中,可以订阅 inputChanged 事件来统一处理不同表单组件的数据变化。 2. 自定义事件驱动的组件状态机:在一些复杂的交互组件中,可以使用自定义事件来驱动组件的状态机。例如,一个具有多种状态的模态框组件,如 openedclosingclosed 等状态。可以通过自定义事件 openModalcloseModal 等来触发状态的转换。

class ModalComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            status: 'closed'
        };
        emitter.on('openModal', this.openModal.bind(this));
        emitter.on('closeModal', this.closeModal.bind(this));
    }
    openModal() {
        if (this.state.status === 'closed') {
            this.setState({
                status: 'opened'
            });
        }
    }
    closeModal() {
        if (this.state.status === 'opened') {
            this.setState({
                status: 'closing'
            });
            setTimeout(() => {
                this.setState({
                    status: 'closed'
                });
            }, 1000);
        }
    }
    componentWillUnmount() {
        emitter.off('openModal');
        emitter.off('closeModal');
    }
    render() {
        return (
            <div>
                {this.state.status === 'opened' && <div>模态框内容</div>}
                <button onClick={() => emitter.emit('openModal')}>打开模态框</button>
                <button onClick={() => emitter.emit('closeModal')}>关闭模态框</button>
            </div>
        );
    }
}

在这个例子中,ModalComponent 通过订阅 openModalcloseModal 自定义事件来驱动模态框的状态变化。

自定义事件在 React Native 中的应用

  1. React Native 中的事件机制与 React 的异同:React Native 基于 React 的思想,但在事件处理上有一些差异。React Native 使用原生的触摸事件等,并且合成事件的实现也与 Web 端有所不同。然而,自定义事件的基本原理仍然适用。
  2. 在 React Native 中实现自定义事件:以发布 - 订阅模式为例,同样可以创建一个 EventEmitter 类。
class EventEmitter {
    constructor() {
        this.events = {};
    }
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
    }
    emit(eventName, ...args) {
        if (this.events[eventName]) {
            this.events[eventName].forEach(callback => callback(...args));
        }
    }
}

const emitter = new EventEmitter();

// 在 React Native 组件中使用
import React, { Component } from'react';
import { Button, View, Text } from'react-native';

class RNComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message: ''
        };
        emitter.on('customRNEvent', (data) => {
            this.setState({
                message: data
            });
        });
    }
    handlePress = () => {
        emitter.emit('customRNEvent', 'React Native 自定义事件被触发!');
    }
    render() {
        return (
            <View>
                <Text>{this.state.message}</Text>
                <Button title="触发自定义事件" onPress={this.handlePress} />
            </View>
        );
    }
}

export default RNComponent;

在这个 React Native 组件中,通过 EventEmitter 实现了自定义事件,当按钮被点击时触发 customRNEvent 事件并更新组件状态。

  1. 与原生模块交互时的自定义事件:在 React Native 开发中,经常会与原生模块交互。可以在原生模块中触发自定义事件,然后在 React Native 组件中监听。例如,在 iOS 原生模块中,通过桥接机制触发自定义事件,React Native 组件使用 NativeEventEmitter 来监听这些事件。
// iOS 原生模块
#import "MyNativeModule.h"
#import <React/RCTEventEmitter.h>

@interface MyNativeModule ()

@property (nonatomic, strong) RCTEventEmitter *eventEmitter;

@end

@implementation MyNativeModule

RCT_EXPORT_MODULE();

- (instancetype)init
{
    if (self = [super init]) {
        self.eventEmitter = [[RCTEventEmitter alloc] initWithBridge:self.bridge];
    }
    return self;
}

- (NSArray<NSString *> *)supportedEvents
{
    return @[@"nativeCustomEvent"];
}

- (void)sendNativeEvent
{
    [self.eventEmitter sendEventWithName:@"nativeCustomEvent" body:@{@"message": @"来自原生模块的自定义事件"}];
}

@end
// React Native 组件监听原生模块事件
import React, { Component } from'react';
import { NativeEventEmitter, NativeModules } from'react-native';

const MyNativeModule = NativeModules.MyNativeModule;
const eventEmitter = new NativeEventEmitter(MyNativeModule);

class NativeInteractionComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            nativeMessage: ''
        };
        this.subscription = eventEmitter.addListener('nativeCustomEvent', (data) => {
            this.setState({
                nativeMessage: data.message
            });
        });
    }
    componentWillUnmount() {
        this.subscription.remove();
    }
    render() {
        return (
            <View>
                <Text>{this.state.nativeMessage}</Text>
            </View>
        );
    }
}

export default NativeInteractionComponent;

在这个例子中,iOS 原生模块 MyNativeModule 触发 nativeCustomEvent 事件,React Native 组件 NativeInteractionComponent 通过 NativeEventEmitter 监听该事件并更新状态。

跨浏览器与兼容性考虑

  1. 不同浏览器对自定义 DOM 事件的支持:虽然现代浏览器对自定义 DOM 事件有较好的支持,但在一些旧版本浏览器中可能存在兼容性问题。例如,IE 浏览器对 CustomEvent 的支持有限。在使用自定义 DOM 事件时,可以使用 polyfill 来确保兼容性。
if (typeof window.CustomEvent === 'function') return false;

function CustomEvent(event, params) {
    params = params || { bubbles: false, cancelable: false, detail: undefined };
    const evt = document.createEvent('CustomEvent');
    evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
    return evt;
}

window.CustomEvent = CustomEvent;
  1. React 合成事件与自定义事件的兼容性:React 的合成事件在不同浏览器中有统一的行为。当我们结合自定义事件与合成事件时,要注意事件的优先级和处理顺序。例如,在一个组件中既有合成事件 onClick 又有自定义事件 customClick,要确保它们的逻辑不会相互冲突。可以通过合理的事件命名和处理逻辑来避免这种情况。
  2. 跨框架与库的兼容性:如果在项目中同时使用多个前端框架或库,要注意自定义事件与其他框架或库的事件系统的兼容性。例如,在一个同时使用 React 和 jQuery 的项目中,自定义事件的命名和触发机制要避免与 jQuery 的事件系统冲突。可以采用命名空间等方式来隔离不同的事件系统。

调试 React 自定义事件

  1. 使用 console.log 进行调试:在自定义事件的触发和处理函数中添加 console.log 语句是最基本的调试方法。通过打印相关的数据和信息,可以了解事件是否被正确触发,以及传递的数据是否符合预期。
class MyComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message: ''
        };
        emitter.on('customEvent', (data) => {
            console.log('接收到自定义事件数据:', data);
            this.setState({
                message: data
            });
        });
    }
    handleClick = () => {
        console.log('准备触发自定义事件');
        emitter.emit('customEvent', '自定义事件被触发啦!');
    }
    render() {
        return (
            <div>
                <p>{this.state.message}</p>
                <button onClick={this.handleClick}>触发自定义事件</button>
            </div>
        );
    }
}

在上述代码中,通过 console.log 可以清晰地看到事件触发前后的相关信息。 2. 使用 React DevTools 调试:React DevTools 是调试 React 应用的强大工具。虽然它没有专门针对自定义事件的调试功能,但可以通过查看组件的状态变化和 props 传递来间接调试自定义事件。例如,如果自定义事件应该更新组件状态,通过 React DevTools 可以观察到状态是否正确更新,从而判断自定义事件的处理逻辑是否正确。 3. 断点调试:在现代浏览器的开发者工具中,可以使用断点调试功能。在自定义事件的触发和处理函数的代码行上设置断点,然后触发事件,调试器会暂停在断点处,此时可以查看变量的值、调用栈等信息,深入分析事件处理过程中的问题。

通过以上对 React 自定义事件的详细介绍,包括实现方法、应用场景、优化、与其他技术结合等方面,相信开发者能够更好地在项目中运用自定义事件,实现更复杂和灵活的交互逻辑。