聊聊React中的隱藏彩蛋功能
大家好,我卡頌。
React
的代碼量可以說(shuō)是相當(dāng)龐大。在如此龐大的庫(kù)中是否存在「文檔中未提及,但是實(shí)際存在的功能」呢?
答案是肯定的。
本文將向你介紹3個(gè)文檔中未提及的隱藏彩蛋功能。
ref cleanup
在當(dāng)前React中,Ref存在兩種數(shù)據(jù)結(jié)構(gòu):
- <T>(instance: T) => void
- {current: T}
對(duì)于大部分需求,我們會(huì)使用第二種數(shù)據(jù)結(jié)構(gòu)。同時(shí),他也是useRef、createRef返回的數(shù)據(jù)結(jié)構(gòu)。
第一種數(shù)據(jù)結(jié)構(gòu)主要用于DOM監(jiān)控,比如在下面的例子中,div的尺寸會(huì)反映到height狀態(tài)中:
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的尺寸變化無(wú)法實(shí)時(shí)反映到height狀態(tài)。為了反映實(shí)時(shí)變化,需要使用監(jiān)控DOM的原生API,比如:
- ResizeObserver[1],監(jiān)控DOM尺寸變化。
- IntersectionObserver[2],監(jiān)控DOM可視區(qū)域變化。
- MutationObserver[3],監(jiān)控DOM樹(shù)變化。
這些API通常是事件驅(qū)動(dòng),這就涉及到「當(dāng)不需要監(jiān)控后,解綁事件」。
既然事件綁定是在ref回調(diào)中進(jìn)行的,很自然的,解綁事件也應(yīng)該在ref回調(diào)中進(jìn)行。比如,用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>
);
}
在這個(gè)場(chǎng)景中,我們希望函數(shù)類型的ref可以返回一個(gè)新函數(shù),用于解綁事件(類似useEffect回調(diào)的返回值)。
實(shí)際上,在19年的#issues 15176[4]中就有人提出這個(gè)問(wèn)題。在去年底的#pull 25686[5]中相關(guān)改動(dòng)已經(jīng)合并到React main分支。
當(dāng)前React文檔中Ref的部分還未提及這個(gè)功能改動(dòng)。可能在未來(lái)的某個(gè)小版本中,會(huì)上線這個(gè)功能。
Module Component
你覺(jué)得下面的函數(shù)組件能渲染出「hello」么:
function Child() {
return {
render() {
return "hello";
}
};
}
答案是 —— 可以,見(jiàn)Module Component在線示例[6]。
其實(shí)這是一種上古時(shí)期就存在的組件形式,叫做Module Component(即函數(shù)組件返回帶有render屬性的對(duì)象)。
當(dāng)遇到Module Component,React會(huì)將對(duì)應(yīng)函數(shù)組件(上例中的Child組件)轉(zhuǎn)換為Class Component,后續(xù)更新流程與Class Component無(wú)異。
Module Component預(yù)計(jì)會(huì)在未來(lái)的某個(gè)版本被移除(見(jiàn)#pull 15145[7]),所以文檔中并未提及。
要說(shuō)有啥實(shí)際作用,沒(méi)準(zhǔn)可以給你同事帶來(lái)點(diǎn)小小困惑......
開(kāi)啟全局并發(fā)更新
在v18中,只有使用并發(fā)特性(比如useTransiton)觸發(fā)的更新才是并發(fā)更新,其他情況觸發(fā)的更新都是同步更新。
如何才能不使用并發(fā)特性,又能全局開(kāi)啟并發(fā)更新呢?答案是在項(xiàng)目中加入下面這行咒語(yǔ):
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentBatchConfig.transition = {
帥哥: '卡頌'
};
比如,對(duì)于如下例子,渲染一個(gè)耗時(shí)的列表(每個(gè)Item組件render會(huì)耗時(shí)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>;
}
并發(fā)示例地址[8]。
不加上咒語(yǔ)時(shí)的渲染火炬圖如下,整個(gè)更新流程在一個(gè)宏任務(wù)中,耗時(shí)513ms:
加上咒語(yǔ)時(shí)的渲染火炬圖如下,整個(gè)更新流程被時(shí)間切片,每個(gè)切片5ms左右:
咒語(yǔ)為什么會(huì)起作用呢?
在React、ReactDOM中都存在變量__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,這個(gè)變量的作用是 —— 在不同包之間共享數(shù)據(jù)。
比如,所有Hook都是從React包中導(dǎo)出的,但Hook的具體實(shí)現(xiàn)在ReactDOM包中。為了在他們之間共享Hook,就需要一個(gè)媒介,這就是__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED。
類似的,「更新」相關(guān)數(shù)據(jù)也需要在React與ReactDOM間共享,其中就包括 —— 更新是否是并發(fā)更新。
當(dāng)我們賦值React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentBatchConfig.transition后,React會(huì)認(rèn)為當(dāng)前更新是并發(fā)更新。
通過(guò)這種方式,就能全局開(kāi)啟并發(fā)更新。
當(dāng)然,我并不建議你隨意更改__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED下的數(shù)據(jù),畢竟這個(gè)變量的名字還是挺唬人的。
總結(jié)
以上便是3個(gè)React中的隱藏彩蛋功能。其實(shí)除了他們之外,React中還有很多沒(méi)有暴露出來(lái)的API,比如類似Vue中Keep-Alive的Offscreen Component。
當(dāng)前要想體驗(yàn)Offscreen Component只能通過(guò)Suspense間接體驗(yàn)(Suspense能夠在pending與掛載組件間切換就是利用Offscreen Component)。
還有什么你知道的React隱藏功能?
參考資料
[1]ResizeObserver:https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver。
[2]IntersectionObserver:https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver。
[3]MutationObserver:https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver。
[4]#issues 15176:https://github.com/facebook/react/issues/15176。
[5]#pull 25686:https://github.com/facebook/react/pull/25686。
[6]Module Component在線示例:https://codesandbox.io/s/boring-proskuriakova-u4pzyl?file=/src/App.js。
[7]#pull 15145:https://github.com/facebook/react/pull/15145。
[8]并發(fā)示例地址:https://codesandbox.io/s/zen-ben-nldijx?file=/src/index.js。