Latest Props in React Fiber

React

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:createInstancecommitUpdate(代码在这里)。

当然,对于 Class Component 来说,因为方法一般都是固定的成员方法,因此 __reactEventHandlers__reactInternalInstance 记录下的内容没有什么差别。

另外需要注意的一点是,在 React 最新版本中,__reactInternalInstance$xxx 改名叫 __reactFiber$xxx;而 __reactEventHandlers$xxx 改名叫 __reactEvents$xxx