大家好,我卡頌。
React
的程式碼量可以說是相當龐大。在如此龐大的庫中是否存在文件中未提及,但是實際存在的功能呢?
答案是肯定的。
本文將向你介紹3個文件中未提及的隱藏彩蛋功能。
歡迎加入人類高質量前端交流群,帶飛
ref cleanup
在當前React
中,Ref
存在兩種資料結構:
<T>(instance: T) => void
{current: T}
對於大部分需求,我們會使用第二種資料結構。同時,他也是useRef
、createRef
返回的資料結構。
第一種資料結構主要用於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
,比如:
- ResizeObserver,監控
DOM
尺寸變化 - IntersectionObserver,監控
DOM
可視區域變化 - MutationObserver,監控
DOM
樹變化
這些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 Component
,React
會將對應函式元件(上例中的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左右:
咒語為什麼會起作用呢?
在React
、ReactDOM
中都存在變數__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
。
類似的,更新相關資料也需要在React
與ReactDOM
間共享,其中就包括 —— 更新是否是併發更新。
當我們賦值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
,比如類似Vue
中Keep-Alive
的Offscreen Component
。
當前要想體驗Offscreen Component
只能透過Suspense
間接體驗(Suspense
能夠在pending
與掛載元件間切換就是利用Offscreen Component
)。
還有什麼你知道的React
隱藏功能?歡迎在評論區討論~