简体   繁体   English

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

[英]Handle React event delegation with stopPropagation()

I have a project in React which should be able to be placed on any website.我在 React 中有一个项目,它应该可以放在任何网站上。 The idea is that I host a javascript file, people place a div with a specific ID, and React renders in that div.这个想法是我托管一个 javascript 文件,人们放置一个具有特定 ID 的 div,然后 React 在该 div 中呈现。

So far this works, except click-events.到目前为止,这有效,除了点击事件。 These evens are handled at the top level .这些事件在顶层处理 This is all good, but one of the sites where the app should be placed, has stopPropagation() implemented for a off-canvas menu.这一切都很好,但应放置应用程序的站点之一为画布外菜单实现了stopPropagation() Because of this the events aren't working properly.因此,事件无法正常工作。

I tried catching all events at the root-element, and dispatching them manually:我尝试在根元素捕获所有事件,并手动调度它们:

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);
});

This doesn't work, because the event is already being dispatched:这不起作用,因为事件已经被调度:

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

What would be the right way to fix this problem?解决这个问题的正确方法是什么? Not using the synthetic events from React doesn't seem the right way to go for me..不使用 React 的合成事件对我来说似乎不是正确的方法..

Argument 'event h'as already been dispatched.参数 'event h' 已经被调度。 You should clone a new eventobject with old event.您应该使用旧事件克隆一个新的事件对象。

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

Ther is no solution yet.目前还没有解决方案。 React, as you say, listen events on the root of DOM, and filter events if their event.target not inside react's mounted node.如您所说,React 监听 DOM 根上的事件,如果event.target不在 react 的挂载节点内,则过滤事件。
You can try:你可以试试:
1. Redispatch new event in the Reract component, but it will be stopped at outside handler too. 1. 在 React 组件中重新分发新事件,但它也会在外部处理程序中停止。
2. Dispatch new event outside Reract component, higher (closest to BODY ) then node with stopPropagation callback. 2. 在 React 组件之外调度新事件,更高(最接近BODY )然后具有 stopPropagation 回调的节点。 But event.target will point to node, which not inside React's component and you can not change it, beacause it is readonly.但是event.target会指向 node,它不在 React 的组件内部,你不能改变它,因为它是只读的。
Maybe in next versions they will fix it.也许在下一个版本中他们会修复它。

But you can listen for events in the document, no?但是您可以监听文档中的事件,不是吗?

Let say your root component for the whole app is named app .假设整个应用程序的根组件名为app Then, inside it's componentDidMount you can have:然后,在它的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` .. 

    });
};

As you said - React handles all events at the top-level node (document), and to determine which react component relates to some event react uses event.target property.正如您所说 - React 处理顶级节点(文档)上的所有事件,并确定哪个 React 组件与某个事件相关,React 使用 event.target 属性。 So to make everything work you should manually dispatch stopped event on document node and set proper "target" property to this event.因此,为了使一切正常,您应该在文档节点上手动调度已停止的事件,并为此事件设置适当的“目标”属性。

There is 2 problems to solve:有2个问题需要解决:

  • You can't trigger event that is already dispatched.您不能触发已经分派的事件。 To solve this you have to create a fresh copy of this event.要解决此问题,您必须创建此事件的新副本。
  • After you do dispatchEvent() on some node browser automatically set "target" property of this event to be the node on which event is fired.在某些节点浏览器上执行dispatchEvent()后,会自动将此事件的“目标”属性设置为触发事件的节点。 To solve this you should set proper target before dispatchEvent(), and make this property read-only using property descriptors.要解决这个问题,您应该在 dispatchEvent() 之前设置适当的目标,并使用属性描述符将此属性设为只读。

General solution:一般解决方案:

Solution tested in all modern browsers and IE9+在所有现代浏览器和 IE9+ 中测试的解决方案

Here is a source code of solution: https://jsbin.com/mezosac/1/edit?html,css,js,output .这是解决方案的源代码: https : //jsbin.com/mezosac/1/edit?html,css,js,output (Sometimes it hangs, so if you don't see UI elements in preview area - click on "run win js" button on top right corner) (有时它会挂起,所以如果您在预览区域没有看到 UI 元素 - 单击右上角的“运行 win js”按钮)

It is well commented, so I will not describe here all of that stuff, but I will quickly explain main points:它的评论很好,所以我不会在这里描述所有这些东西,但我会快速解释要点:

  1. Event should be redispatched immediately after it was stopped, to achieve this I extended native stopPropagation and stopImmediatePropagation methods of event to call my redispatchEventForReact function before stopping propagation:事件应在停止后立即重新stopImmediatePropagation ,为了实现这一点,我扩展了事件的本机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); }; }

    And there is another one possibility to stop event - setting "cancelBubble" property to "true".还有另一种停止事件的可能性 - 将“cancelBubble”属性设置为“true”。 If you take a look at cancalBubble property descriptor - you will see that this property indeed is a pair of getters/setters, so it's easy to inject "redispatchEventForReact" call inside setter using Object.defineProperty:如果您查看 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 function: redispatchEventForReact函数:

    2.1 Before we dispatch event for react we should remove our customized stopPropagation and stopImmediatePropagation methods (because in react code some component in theory can invoke e.stopPropagation, which will trigger redispatchEventForReact again, and this will lead to infinite loop): 2.1 在我们为react调度事件之前,我们应该删除我们自定义的stopPropagationstopImmediatePropagation方法(因为在react代码中,理论上某些组件可以调用e.stopPropagation,它会再次触发redispatchEventForReact,这将导致无限循环):

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

    2.2 Then we should make a copy of this event. 2.2 那么我们应该复制这个事件。 It's easy to do in modern browsers, but take a looot of code for IE11-, so I moved this logic in separate function (see attached source code on jsbin for details):在现代浏览器中很容易做到,但为 IE11- 节省了大量代码,所以我将这个逻辑移到单独的函数中(有关详细信息,请参阅 jsbin 上的附加源代码):

     const newEvent = cloneDOMEvent(event);

    2.3 Because browser set "target" property of event automatically when event is dispatched we should make it read-only. 2.3 由于浏览器在事件被调度时自动设置事件的“目标”属性,我们应该将其设置为只读。 Important bit here - setting value and writeable=false will not work in IE11-, so we have to use getter and empty setter:这里的重要一点 - 设置 value 和 writeable=false 在 IE11 中不起作用,所以我们必须使用 getter 和空 setter:

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

    2.4 And finally we can dispatch event for react: 2.4 最后,我们可以为 react 调度事件:

     document.dispatchEvent(newEvent);
  3. To guarantee that hacks for react will be injected in event before something stopped this event we should listen to this event on root node in capturing phase and do injections:为了保证在某些东西停止此事件之前将反应的hacks注入事件中,我们应该在捕获阶段在根节点上侦听此事件并进行注入:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM