使用 stopPropagation() 处理 React 事件委托

IT技术 javascript events reactjs
2021-04-27 21:50:28

我在 React 中有一个项目,它应该可以放在任何网站上。这个想法是我托管一个 javascript 文件,人们放置一个具有特定 ID 的 div,然后 React 在该 div 中呈现。

到目前为止,这有效,除了点击事件。这些事件在顶层处理这一切都很好,但是应该放置应用程序的站点之一已经stopPropagation()实现了画布菜单。因此,事件无法正常工作。

我尝试在根元素捕获所有事件,并手动调度它们:

this.refs.wrapper.addEventListener('click', (event) => {
    console.log(event);
    console.log(event.type);
    const evt = event || window.event;
    evt.preventDefault();
    evt.stopImmediatePropagation();
    if (evt.stopPropagation) evt.stopPropagation();
    if (evt.cancelBubble !== null) evt.cancelBubble = true;
    document.dispatchEvent(event);
});

这不起作用,因为事件已经被调度:

Uncaught InvalidStateError: Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.

解决这个问题的正确方法是什么?不使用 React 的合成事件对我来说似乎不是正确的方法..

4个回答

参数 'event h' 已经被调度。您应该使用旧事件克隆一个新的事件对象。

var newevent = new event.constructor(event.type, event)

目前还没有解决方案。如您所说,React 监听 DOM 根上的事件,如果事件event.target不在React的挂载节点内,则过滤事件
您可以尝试:
1. 在 React 组件中重新分发新事件,但它也会在外部处理程序处停止。
2. 在 React 组件外调度新事件,更高(最接近BODY)然后具有 stopPropagation 回调的节点。但是event.target会指向节点,它不在 React 的组件内部,您不能更改它,因为它是只读的。
也许在下一个版本中他们会修复它。

但是您可以监听文档中的事件,不是吗?

假设整个应用程序的根组件名为app. 然后,在它里面componentDidMount你可以有:

// when the main App component mounts - we'll add the event handlers ..
componentDidMount() {

    var appComponent = this;

    document.addEventListener('click', function(e) {

        var clickedElement = e.target;

        // Do something with the clickedElement - by ID or class .. 
        // You'll have reference to the top level component in `appComponent` .. 

    });
};

正如您所说 - React 在顶级节点(文档)处理所有事件,并确定哪个 React 组件与某个事件相关,React 使用 event.target 属性。因此,为了使一切正常,您应该在文档节点上手动调度已停止的事件,并为此事件设置适当的“目标”属性。

有2个问题需要解决:

  • 您无法触发已分派的事件。要解决此问题,您必须创建此事件的新副本。
  • dispatchEvent()某些节点浏览器上执行后,会自动将此事件的“目标”属性设置为触发事件的节点。要解决这个问题,您应该在 dispatchEvent() 之前设置适当的目标,并使用属性描述符将此属性设为只读。

一般解决方案:

在所有现代浏览器和 IE9+ 中测试的解决方案

这是解决方案的源代码:https : //jsbin.com/mezosac/1/edit?html,css,js,output(有时它会挂起,所以如果您在预览区域没有看到 UI 元素 - 单击右上角的“运行 win js”按钮)

它的评论很好,所以我不会在这里描述所有这些东西,但我会快速解释要点:

  1. 事件应该在停止后立即重新调度,为了实现这一点,我扩展了本地stopPropagationstopImmediatePropagation事件方法以redispatchEventForReact在停止传播之前调用我的函数:

    if (event.stopPropagation) {
        const nativeStopPropagation = event.stopPropagation;
        event.stopPropagation = function fakeStopPropagation() {
            redispatchEventForReact();
            nativeStopPropagation.call(this);
        };
    }
    
    if (event.stopImmediatePropagation) {
        const nativeStopImmediatePropagation = event.stopImmediatePropagation;
        event.stopImmediatePropagation = function fakeStopImmediatePropagation() {
            redispatchEventForReact();
            nativeStopImmediatePropagation.call(this);
        };
    }
    

    还有另一种停止事件的可能性 - 将“cancelBubble”属性设置为“true”。如果您查看 cancalBubble 属性描述符 - 您会发现该属性确实是一对 getter/setter,因此很容易使用 Object.defineProperty 在 setter 中注入“redispatchEventForReact”调用:

    if ('cancelBubble' in event) {
        const initialCancelBubbleDescriptor = getPropertyDescriptor(event, 'cancelBubble');
        Object.defineProperty(event, 'cancelBubble', {
            ...initialCancelBubbleDescriptor,
            set(value) {
                redispatchEventForReact();
                initialCancelBubbleDescriptor.set.call(this, value);
            }
        });
    }
    
  2. redispatchEventForReact 功能:

    2.1 在我们为react调度事件之前,我们应该删除我们自定义的stopPropagationstopImmediatePropagation方法(因为在react代码中,理论上某些组件可以调用e.stopPropagation,它会再次触发redispatchEventForReact,这将导致无限循环):

    delete event.stopPropagation;
    delete event.stopImmediatePropagation;
    delete event.cancelBubble;
    

    2.2 那么我们应该复制这个事件。在现代浏览器中很容易做到,但是为 IE11- 节省了大量代码,所以我将这个逻辑移到单独的函数中(有关详细信息,请参阅 jsbin 上的附加源代码):

    const newEvent = cloneDOMEvent(event);
    

    2.3 由于浏览器在事件被调度时自动设置事件的“目标”属性,我们应该将其设置为只读。这里的重要一点 - 设置 value 和 writeable=false 在 IE11 中不起作用,所以我们必须使用 getter 和空 setter:

    Object.defineProperty(newEvent, 'target', {
        enumerable: true,
        configurable: false,
        get() { return event.target; },
        set(val) {}
    });
    

    2.4 最后,我们可以为 react 调度事件:

    document.dispatchEvent(newEvent);
    
  3. 为了保证在停止此事件之前将针对 react 的 hacks 注入到事件中,我们应该在捕获阶段在根节点上侦听此事件并进行注入:

    const EVENTS_TO_REDISPATCH = ['click'];
    EVENTS_TO_REDISPATCH.forEach(eventToRedispatch => {
        document.addEventListener(eventToRedispatch, prepareEventToBeRedispatched, true);
    });