React.js自定义事件用于与父节点通信

94

我正在创建和监听普通的DOM CustomEvent以与父节点进行通信:

在子节点中:

  var moveEvent = new CustomEvent('the-graph-group-move', { 
    detail: {
      nodes: this.props.nodes,
      x: deltaX,
      y: deltaY
    },
    bubbles: true
  });
  this.getDOMNode().dispatchEvent(moveEvent);
在父级中:
componentDidMount: function () {
  this.getDOMNode().addEventListener("the-graph-group-move", this.moveGroup);
},

这个方法是可行的,但是有没有更适合React的方式呢?


8
React的做法是将回调函数通过props显式地传递给子组件——<Child onCustomEvent={this.handleCustomEvent} />。React不支持带有冒泡功能的自定义事件。 - andreypopp
18
那么,将回调往下传递而不是向上传递事件?看起来很合理。 - forresto
30
@forresto喜欢讽刺,+1。 - Dimitar Christoff
15
我没有使用讽刺的语气。 - forresto
5
确立最佳实践和防止可行的模式是两码事。相比之下,http://twitter.github.io/flight/ 使用 DOMEvents 来冒泡和传播合成事件,形成了如此鲜明的对比。 - Dimitar Christoff
显示剩余8条评论
6个回答

54

如上所述:

React的做法是通过props显式地向子组件传递回调函数,不支持自定义事件和冒泡。

响应式编程抽象是正交的:

通过观察者模式来编写交互式系统很困难且容易出错,但它仍然是许多生产环境中的实现标准。我们提出了一种方法,逐渐废弃观察者而采用响应式编程抽象。几个库层帮助程序员平滑地从回调迁移到更声明性的编程模型。

React哲学基于命令模式:

enter image description here

参考资料


9
我对为什么React没有支持自定义合成事件感兴趣。这是因为它们从未被实现,还是因为有一个有效的设计决策而没有实现?事件是否像goto语句一样被认为是有害的? - Michael Bylstra
7
由于"经典问题",定制事件不被看好:长时间的事件链会导致难以确定某些代码的触发原因。React 的理念是使用“单向数据流”和“命令模式”,详情请参考命令模式 以及一种单向数据流的介绍 - Paul Sweatte
4
考虑到组件通过React上下文与由祖先注入的Datastore进行通信(Redux、React Router等都使用上下文),因此说一个子代通过上下文中的任何函数回调到祖先不符合React的习惯是完全错误的。在调用Redux的“dispatch”方法并传入一个“action”对象,以及在上下文中调用一个“事件处理程序”并传入一个“事件”对象之间实际上没有任何区别。 - Andy
2
在某些人使用Redux的方式中,长链事件触发其他事件非常普遍(例如redux-saga的疯狂)。 - Andy
2
通过观察者模式编写交互式系统是困难且容易出错的。哈哈,这只是在谈论发明React的人的编程技能。没有观察者模式是一个糟糕的系统,会产生大量代码,并且实际上可能会引入更多错误。 - Lenka Pitonakova
显示剩余2条评论

9

您可以编写一个简单的服务,然后使用它。

/** eventsService */
module.exports = {
  callbacks: {},

  /**
   * @param {string} eventName
   * @param {*} data
   */
  triggerEvent(eventName, data = null) {
    if (this.callbacks[eventName]) {
      Object.keys(this.callbacks[eventName]).forEach((id) => {
        this.callbacks[eventName][id](data);
      });
    }
  },

  /**
   * @param {string} eventName name of event
   * @param {string} id callback identifier
   * @param {Function} callback
   */
  listenEvent(eventName, id, callback) {
    this.callbacks[eventName][id] = callback;
  },

  /**
   * @param {string} eventName name of event
   * @param {string} id callback identifier
   */
  unlistenEvent(eventName, id) {
    delete this.callbacks[eventName][id];
  },
};

例子(触发同样适用)

import eventsService from '../../../../services/events';
export default class FooterMenu extends Component {
  componentWillMount() {
    eventsService
      .listenEvent('cart', 'footer', this.cartUpdatedListener.bind(this));
  }

  componentWillUnmount() {
    eventsService
      .unlistenEvent('cart', 'footer');
  }

  cartUpdatedListener() {
    console.log('cart updated');
  }
}

我认为EventAggregator/EventHub是一个很好的解决方案。为了支持您的答案,我建议实现类似下面这样的代码:class Bus extends EventTarget {...}其中 "..." 实现了 发布-订阅 模式/接口(publish(), subscribe(), unsubscribe()),它只是简单地包装了 super.dispatchEvent()super.addEventListener()super.removeEventListener()(分别)。 - Cody

6
您可以通过通过通过上下文传递的回调函数将事件向上冒泡:[CodePen]
import * as React from 'react';

const MyEventContext = React.createContext(() => {});

const MyEventBubbleContext = ({children, onMyEvent}) => {
  const bubbleEvent = React.useContext(MyEventContext);
  const handleMyEvent = React.useCallback((...args) => {
    // stop propagation if handler returns false
    if (onMyEvent(...args) !== false) {
      // bubble the event
      bubbleEvent(...args);
    }
  }, [onMyEvent]);
  return (
    <MyEventContext.Provider value={handleMyEvent}>
      {children}
    </MyEventContext.Provider>
  );
};

const MyComponent = () => (
  <MyEventBubbleContext onMyEvent={e => console.log('grandparent got event: ', e)}>
    <MyEventBubbleContext onMyEvent={e => console.log('parent got event: ', e)}>
      <MyEventContext.Consumer>
        {onMyEvent => <button onClick={onMyEvent}>Click me</button>}
      </MyEventContext.Consumer>
    </MyEventBubbleContext>
  </MyEventBubbleContext>
);

export default MyComponent;

3
我知道这个问题现在已经很老了,但这个答案可能仍然有助于某些人。我为React编写了一个JSX pragma,它添加了声明性的自定义事件:jsx-native-events
基本上,你只需要使用onEvent<EventName>模式来观察事件即可。
<some-custom-element onEventSomeEvent={ callback }></some-custom-element>

3

如果你在ReactJs应用程序中绝对必须使用观察者模式,那么一个可能的解决方案是劫持普通事件。例如,如果您想要将删除键引起标记为删除的 <div>,您可以让 <div> 监听 keydown 事件,该事件将被自定义事件调用。在 body 上捕获 keydown,并在选定的 <div> 上分派 customEvent keydown 事件。分享一下,以帮助有需要的人。


3

这基本上是在说“实现观察者模式”。我同意Michael Bylstra在OP中的评论,将Less Simple Communication中提到的问题描述为“钻孔”...没有冒泡,传递回调函数无法处理超过1级深度结构。 - ericsoco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接