-
Notifications
You must be signed in to change notification settings - Fork 45
解决findDOMNode 过期警告问题 #215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
解决findDOMNode 过期警告问题 #215
Changes from 6 commits
356a547
1080adb
e4eec9e
b757319
2638094
d7f9ac4
126f66e
551e612
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| }; | ||
|
|
||
| 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; | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion MutationObserver 回调中未对 MutationRecord 的 target 做更多限制。
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里只关系节点的移除和新增 属性变化不关系 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @robertpanvip,明白了,感谢你的说明。仅基于节点的增删场景即可满足当前需求,如果以后需要处理属性或其它更复杂的变动逻辑,我们可以再进行细化。 ✏️ Learnings added
|
||
| // 监听子节点的变动(但不监听属性变化) | ||
| 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; // 导出组件 | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
对 document.createElement 的黑客式重写需谨慎。
重写全局创建元素的方法,会影响整份代码中其他组件或第三方库对 document.createElement 的使用,可能在将来带来意外的兼容性或维护挑战。建议在局部范围内封装或使用更小的自定义逻辑,而非覆盖全局。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 最好不要污染原生方法。
There was a problem hiding this comment.
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方法。这样既能保留在组件局部创建注释节点的需求,也能使项目保持纯净,减少维护成本和不可预期的风险。There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai 这个文件如果不污染 document.createElement,又要实现同样的效果,可以怎么写?
There was a problem hiding this comment.
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 字段。
示例实现如下:
如上做法不会污染全局的
document.createElement,也能在最终的 DOM 中实现注释节点。原理就是先用一个占位 DOM,让 React 正常挂载。然后在挂载完成后再通过原生 DOM API 创建注释节点并将其替换。这样你就能在组件内局部使用注释节点,而无需对全局进行 Hack。There was a problem hiding this comment.
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 才不会报错 实现方法有点诡异
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.