==========================================
React 正在淪為前端圈的「孔乙己」:脫不下的長衫與失控的基建
React 團隊一到前端圈,所有敲程式的人便都看着他笑,有的叫道:「React,你又悄悄摸摸搞出幾個極其拧巴的拼接怪 API!」
他不回答,對櫃裡說:「加兩套重型編譯器,要一碟 useEffectEvent。」便排出幾個難懂的心智模型。
他們又故意大聲嚷道:「你一定又在底層偷偷搞副作用(Side Effects)了!」
React 睜大眼睛說:「你怎麼這樣憑空污人清白……」
「什麼清白?我前天親眼見你的 useEffect 裡非同步網路請求滿天飛,閉包陷阱套着閉包陷阱,連個最新狀態都拿不到,還在 commit 階段偷偷改 Ref,被全網吊着打。」
React 便漲紅了臉,額上的青筋條條綻出,爭辯道:「非同步……非同步的事怎麼能叫副作用呢!……那叫代數效應(Algebraic Effects)!React 調度器裡的事,能算不純麼?」接連便是些難懂的話,什麼「Fiber 架構」,什麼「並發渲染(Concurrent)」,什麼「UI 是狀態的純函數映射」,引得整個 Web 社群內外充滿了快活的空氣。
你明明一身歷史包袱,底層 DOM 突變和時序補丁糊了一層又一層,還要死死捂着那件打滿補丁的「函數式程式設計」長衫裝清高。你連業務程式碼裡最基本的非同步抓取和狀態流轉都做不到開箱即用,還天天擺這兒給開發者念經,說什麼「要保持純潔,要無副作用」。
既然你端着全球最大前端基建的架子,那我就得用配得上你這份傲慢的嚴苛標準來伺候你。我不聽你那套自欺欺人的八股文,也不陪你玩「心智模型」的文字遊戲。我只負責把你剝個精光,拿着放大鏡逐行扒开你 ReactFiberCommitWork.js 的原始碼,把你裝死關掉的 Issue 挨個掘出來。
所有寫過 React 的人都遇到過這種事:
你寫了一個聊天室元件,連線成功後要跳個提示,提示要用當前的主題色 theme。
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', () => {
showToast('連線成功!', theme) // 這裡用到了 theme
})
connection.connect()
return connection.disconnect
// ...
}, [roomId, theme]) // 🚨 噩夢來了:React 逼你把 theme 加進依賴陣列
}
問題在哪?
你只是想在跳提示時「讀一下」最新的顏色(theme),但因為 React 的閉包機制,你被迫把 theme 寫進依賴陣列。
結果就是:使用者隨便切個深色模式(theme 變了),你的聊天室就會斷線重連一次。這簡直是災難。
React 在 19.2 推出了 useEffectEvent:把「需要最新值、但不想加依賴」的邏輯包起來。
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showToast('連線成功!', theme) // 這裡總能拿到最新的 theme
})
useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', () => {
onConnected()
})
connection.connect()
}, [roomId]) // 🎉 roomId 變了才重連,theme 終於不用加進依賴了
}
聽起來是個好東西。
React 給這個 API 加了一條硬性限制:
"A function wrapped in useEffectEvent can't be called during rendering."
也就是說:這個函式絕對不准在元件渲染過程中呼叫,只能在 Effect 或者事件裡呼叫。
如果你把它傳給子元件,子元件在 render 里呼叫了一下,整個應用直接報錯白屏。
為什麼限制這麼死?
原始碼裡,這個函式的最新值是在 DOM 渲染完之後的 commit 階段才掛到內部 ref 上的(見 ReactFiberCommitWork.js)。渲染的時候它還是個空殼或舊值。官方解決不了這時序問題,於是用一個粗暴的報錯來阻止開發者。
相比之下社群是怎麼做的?
社群早就用 useRef 自己封了一個 useLatestCallback(或等價物):包裝函式引用穩定,呼叫時總是執行目前 ref 裡的最新函式。
沒有任何呼叫時機的限制,想在哪呼叫在哪呼叫——render、effect、事件、傳給子元件,都不會因為「during rendering」被攔截。
詳見我的 useLatestCallback 實作:github.com/beixiyo/rea…
官方無視社群極其好用的作業不抄,偏要自己造一個內部實作極其拗巴、強加各種規則、心智負擔極重的 useEffectEvent。這就是「設計拗巴,文件更拗巴」。
引用:
ReactFiberHooks.js(useEffectEventImpl、isInvalidExecutionContextForEventFunction)、ReactFiberCommitWork.js(commit 時 ref.impl = nextImpl)一句話:Prepack 是 Meta(Facebook)在 2017 年左右搞的一個失敗的 JavaScript 編譯器專案。
當時 Facebook 異想天開:既然 JS 跑得慢,能不能在打包編譯的時候就把能算出來的程式碼提前算好?
比如 let a = 1 + 2 在編譯時直接變成 let a = 3。後來他們試圖把 Prepack 用在 React 上,想在編譯期把元件提前「折疊」優化。
但 JavaScript 太動態了,這個餅根本畫不圓,專案黃了,被官方放棄。
為什麼文章裡要提它?
因為 React 團隊對「編譯器」有一種病態的執念。
當社群都在擁抱 Signal(用執行時的細粒度反應式解決效能問題)時,React 偏不。他們覺得:當年 Prepack 雖然失敗了,但我現在搞個縮小版叫 React Compiler,繼續搞編譯期魔法。
提 Prepack 就是為了扒皮:React 寧可在「編譯期優化」這條曾走過彎路的老樹上吊死,也不肯聽社群的意見去換一套更先進的反應式模型(Signal)。
面對依賴陣列帶來的災難,社群呼籲引入 Signal 這種現代化的細粒度反應式。React 選了什麼?選了他們當年失敗過的「編譯期優化」老路。
從當年胎死腹中的 Prepack,到如今的 React Compiler,官方展現出一種驚人的技術執念:
我們寧可造一個巨型編譯器來強行分析依賴、強行插入快取,也絕不承認「元件級渲染 + 依賴陣列」這個底層模型本身已經落後了。
這不叫優雅的工程演進,這叫為了掩蓋第一代屎山的惡臭,強行噴香水。
引用:
GitHub #27164("feature: make react more reactive (feedback for future)"):
GitHub #31393("React Why Not Consider Support Signals"):
身為被全世界當基建的庫,對「為什麼不做 Signal」這種等級的討論,不在 issue / 部落格 / RFC 裡留下可檢索的、負責任的說明,而是用「去聽我們某次 Conf」打發,這是對社群回饋的輕慢,也是技術傲慢。
其實證明 React 底層不僅能上 Signal、而且能上得極其優雅的鐵證,早就擺在那裡了。
社群掏出的 Preact Signals,直接把執行時細粒度更新的滿分作業拍在了官方臉上。它完美相容現有的元件模型,直接證明了閉包陷阱完全可以靠一套現代化的反應式機制來根治。
最打臉的是:人家一個第三方套件,在根本碰不到你 React 核心原始碼的情況下,都能靠外掛把這套機制跑得明明白白。而你官方握着 Fiber 調度器的生殺大權,卻選擇視而不見,死活說「做不了」。
只能說 React 團隊這幾年確實是寫編譯器寫魔怔了。簡單的執行時解法他們不屑於做,正路不走,非得拉坨大的出來,好像不搞個重型編譯鏈就配不上大廠的 KPI 一樣。
更可笑的是,如果你現在想用 Preact Signals,你會發現它跟官方硬推的 React Compiler 是直接衝突的。
為什麼衝突?因為 React Compiler 根本就不是什么優雅的架構演進,它本質上就是一個極其自負的 AST 大改造插件。你原本乾乾淨淨的程式碼,被它過一遍,AST 樹上全是被它強行塞進去的 useMemo 和快取標記,程式碼執行軌跡完全成了一個黑盒。
搞得這麼抽象,不知道的還以為你在做 JIT。
(如果你也受夠了官方這種強行喂屎的黑盒操作,想看看怎麼在 React 屎山裡自救,詳見我踩坑兩年寫出的血淚總結:《花了兩年用遍了 React 所有狀態管理庫,我選出了最現代化的 Signal 方案》)
引用:
從公開資訊能拼出的「為什麼是 Compiler 而不是 Signal」大致是:
但這些沒有在任何官方部落格、RFC 或上述 issue 裡被系統寫出來。
選型結果就是:Compiler + 更多工具鏈;對 Signal 的態度是:不計畫、不接題、關 issue 時指到 Conf 影片。
技術選型存在,但解釋不透明;社群問「為啥不 Signal」得不到可檢索的、負責任的答覆——這就是技術傲慢:我們怎麼做你們就怎麼用,理由你們自己找。
我可以不用 React,但躲不開。
總有人問我:「既然你這麼懂,自己包一套解法不就行了?幹嘛天天逮着罵?」
說實話,React 這一地雞毛的閉包陷阱、渲染地獄,我早就摸透了,甚至有極其成熟的解法和替代方案。但這不代表我覺得它合理!這種開發模式簡直蠢透了!我一個做業務開發的,憑什麼要天天擱這兒給框架擦屁股?
前端圈一直有股令人作嘔的風氣:「React 孝子」們看不起 Vue 等其他框架。他們把「用 React」當成政治正確,把對 React 的批評當成異端。結果就是爛設計沒人敢往死裡罵,屎山越堆越高。
它不僅折磨老手,更是在新手面前砌起了一堵嘆息之牆。新手滿懷熱情想畫個互動,結果光是搞懂 useEffect 為什麼會死循環、計時器裡的 State 為什麼永遠停留在上個世紀,就得先脫兩層皮。硬生生把一個前端門檻搞得如此畸形、反直覺,官方非但不反思,那一群孝子反而把這種極高的心智負擔當成「技術深度」四處炫耀。把喂屎包裝成「最佳實踐」,這就是你們引以為傲的工程化?
進入 AI 時代,這場災難徹底演變成了賽博瘟疫。全網喂的語料導致現在的 AI 寫前端預設就是 React,十之八九是 JSX + hooks。
我現在日常開發主要靠 AI 寫程式,但面對 React 這個奇葩,即便我在 Prompt 和工作流裡寫了上百行的防坑鐵律嚴防死守,AI 還是會時不時被 React 那套反人類的陰間規則繞暈,悄無聲息地給你拉一坨極其隱蔽的屎。到頭來,我還得停下手中的活,親自下場 Debug,撥開那一層層令人窒息的依賴陣列,去查看到底是底層哪個 Hook 又在發癲。這不是在寫程式,這是在做賽博排雷。整個 Web 社群的程式碼基建正在不可逆轉地走向失控的屎山化。
你以為你不用 React 就能自保?直到有一天,你接一個核心的 SDK,點開文件一看:對不起,只有 React 版本。為了用這一個元件,你被迫在專案裡引入整套 React 執行時,被迫去吃那一套噁心的 Hooks 閉包。這不叫技術選型,這叫強買強賣的生態流氓。
所以,你問我為什麼逮着 React 罵?因為他早已不是一個你可以不用的工具,而是一場避無可避的生態瘟疫。罵 React,不是在罵「一個你可以不用的函式庫」,而是在罵已經失控的基建。
狂熱粉絲只會告訴你 useEffectEvent 怎麼用,而我會翻出 ReactFiberCommitWork.js 的原始碼告訴你它為什麼這麼難用;新手面對被關掉的 Issue 只會覺得是自己提錯了,而我會順著線索找到那場企圖搪塞一切的 React Conf 影片。
我拿着所有的官方日誌、原始碼和 Issue 連結站在這裡,指着這些拗巴的設計說:作為基建,你現在的傲慢、閉門造車和對社群聲音的冷處理,真的很難看。