3

React Fragment 添加事件监听?

 2 years ago
source link: https://mebtte.com/react_fragment_with_event_listener
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

React Fragment 添加事件监听?

2022-06-26

React Fragment 支持不添加额外节点的情况下对子元素进行分组, 在文档上中 Fragment 的 Props 只有 key, 我们给 Fragment 添加任何事件监听例如 onClick 都是没有效果的.

<Fragment onClick={() => alert('click from fragment')}>
  <button type="button">yes</button>
  <button type="button">no</button>
</Fragment>

通常情况下我们会把 Fragment 的事件监听分配给各个子节点:

const onClick = () => alert('click from fragment');
return (
  <Fragment>
    <button type="button" onClick={onClick}>
      yes
    </button>
    <button type="button" onClick={onClick}>
      no
    </button>
  </Fragment>
);

不过这种写法不太优雅, 特别是子节点自身就有同名事件监听的情况下:

const onClick = () => alert('click from fragment');
const onClickYes = () => {
  alert('click from yes');
  onClick();
};
const onClickNo = () => {
  alert('click from no');
  onClick();
};
return (
  <Fragment>
    <button type="button" onClick={onClickYes}>
      yes
    </button>
    <button type="button" onClick={onClickNo}>
      no
    </button>
  </Fragment>
);

React 仓库有一个讨论以上情况的 issue, 遗憾的是 React 官方暂没有打算在 Fragment 上添加事件监听, 不过有一个讨论引起了我的注意:

issue 里提及了 display contents

issue 里提及了 display contents

于是在 MDN 上查阅了 display: contents:

display: contents

display: contents

简单来说, 一个 display=contents 的节点, 自身表现相当于 display=none 但是子节点会照常渲染, 我们看个例子:

<div
  class="parent"
  style="width: 200px; height: 200px; border: 1px solid black;"
>
  <div class="child" style="color: blue;">child</d>
  <div class="child" style="color: red;">child</div>
</div>

上面的 html 渲染结果会是这个样子:

e872d9387a78a9cc48dd25e14e7cd686.png

如果我们给 parent 添加 display: contents 就会取消渲染, 但是 child 正常渲染:

display_contents

display=contents 虽然不会渲染但是保留了其他特性, 包括事件监听, 根据这个特点, 我们可以用来模拟 Fragment:

<div
  style={{ display: 'contents' }}
  onClick={() => alert('click from fragment')}
>
  <button type="button" onClick={() => alert('click from yes')}>
    yes
  </button>
  <button type="button" onClick={() => alert('click from no')}>
    no
  </button>
</div>

这样实现的 Fragment 虽然达到了不可见的效果, 不过依然存在 DOM 结构中, 在一些特殊场景下依然会导致问题, 比如官方文档中 table 的例子:

80c55d92cb93706781cfc156d18672e0.png

Warning: validateDOMNesting

Warning: validateDOMNesting

所以 display: contents 并不能很好地解决这个问题.

React Fragment 是 16.2 推出的, 在这之前, 我一直用 react-aux 实现 Fragment 的功能, 它的原理很简单:

// https://github.com/gajus/react-aux/blob/master/src/ReactAux.js
module.exports = function Aux(props) {
  return props.children;
};

同时, React 自身提供了对 children 的遍历方法 Children.map, 所以是不是可以在 react-aux 的基础上对 children 进行遍历操作注入事件监听?

测试多次后得到了以下组件:

function Fragment(props) {
  const { children, ...rest } = props;
  return Children.map(children, (child) => {
    const externalProps = {};
    Object.keys(rest).forEach((propName) => {
      /**
       * 只需要处理 on 开头的 props
       * 忽略非事件监听
       */
      if (propName.startsWith('on')) {
        externalProps[propName] = (event) => {
          /**
           * 模拟事件捕获/冒泡
           * 捕获先触发 Fragment 的事件监听, 后子节点
           * 冒泡先触发子节点的事件监听, 后 Frgment
           */

          /**
           * 如果子节点自身有同名事件监听
           * 按照捕获或者冒泡的顺序触发
           * 需要注意把 event 传递
           */
          const emitChild = () => {
            if (child.props[propName]) {
              child.props[propName](event);
            }
          };

          const emit = (capture) => {
            if (!capture) {
              emitChild();
            }

            /**
             * 触发 Fragment 的事件监听
             * 同样地把 event 传递
             */
            rest[propName](event);

            if (capture) {
              emitChild();
            }
          };

          emit();
        };
      }
    });

    /**
     * 克隆原有的子节点
     * 改写原来的 props
     */
    return cloneElement(child, {
      ...child.props,
      ...externalProps,
    });
  });
}

值得注意的是事件处理方法有 捕获冒泡 两种方式, 需要额外处理 Fragment 和子节点的触发顺序. 此外, 如果通过 child.props.onClick = x 这样的方式改写 props 是不会生效的, 所以需要利用 React 节点的克隆方法 cloneElement 改写 props.

下面例子中, Fragment 的事件处理会在子节点身上触发, 捕获和冒泡的触发顺序也正确, 以及支持嵌套:

fragment_with_event_listener

当然, 没有特殊需求的情况还是建议使用 React 提供的 Fragment, 毕竟上面的封装也会导致一部分的性能损失.

进一步阅读


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK