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

在 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)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code