Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/SingleObserver/HTMLComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from "react";

export interface CommentProps {
data?: string;
}

type ElementLike = {
setAttribute: () => boolean;
style: object;
};

const createElement = document.createElement;
const TagSymbol = `comment__`;

//react 内部是用这个创建节点的 由于react本身不支持创建注释节点 这里hack一下
document.createElement = function (
tagName: string,
options?: ElementCreationOptions
) {
if (
tagName === "noscript" &&
options?.is &&
options.is.startsWith(TagSymbol)
) {
const regex = new RegExp(`^${TagSymbol}(.*)$`);
const match = options?.is.match(regex);
if (match) {
const data = match[1].trim?.();
const comment = document.createComment(data) as unknown as ElementLike;
comment.setAttribute = () => true;
comment.style = {};
return comment as unknown as HTMLElement;
}
}
return createElement.call(this, tagName, options);
};
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

对 document.createElement 的黑客式重写需谨慎。
重写全局创建元素的方法,会影响整份代码中其他组件或第三方库对 document.createElement 的使用,可能在将来带来意外的兼容性或维护挑战。建议在局部范围内封装或使用更小的自定义逻辑,而非覆盖全局。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 最好不要污染原生方法。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afc163 感谢提醒!在生产环境中全局覆盖 document.createElement 的确存在潜在风险,尤其是在大型项目或使用第三方库时,都可能遇到不可预期的问题。为避免污染原生方法,一个更稳妥的做法是只在组件内处理想要生成注释节点的逻辑,比如通过自定义函数或自定义渲染逻辑直接调用 document.createComment,而不是覆盖全局的 createElement 方法。这样既能保留在组件局部创建注释节点的需求,也能使项目保持纯净,减少维护成本和不可预期的风险。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai 这个文件如果不污染 document.createElement,又要实现同样的效果,可以怎么写?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@afc163 可以考虑在组件内部直接生成并替换一个占位节点,而不是全局覆盖 document.createElement。下面是一个示例思路:

• 在渲染时先返回一个普通的元素(例如一个空的 ),提供给 React 以便正常挂载。
• 在组件挂载后(useEffect 内),创建注释节点并替换掉之前渲染的占位元素。
• 如果需要更新注释内容,可在依赖更新后修改注释节点的 data 字段。

示例实现如下:

import React from "react";

export interface CommentProps {
  data?: string;
}

function HTMLComment({ data }: CommentProps) {
  const placeholderRef = React.useRef<HTMLSpanElement | null>(null);
  const commentNodeRef = React.useRef<Comment | null>(null);

  React.useEffect(() => {
    // 如果尚未创建过注释节点,则创建并替换掉占位节点
    if (!commentNodeRef.current) {
      const node = document.createComment(data || "");
      commentNodeRef.current = node;
      if (placeholderRef.current?.parentNode) {
        placeholderRef.current.parentNode.replaceChild(node, placeholderRef.current);
      }
    } else {
      // 已有注释节点时,更新其内容
      commentNodeRef.current.data = data || "";
    }
  }, [data]);

  // 返回一个空的占位元素,便于挂载
  return <span ref={placeholderRef} style={{ display: "none" }} />;
}

export default React.memo(HTMLComment);

如上做法不会污染全局的 document.createElement,也能在最终的 DOM 中实现注释节点。原理就是先用一个占位 DOM,让 React 正常挂载。然后在挂载完成后再通过原生 DOM API 创建注释节点并将其替换。这样你就能在组件内局部使用注释节点,而无需对全局进行 Hack。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这种写法我试过 react17和react18 会有差异 react17 需要去重写placeholderRef.ccurent 节点的appendChild,replaceChild 方法 react 17 才不会报错 实现方法有点诡异

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


function CommentRender(
{ data = "" }: CommentProps,
ref: React.ForwardedRef<null | Comment>
) {
return (
<noscript
ref={ref as React.ForwardedRef<null | HTMLMetaElement>}
is={`${TagSymbol}${data}`}
/>
);
}

/**支持在react中生成注释节点*/
const HTMLComment = React.memo(
React.forwardRef(CommentRender),
(prevProps, next) => prevProps.data === next.data
);

HTMLComment.displayName = "Comment"; // 设置组件的 displayName,方便调试

export default HTMLComment;
94 changes: 94 additions & 0 deletions src/SingleObserver/Ref.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as React from "react";
import HTMLComment from "./HTMLComment";

export interface RefProps extends React.HTMLAttributes<HTMLDivElement> {}

function updateRef(
node: Element | Text | null,
ref: React.ForwardedRef<Element | Text | null>
) {
if (typeof ref === "function") {
ref(node);
} else if (ref) {
ref.current = node;
}
}

