ReactDOM 在创建 DOM 元素的时候,会将 Fiber 信息也存储到 DOM 节点上(参考之前的文章)。但是需要注意的一点是,从 Fiber 上拿到的 Props 不一定是最新的数据。特别是对于使用 React Hooks 的情况来说,需要额外小心。举例来说:
const App = () => {
const btn = React.useRef(null);
const [value, setValue] = React.useState("");
function onChange(event) {
setValue(event.target.value);
}
function onClick() {
if (!btn.current) return;
const fiberKey = Object.keys(btn.current).filter((key) =>
key.startsWith("__reactInternalInstance")
)[0];
if (!fiberKey) return;
const fiber = btn.current[fiberKey];
console.log(fiber.memoizedProps.onClick.valueInContext);
}
onClick.valueInContext = value;
return (
<form>
<input onChange={onChange} value={value}>
<button
ref={btn}
type="submit"
onClick={onClick}
>
输入内容后点我
</button>
</form>
);
};
在输入框输入一段文字之后点击按钮,可以看到最终在 console 输出的内容并不是完整的输入内容。比如,输入 12345
但是在 console 只输出了 1234
。通过阅读 React 中对于事件处理的代码可以发现,在获取当前监听事件的时候(这里的 getListener
函数),使用了 getFiberCurrentPropsFromNode
这个函数,而这个函数的定义方法在这里。可以看到,使用的是另一个 DOM 节点上的字段:'__reactEventHandlers$' + randomKey
。
因此,上面的代码只需要稍作调整,就可以正常工作了:
const App = () => {
const btn = React.useRef(null);
const [value, setValue] = React.useState("");
function onChange(event) {
setValue(event.target.value);
}
function onClick() {
if (!btn.current) return;
const propsKey = Object.keys(btn.current).filter((key) =>
// ** 主要改动点 **
key.startsWith("__reactEventHandlers")
)[0];
if (!propsKey) return;
const props = btn.current[propsKey];
console.log(props.onClick.valueInContext);
}
onClick.valueInContext = value;
return (
<form>
<input onChange={onChange} value={value}>
<button
ref={btn}
type="submit"
onClick={onClick}
>
输入内容后点我
</button>
</form>
);
};
对于 __reactEventHandlers
字段的更新,可以参考 ReactDOM 给 Reconciler 配置的两个 API:createInstance
和 commitUpdate
(代码在这里)。
当然,对于 Class Component 来说,因为方法一般都是固定的成员方法,因此 __reactEventHandlers
和 __reactInternalInstance
记录下的内容没有什么差别。
另外需要注意的一点是,在 React 最新版本中,__reactInternalInstance$xxx
改名叫 __reactFiber$xxx
;而 __reactEventHandlers$xxx
改名叫 __reactEvents$xxx
。