React 17 中根据 DOM 节点获取 React 组件实例

1,277次阅读
没有评论

共计 2717 个字符,预计需要花费 7 分钟才能阅读完成。

提醒:本文最后更新于2025-07-07 14:38,文中所关联的信息可能已发生改变,请知悉!

在 React 中,通过 ReactDOM.findDomNode 方法可以获取组件实例中 render 方法返回的 DOM 元素。但是如果反过来,想根据 DOM 元素取得组件的实例怎么办?以下代码相信熟悉 React 的同学都见过:

/** 根据 DOM 节点查找其所在的 React 组件实例 */
export function findReactElement(node) {
for (const key in node) {
if (key.startsWith('__reactInternalInstance$') && node[key]._debugOwner) {
return node[key]._debugOwner.stateNode;
}
}
return null;
}

这是在 React 16 中根据 fiber 机制的特点实现的一种取巧方法。但在 React 17 中该方法失效了。简单的调试一下,发现 node 节点属性上的 node._reactInternals.child.stateNode 即是我们想要的,那么修改一下:

/** 根据 DOM 节点查找其所在的 React 组件实例 */
export function findReactElement(node) {
for (const key in node) {
if (key.startsWith('_reactInternals') && node[key].child) {
return node[key].child.stateNode;
}
}
return null;
}

测试一下,效果与预期一致。本以为就这么简单的完成了兼容,可是很快测试小姐姐的 bug 就找上门了,并且标注为了严重级别。。。

模拟测试环境进行调试,发现 node._reactInternals 居然没有了。结合搜索引擎查找和场景模拟分析,原来 _reactInternals 只存在于开发模式中。在生产模式下,发现从 node.__reactFiber$ajyslsr26ui.return.stateNode 上可以找到组件实例。其中以 __reactFiber$ 开头的属性值即为当前 DOM 的 FiberNode 节点。

FiberNode 节点的 typeelementType 属性指明了其节点类型,而 startNode 则指向具体的节点实例。type 属性为字符串则主要表示其 DOM 节点对应的 HTML 元素标签名,为对象则是组件对应的原型。此外,._debugOwner 属性(若存在)则直接指向其所在组件的 FiberNode 节点。于是,根据 FiberNode 节点及其父节点(.return属性)信息向上查找即可找到 DOM 节点所在组件的实例。

我们看一下 ReactDOM 源码里是怎么处理类似查找的:

const randomKey = Math.random()
.toString(36)
.slice(2);
const internalInstanceKey = '__reactFiber$' + randomKey;
const internalContainerInstanceKey = '__reactContainer$' + randomKey;
// 省略无关代码....
/**
* Given a DOM node, return the ReactDOMComponent or ReactDOMTextComponent
* instance, or null if the node was not rendered by this React.
*/
export function getInstanceFromNode(node: Node): Fiber | null {
const inst =
(node: any)[internalInstanceKey] ||
(node: any)[internalContainerInstanceKey];
if (inst) {
if (
inst.tag === HostComponent ||
inst.tag === HostText ||
inst.tag === SuspenseComponent ||
inst.tag === HostRoot
) {
return inst;
} else {
return null;
}
}
return null;
}

以上源码引自:function getInstanceFromNode(node: Node): Fiber | null

ReactDOM 相关源码的实现上可以看出, __reactFiber$ 开头的属性值指向了 FiberNode 实例,而 tag 属性则标识了节点的具体类型。

经过以上分析,可以得出在 React 17 中根据 DOM 节点获取其所在的 React 组件实例的方法,实例如下:

/* 查找组件实例 */
const GetCompFiber = fiber => {
// 也可以根据 fiber.tag 属性查找 -- 暂未作足够的测试验证
// @see https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
// while (![0, 1].includes(fiber.tag)) {
while (typeof fiber.type === 'string') {
fiber = fiber.return;
}
return fiber.stateNode;
}
/** 根据 DOM 节点查找其所在的 React 组件实例 */
export function findReactElement(node) {
if (!node) return null;
for (const key in node) {
if (!node[key]) continue;
// react17
if (key.startsWith('__reactFiber$') || key.startsWith('__reactContainer$')) {
if (node[key]._debugOwner) return node[key]._debugOwner.stateNode;
return GetCompFiber(node[key]);
}
// 兼容 react16
if (key.startsWith('__reactInternalInstance$') && node[key]._debugOwner) {
return node[key]._debugOwner.stateNode;
}
}
return null;
}

最后提示一下,一般来说,获取组件实例的主要目的是为了调用其方法或属性。为了实现这种目的,使用状态管理或简单的事件触发机制是比较合理的方案。根据 DOM 元素获取组件实例是一种非常规的取巧方法,由于不是标准的官方支持方案,在版本升级时可能会因为方案失效而带来一些风险。

正文完
 0
任侠
版权声明:本站原创文章,由 任侠 于2021-09-21发表,共计2717字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码