聊聊React中的隱藏彩蛋功能

卡頌發表於2023-04-25

大家好,我卡頌。

React的程式碼量可以說是相當龐大。在如此龐大的庫中是否存在文件中未提及,但是實際存在的功能呢?

答案是肯定的。

本文將向你介紹3個文件中未提及的隱藏彩蛋功能。

歡迎加入人類高質量前端交流群,帶飛

ref cleanup

在當前React中,Ref存在兩種資料結構:

  1. <T>(instance: T) => void
  2. {current: T}

對於大部分需求,我們會使用第二種資料結構。同時,他也是useRefcreateRef返回的資料結構。

第一種資料結構主要用於DOM監控,比如在下面的例子中,div的尺寸會反映到height狀態中:

function MeasureExample() {
  const [height, setHeight] = useState(0);

  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);

  return (
    <div ref={measuredRef}>Hello 卡頌</div>
  );
}

但在上面的例子中,DOM的尺寸變化無法實時反映到height狀態。為了反映實時變化,需要使用監控DOM的原生API,比如:

這些API通常是事件驅動,這就涉及到當不需要監控後,解綁事件

既然事件繫結是在ref回撥中進行的,很自然的,解綁事件也應該在ref回撥中進行。比如,用ResizeObserver改造上述例子:

function MeasureExample() {
  const [entry, setEntry] = useState();
  
  const measuredRef = useCallback((node) => {
    const observer = new ResizeObserver(([entry]) => {
      setEntry(entry)
    })

    observer.observe(node)
    // 解綁事件
    return () => {
      observer.disconnect()
    }
  }, [])

  return (
    <div ref={measuredRef}>Hello 卡頌</div>
  );
}

在這個場景中,我們希望函式型別的ref可以返回一個新函式,用於解綁事件(類似useEffect回撥的返回值)。

實際上,在19年的#issues 15176中就有人提出這個問題。在去年底的#pull 25686中相關改動已經合併到React main分支。

當前React文件中Ref的部分還未提及這個功能改動。可能在未來的某個小版本中,會上線這個功能。

Module Component

你覺得下面的函式元件能渲染出hello麼:

function Child() {
  return {
    render() {
      return "hello";
    }
  };
}

答案是 —— 可以,見Module Component線上示例

其實這是一種上古時期就存在的元件形式,叫做Module Component(即函式元件返回帶有render屬性的物件)。

當遇到Module ComponentReact會將對應函式元件(上例中的Child元件)轉換為Class Component,後續更新流程與Class Component無異。

Module Component預計會在未來的某個版本被移除(見#pull 15145),所以文件中並未提及。

要說有啥實際作用,沒準可以給你同事帶來點小小困惑......

開啟全域性併發更新

v18中,只有使用併發特性(比如useTransiton)觸發的更新才是併發更新,其他情況觸發的更新都是同步更新。

如何才能不使用併發特性,又能全域性開啟併發更新呢?答案是在專案中加入下面這行咒語:

React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentBatchConfig.transition = {
  帥哥: '卡頌'
};

比如,對於如下例子,渲染一個耗時的列表(每個Item元件render會耗時5ms):

function App() {
  return (
    <ul className="App">
      {Array.from({ length: 100 }).map((_, i) => (
        <Item key={i} num={i}>
          {i}
        </Item>
      ))}
    </ul>
  );
}

function Item({ children }) {
  const cur = performance.now();
  while (performance.now() - cur < 5) {}
  return <li>{children}</li>;
}
併發示例地址

不加上咒語時的渲染火炬圖如下,整個更新流程在一個宏任務中,耗時513ms:

加上咒語時的渲染火炬圖如下,整個更新流程被時間切片,每個切片5ms左右:

咒語為什麼會起作用呢?

ReactReactDOM中都存在變數__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,這個變數的作用是 —— 在不同包之間共享資料。

比如,所有Hook都是從React包中匯出的,但Hook的具體實現在ReactDOM包中。為了在他們之間共享Hook,就需要一個媒介,這就是__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED

類似的,更新相關資料也需要在ReactReactDOM間共享,其中就包括 —— 更新是否是併發更新。

當我們賦值React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentBatchConfig.transition後,React會認為當前更新是併發更新。

透過這種方式,就能全域性開啟併發更新。

當然,我並不建議你隨意更改__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED下的資料,畢竟這個變數的名字還是挺唬人的。

總結

以上便是3個React中的隱藏彩蛋功能。其實除了他們之外,React中還有很多沒有暴露出來的API,比如類似VueKeep-AliveOffscreen Component

當前要想體驗Offscreen Component只能透過Suspense間接體驗(Suspense能夠在pending與掛載元件間切換就是利用Offscreen Component)。

還有什麼你知道的React隱藏功能?歡迎在評論區討論~

相關文章