惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cisco Blogs
NISL@THU
NISL@THU
G
GRAHAM CLULEY
T
Threatpost
I
Intezer
D
Darknet – Hacking Tools, Hacker News & Cyber Security
P
Proofpoint News Feed
L
Lohrmann on Cybersecurity
Cisco Talos Blog
Cisco Talos Blog
P
Privacy & Cybersecurity Law Blog
Security Latest
Security Latest
P
Palo Alto Networks Blog
L
LINUX DO - 热门话题
Cyberwarzone
Cyberwarzone
AI
AI
Help Net Security
Help Net Security
Forbes - Security
Forbes - Security
T
The Exploit Database - CXSecurity.com
月光博客
月光博客
The GitHub Blog
The GitHub Blog
aimingoo的专栏
aimingoo的专栏
C
CERT Recently Published Vulnerability Notes
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
N
News and Events Feed by Topic
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Scott Helme
Scott Helme
A
About on SuperTechFans
N
Netflix TechBlog - Medium
TaoSecurity Blog
TaoSecurity Blog
V
V2EX
MongoDB | Blog
MongoDB | Blog
AWS News Blog
AWS News Blog
Google DeepMind News
Google DeepMind News
Google Online Security Blog
Google Online Security Blog
O
OpenAI News
Y
Y Combinator Blog
S
Securelist
GbyAI
GbyAI
D
Docker
SecWiki News
SecWiki News
The Hacker News
The Hacker News
有赞技术团队
有赞技术团队
T
Tenable Blog
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
P
Privacy International News Feed
S
Security Affairs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hacker News - Newest:
Hacker News - Newest: "LLM"
H
Hackread – Cybersecurity News, Data Breaches, AI and More

动物的光合作用

使用 Peer Relay 提高 Tailscale 的速度 内网穿透下 NAS 使用 DNS 轮循作为负载均衡 <众安车保>上当指南 - 动物的光合作用 JS 中的 Shebang/Hashbang - 动物的光合作用 我的自建 NAS - 动物的光合作用 React 组件中复用代码的方式 - 动物的光合作用 负载均衡下的前端资源更新策略 - 动物的光合作用 CSS 基于视区的长度单位 - 动物的光合作用 Excalidraw 支持自定义字体 - 动物的光合作用 我用 Docker 部署的一些服务 - 动物的光合作用 CSS 书写模式和逻辑属性 - 动物的光合作用 大量活动页面持续性维护的解决方案 - 动物的光合作用 Mac mini 搭建外网可访问的简易 NAS - 动物的光合作用 如何在 React 解决竞态条件 - 动物的光合作用 使用脚本备份 Github 仓库 - 动物的光合作用 高性能 React 应用的几个小技巧 - 动物的光合作用 复习 DOM 事件 - 动物的光合作用 我的博客架构变迁之路 - 动物的光合作用 浏览器和 JavaScript 的一些新特性 - 动物的光合作用 useCallback 的误区 - 动物的光合作用 在 React 中使用事件分离状态 - 动物的光合作用 如何处理 RESTFUL 数据异常导致的前端错误 - 动物的光合作用 基于 Node.js 的 WebFont 解决方案
React Fragment 添加事件监听? - 动物的光合作用
mebtte · 2022-06-26 · via 动物的光合作用

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 渲染结果会是这个样子:

如果我们给 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 的例子:

Warning: validateDOMNesting
Warning: validateDOMNesting

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

封装组件

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

<div>
  <Aux>
    <div>a</div>
    <span>b</span>
  </Aux>
</div>
/**
 * 输出真实 DOM
 * <div>
 *   <div>a</div>
 *   <span>b</span>
 * </div>
 */

它的原理很简单, 利用 React 自身对 children 合并的处理:

// 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, 毕竟上面的封装也会导致一部分的性能损失.

进一步阅读

使用 Discussions 讨论 Github 上编辑 分享到 X