const RefRender: React.ForwardRefRenderFunction<
Element | null | Text,
RefProps
> = (props, ref) => {
const commentRef = React.useRef<Comment | null>(null);
const commentEndRef = React.useRef<Comment | null>();
const contentsRef = React.useRef<Text | Element | null>();

// 根据注释节点获取目标内容节点
const resolveContent = (ele: Comment | null) => {
if (ele === null) {
return null;
}
const current = ele.nextSibling!;
if (current !== commentEndRef.current) {
return current as Text | Element | null;
}
return null;
};

// 重新分配内容节点的 ref
const assignRef = () => {
contentsRef.current = resolveContent(commentRef.current!);
updateRef(contentsRef.current, ref); // 当 DOM 变化时更新 ref
};

React.useLayoutEffect(() => {
const parent = commentRef.current!.parentNode;
if (!parent) return () => 0;

// 创建 `MutationObserver`,监听 DOM 子节点变化
const ob = new MutationObserver((mutations) => {
if (parent) {
mutations.forEach((m) => {
m.removedNodes.forEach((node) => {
if (node === contentsRef.current) {
updateRef(null, ref); // 当 DOM 变化时更新 ref
contentsRef.current = null;
}
});
m.addedNodes.forEach(() => {
assignRef();
});
});
}
});
Comment on lines +47 to +62
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

MutationObserver 回调中未对 MutationRecord 的 target 做更多限制。
目前仅基于 removedNodes 和 addedNodes 判断。如果后续要处理属性突变(attributes: true)或结构更复杂的变动,需要进一步细化判断逻辑。

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里只关系节点的移除和新增 属性变化不关系

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robertpanvip,明白了,感谢你的说明。仅基于节点的增删场景即可满足当前需求,如果以后需要处理属性或其它更复杂的变动逻辑,我们可以再进行细化。


✏️ Learnings added
Learnt from: robertpanvip
PR: react-component/resize-observer#215
File: src/SingleObserver/Ref.tsx:47-62
Timestamp: 2024-12-19T07:27:01.427Z
Learning: 在 Ref.tsx 中的 MutationObserver 回调中,仅关心节点的增删(removedNodes 和 addedNodes),不处理属性变化。

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

// 监听子节点的变动(但不监听属性变化)
ob.observe(parent!, {
childList: true,
subtree: false,
attributes: false,
});

return () => {
ob.disconnect(); // 组件卸载时断开观察器
};
}, [ref]);
return (
<>
<HTMLComment ref={commentRef} data="Ref" />
{props.children}
<HTMLComment
ref={(ref) => {
commentEndRef.current = ref;
// 初次分配 ref 这时候commentRef 肯定已经有值了
assignRef();
}}
data="Ref"
/>
</>
);
};

// 使用 `forwardRef` 将 RefRender 包装成带 ref 的组件
const Ref = React.forwardRef(RefRender);

Ref.displayName = "Ref"; // 设置组件的 displayName,方便调试
export default Ref; // 导出组件
19 changes: 10 additions & 9 deletions src/SingleObserver/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react';
import type { ResizeObserverProps } from '..';
import { CollectionContext } from '../Collection';
import { observe, unobserve } from '../utils/observerUtil';
import DomWrapper from './DomWrapper';
import DomRef from './Ref';

export interface SingleObserverProps extends ResizeObserverProps {
children: React.ReactElement | ((ref: React.RefObject<Element>) => React.ReactElement);
Expand All @@ -13,7 +13,7 @@ export interface SingleObserverProps extends ResizeObserverProps {
function SingleObserver(props: SingleObserverProps, ref: React.Ref<HTMLElement>) {
const { children, disabled } = props;
const elementRef = React.useRef<Element>(null);
const wrapperRef = React.useRef<DomWrapper>(null);
const wrapperRef = React.useRef<Element|Text|null>(null);

const onCollectionResize = React.useContext(CollectionContext);

Expand Down Expand Up @@ -42,7 +42,7 @@ function SingleObserver(props: SingleObserverProps, ref: React.Ref<HTMLElement>)
(elementRef.current && typeof elementRef.current === 'object'
? findDOMNode<HTMLElement>((elementRef.current as any)?.nativeElement)
: null) ||
findDOMNode<HTMLElement>(wrapperRef.current);
wrapperRef.current;

React.useImperativeHandle(ref, () => getDom());

Expand Down Expand Up @@ -108,14 +108,15 @@ function SingleObserver(props: SingleObserverProps, ref: React.Ref<HTMLElement>)
}, [elementRef.current, disabled]);

// ============================ Render ============================
return (
<DomWrapper ref={wrapperRef}>
{canRef
? React.cloneElement(mergedChildren as any, {
if(canRef){
return React.cloneElement(mergedChildren as any, {
ref: mergedRef,
})
: mergedChildren}
</DomWrapper>
}
return (
<DomRef ref={wrapperRef}>
{ mergedChildren}
</DomRef>
);
}

Expand Down