Things I Learned (React)

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


React Capture Event🔗

React

React 为了消除不同浏览器上的 Event 差异,设计了一套合成事件(SyntheticEvent)。一般常用的有 onClickonKeyDown 等等。

类似于原生的浏览器事件,React 的合成事件也有捕获和冒泡两个不同的阶段。一般常用的 onClick 是在冒泡阶段的回调函数,对应的捕获阶段的回调函数是 onClickCapture

需要注意的是,React 合成事件的设计,是在顶层元素上捕获事件,然后通过 React 内部的机制生成对应的合成事件,并转发给 React 元素。其中的捕获和冒泡是由 React 自身来维护的。通过下面的例子,可以直观的看到 React 合成事件和原生浏览器的事件之间的执行顺序。

假设有下面一段 HTML 代码:

<div id="not-react-dom-outer">
  <div id ="not-react-dom-inner">
    <div id="app"></div>
  </div>
</div>

以及下面这段配套的 JavaScript 代码:

const outer = document.querySelector('#not-react-dom-outer');
outer.addEventListener('click', function(e) {
  console.log('not-react-dom-outer (bubble)');
}, false);
outer.addEventListener('click', function(e) {
  console.log('not-react-dom-outer (capture)');
}, true);
 
const inner = document.querySelector('#not-react-dom-inner');
inner.addEventListener('click', function(e) {
  console.log('not react div inner (bubble)');
}, false);
inner.addEventListener('click', function(e) {
  console.log('not react div inner (capture)');
}, true);

const Button = () => (
  <div
    onClick={() => console.log('react div (bubble)')}
    onClickCapture={() => console.log('react div (capture)')}
  >
    <button
      onClick={() => console.log('react button (bubble)')}
      onClickCapture={() => console.log('react button (capture)')}
    >
      Click Me
    </button>
  </div>
);

ReactDOM.render(<Button />, document.getElementById('app'));

那么,在点击了 <button> 按钮之后,控制台的输出顺序为:

not-react-dom-outer (capture)
not react div inner (capture)
not react div inner (bubble)
not-react-dom-outer (bubble)
react div (capture)
react button (capture)
react button (bubble)
react div (bubble)

执行顺序上是先完成了从捕获到冒泡的所有原生事件,然后再执行从捕获到冒泡的所有 React 合成事件。

关于合成事件,可以参考官方给出的文档