在 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 节点的 type
或 elementType
属性指明了其节点类型,而 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 元素获取组件实例是一种非常规的取巧方法,由于不是标准的官方支持方案,在版本升级时可能会因为方案失效而带来一些风险。