React 曾經(jīng)以簡潔、靈活的組件化理念征服了無數(shù)前端開發(fā)者,成為構(gòu)建現(xiàn)代 Web 應(yīng)用的事實標(biāo)準(zhǔn)。然而,在它大獲成功的背后,也有越來越多開發(fā)者開始感到困惑,甚至抓狂——這個曾被譽為“前端終極解法”的框架,真的還那么“簡單”嗎?
在這篇長文中,作者以切身經(jīng)歷,從 Angular 的崛起講到 React 的“瘋狂演化”,層層剖析 React 在狀態(tài)管理、架構(gòu)設(shè)計、Hooks 使用等方面所帶來的實際困擾。他并非只是吐槽,而是試圖從更深層的角度理解:為什么我們寫前端會變得這么復(fù)雜?到底是 React 的錯,還是整個 Web 開發(fā)模式本身早就出了問題?
作者 | mbrizic 編譯 | 蘇宓
出品 | CSDN(ID:CSDNnews)
那些年我寫的 Angular
在我還只是個初級程序員、四處接活的日子里,我靠寫 Angular.JS 謀生。那時候,它算是一門非常出色的技術(shù)。絕對是當(dāng)時最大的 JS 框架,更重要的是,它可能是第一個真正意義上的 Web 開發(fā)框架。在那之前,大多都是“庫”(libraries)——提供一堆函數(shù)供你使用;而 Angular 不僅給了你工具,還提供了整個搭建 Web 應(yīng)用的結(jié)構(gòu)和范式。
當(dāng)然,好也只是相對而言。Angular 之所以顯得好,是因為它的前輩們都不行。當(dāng)時也有些單頁應(yīng)用框架,比如 Backbone、Knockout,但它們都沒留下太大印象。真正被 Angular 干翻的對手,其實是 jQuery。
雖然 jQuery 本質(zhì)上只是 HTML DOM API 的一層包裝(而那時候的 DOM API 確實挺糟糕的),但如果你想做個復(fù)雜點的網(wǎng)頁,它幾乎就是事實標(biāo)準(zhǔn)。它的用法非常直接:你在 JS 里手動創(chuàng)建 HTML 元素、控制行為、挪來挪去,靠一堆命令式代碼讓網(wǎng)站變得“像應(yīng)用”。
這對于簡單項目這沒問題,但你可以想象,一旦項目規(guī)模變大,就會變成維護(hù)者的噩夢。而這正是當(dāng)時大家普遍遇到的問題。也不能怪 jQuery,只能說用戶的需求在變,他們想要的交互越來越復(fù)雜,而開發(fā)者只能硬著頭皮用 jQuery 去實現(xiàn)。哪怕它早就不適合干這活了。
這時候 Angular 出現(xiàn)了,一切豁然開朗。
你終于可以專注于編寫 UI 和業(yè)務(wù)邏輯了,而不用再去手動拼 HTML。這真的是一次顛覆性的進(jìn)步:第一次,有了能做“大型”交互式 Web 應(yīng)用的正經(jīng)工具。它帶來的一些“魔法”特性包括:
A)組件化。雖然它的命名有點怪怪的,叫 “directives”,但本質(zhì)就是你可以定義一個 HTML+JS 組合的 UI 片段,然后在多個地方復(fù)用。
B)雙向綁定。你定義一個變量,它一旦發(fā)生變化,界面上所有用到它的地方都會自動更新。這套機制當(dāng)時用起來非常順手。但后來,有人開始挑毛病,說這種“數(shù)據(jù)可以隨處流動”的方式太混亂,于是大家開始推崇“單向數(shù)據(jù)流”(從上往下傳)。聽起來確實更“工程化”,但一落地反而讓開發(fā)變得更復(fù)雜,還引發(fā)了一連串關(guān)于狀態(tài)管理的爭論,最后的結(jié)果就是——我們?nèi)荚谟?Redux 了。
我的第一份工作就是把一個龐大、難以維護(hù)的 jQuery 應(yīng)用重構(gòu)成 Angular 項目,整個過程還算順利,效果也挺不錯。
然而,不怎么美好的是——幾年后我又不得不把那些相同的頁面再用 Angular 2 重寫一遍。幸好我在公司打算第三次用 React 重寫時,及時離職了。
React 登場
后來我有機會接觸了 React,甚至在一兩個項目里真正用了起來。
我還記得第一次看到它時,那種眼前一亮的感覺。當(dāng)時大家還在用的是 Angular 2——它完全重寫了初代 Angular,結(jié)果是模板代碼量翻倍、強制使用 TypeScript、數(shù)據(jù)單向綁定、響應(yīng)式/可觀察者模式……單拎出來都不錯,但組合在一起后,開發(fā)體驗異常復(fù)雜,構(gòu)建速度慢、運行也慢。
React 就像鐘擺一樣,把前端框架搖回了“簡單主義”的方向,大家自然一擁而上。那段時間,React 的確還保持著“簡單”,于是它一路走紅,成了構(gòu)建 SPA(單頁應(yīng)用)的首選庫。
細(xì)心的人可能會發(fā)現(xiàn),我在這里開始稱它為“庫”而不是“框架”了,可見它當(dāng)時確實更輕量。但你光靠一個庫,是不可能搞定完整應(yīng)用的。你得用好幾個庫去填充功能缺口,還得自己制定代碼結(jié)構(gòu)。React 的“自帶啤酒”哲學(xué)就是:你想喝啥都得自己帶——于是你其實在半手工搭一個屬于你自己的“框架”,并承擔(dān)所有由此帶來的后果。
結(jié)果就是——沒有兩個 React 應(yīng)用是完全相同的。每個項目都像是從互聯(lián)網(wǎng)上拼湊了一堆庫、DIY 出來的定制“微型框架”。
我當(dāng)時不幸參與的幾個 React 項目,全都讓我產(chǎn)生了一個念頭:就算是 Angular 2,也比這玩意兒強。JSX 本身沒啥問題,看著也算穩(wěn),但它周圍那一圈東西……完全是一鍋粥。
于是我跑路了,轉(zhuǎn)身去寫 Java 后端,這大概已經(jīng)說明一切。
就在我以為可以逃離的時候……
有人說,人的知識都是“開關(guān)式”的,一共有兩種情況——一是你懂了,另一種就是你沒懂。我顯然屬于后者,因為我最近又把自己拉回了 React。
當(dāng)然,這只是個業(yè)余項目,不像正式上線的產(chǎn)品那樣復(fù)雜,所以我也沒完全體驗到 React 的“全部威力”。但即便如此,這次的使用過程還是不僅驗證了我對它的低預(yù)期,甚至比我想的還要離譜。
React 給人的感覺真的很瘋狂——我不明白為什么沒人在認(rèn)真討論這件事。
架構(gòu)、組件、狀態(tài)
我們先從 React 所“推崇”的架構(gòu)講起。就像前面說的,React 只是一個庫,它并不會強迫你做任何事情,但由于 JSX 的存在,它隱含的限制導(dǎo)致一些模式成為了“默認(rèn)架構(gòu)”。在很久以前,我們常討論 MVC、MVVM、MVP 這些架構(gòu),其實只是同一個套路的不同變體。
那么 React 屬于哪種?
我覺得它都不是,反而是一個全新的范式,可以稱作“組件式架構(gòu)”。
乍一看,這邏輯很清晰。你有組件,組成一個從上往下的樹形結(jié)構(gòu),于是應(yīng)用就搭好了。React 內(nèi)部會幫你處理狀態(tài)更新與 UI 同步,挺簡單的。
但不知從哪一刻開始,這一切就開始變得“過于聰明”了。對于一個自稱“UI 庫”的東西,React 擁有大量復(fù)雜的術(shù)語;而對一個跟函數(shù)式編程毫不相關(guān)的項目來說,它卻偏偏借來了大量函數(shù)式的術(shù)語。
這篇文章中,我們就從“狀態(tài)”開始說吧。如果你有一個自頂向下的組件樹,按理說也應(yīng)該自頂向下傳狀態(tài)。但現(xiàn)實中組件數(shù)量繁多、顆粒度很小,結(jié)果你會花費大量時間和代碼,只為了把某些數(shù)據(jù)“接力傳遞”到需要的地方。
這個問題通過用 React hooks 把狀態(tài)“偷偷塞”進(jìn)組件里解決了。我倒是沒聽誰抱怨過,但你們是認(rèn)真的嗎?你們的意思是——任何組件都可以隨便用整個應(yīng)用里的任何狀態(tài)?更夸張的是,任何組件都能修改狀態(tài),然后影響到其他組件的顯示?
這怎么能通過代碼審查?這根本就是全局變量,只不過加了一層“復(fù)雜一點的修改流程”偽裝。甚至連“流程”都談不上,就是一種儀式感。因為你根本無法防止別人在任何地方隨意變更狀態(tài)。大家真的覺得,只要給這種做法取個聰明名字(比如叫 reducer),它就突然一下變成了好架構(gòu)了嗎?
所以說,如果“自上而下傳遞狀態(tài)”和“旁加載狀態(tài)”這兩種做法都很爛,那到底該怎么解決這個問題呢?說實話,我也沒什么好辦法。唯一能想到的就是:也許我們根本就沒法優(yōu)雅地解決它,那說明整個“組件化架構(gòu)”可能一開始就是個錯誤,我們不該把它捧成所謂的“最佳設(shè)計典范”,然后就停止了探索和創(chuàng)新。也許這次真的輪到再發(fā)明一個新的 JS 框架,試試有沒有更好的思路了。
React Hooks
接下來我們繼續(xù)聊那些“令人懷疑怎么能過審的設(shè)計”——React Hooks。不可否認(rèn),它們很有用,但它們的存在本身讓我一頭霧水。
我甚至懶得吐槽“組件是純函數(shù)”這種說法,因為 Hooks 根本就不是純函數(shù),它們是一個個有狀態(tài)的小黑盒。更糟的是,Hooks 可以組合,你可能堆好幾層,全是黑盒,層層嵌套,狀態(tài)分布四面八方。
不過算了,我主要還是想吐槽一下 useEffect。按理說,“副作用”這個概念應(yīng)該很好理解:你改了個狀態(tài),然后需要做點額外的事,比如發(fā)個網(wǎng)絡(luò)請求。理論上,這種把“核心業(yè)務(wù)邏輯”和“side effect”區(qū)分開的做法,聽起來挺有道理的。可問題是,在實際開發(fā)中,你真的能把它們分得那么清楚嗎?
我最不滿的一點就是——大家把 useEffect 當(dāng)成“組件掛載后執(zhí)行點什么”的工具來用。我知道 React 從類組件轉(zhuǎn)向 Hooks 的時候,useEffect 是最接近 componentDidMount 的替代方案。但這不就是個赤裸裸的“黑魔法”嗎?
你用一個“副作用”的鉤子來初始化組件?如果你只是從里面發(fā)個 API 請求,那確實算是副作用。可問題是,那個 API 請求……它還要更新組件的狀態(tài)。也就是說,一個看起來很無害的 useEffect 副作用鉤子,實際上在管理組件狀態(tài)。沒人覺得這事兒離譜嗎?
更瘋狂的是:如果你還想基于這個狀態(tài)再做點別的操作,你就得……再寫一個 useEffect,依賴前一個 useEffect 設(shè)置的那個狀態(tài)。
這段代碼,來自一家剛被收購、估值數(shù)千萬美元的公司的線上產(chǎn)品。我這里稍作改編,把里面真實的業(yè)務(wù)實體替換成了“house”和“cat”這樣更好理解的例子。你可以先看看,試著猜猜這段代碼到底是按什么順序執(zhí)行的。準(zhǔn)備好了嗎?答案在下面這張圖里:
所以就是這樣——原本可以用簡單命令式代碼寫清楚的狀態(tài)變化流程,現(xiàn)在被拆散成兩個異步函數(shù),而你僅能靠每個函數(shù)底部那個“依賴數(shù)組”來猜它們的執(zhí)行順序。而實際的理解方式,居然是從下往上“反著讀”。
我記得以前大家還在嫌 JavaScript 的 Promise 太繁瑣、then 鏈太多,再往前還有“回調(diào)地獄”……但說真的,不管哪個,都比這清晰!
我理解這類問題也許可以通過兩個方式緩解:
a)抽出去放進(jìn)單獨文件(只是在藏問題),
b)用 Redux 或類似方案重構(gòu)(但我經(jīng)驗有限,不敢下結(jié)論)。
“最佳實踐模式”
以上種種,組合起來看就很丑,完全背離了 React “Hello World” 示例中所承諾的那種簡潔優(yōu)雅。但等等,我還沒吐槽完。
我最近讀了一個熟人寫的博文,標(biāo)題叫《最常見的 React 設(shè)計模式》。原本沒抱太大期望,結(jié)果讀下來還是被震驚到了:就為了在屏幕上渲染一個列表,模式居然可以復(fù)雜成這樣,每一步都要極高的認(rèn)知負(fù)擔(dān)。
最諷刺的是:文章里居然完全沒提“這些寫法是不是太繞了”。這一切復(fù)雜性都被默認(rèn)接受了。看上去大家真的就是這么寫 UI 的,沒人覺得有什么不對勁。
然后,有些人還不滿足,還要寫“CSS-in-JS”,然后還真有人為此發(fā)工資。好吧,我承認(rèn) JSX 的出現(xiàn)打破了“關(guān)注點分離就是文件分離”的迷思,把 HTML 和 JS 寫在一起確實也沒啥問題。但你連 CSS 也硬塞進(jìn)來,還要強類型支持?這是不是有點太過分了?
為什么會變成這樣?
如果我只是簡單地說 React 是“瘋了”,然后拍拍屁股走人,那也太容易了。既然我們是理性的人類,那不如稍微深入一點,試著理解這個問題。
我又回憶起了第一份工作,回到當(dāng)年那個負(fù)責(zé)“從 jQuery 遷移到 Angular”的項目,還有其中一位資深同事。他是那種典型的后端大神,架構(gòu)師級別,對軟件開發(fā)有著深厚理解和威望。
我印象最深的不是他的技術(shù)方案,而是他每次看我們前端代碼時的表情。他看著我們的 Angular 應(yīng)用,總是一臉懵逼:“你們到底在干嘛?為什么要搞得這么復(fù)雜?”
問題不在我們——我們也是一群認(rèn)真做事的開發(fā)者。但在一個傳統(tǒng)后端程序員的眼里,當(dāng)時整個 Angular 生態(tài)看起來就像一場災(zāi)難。
今天,我的年齡大概跟他當(dāng)年差不多,而我也正在寫一篇博客,吐槽 Angular React 有多“瘋”。有些輪回,大概是逃不掉的吧。
但我們試著站得更高一點,理解這背后的本質(zhì)。
首先,我認(rèn)為我們可以達(dá)成一個共識:大多數(shù) Web 應(yīng)用,本就不該做成 Web 應(yīng)用。
很多人一開始不需要用到 SPA,但最終還是選擇 SPA 的方式,因為他們覺得可能以后會用到,所以不如一開始就選 SPA。
但我想說的是,其實你用 React 并不是“沒代價”的。只是我們太習(xí)慣了“默認(rèn)就用單頁應(yīng)用(SPA)”的思維方式,反而忘了更簡單的替代方案有多輕松。比如,用一個傳統(tǒng)的、純粹的服務(wù)端渲染頁面,復(fù)雜度比 React 低太多太多了——沒有前后端通信的額外開銷,前端代碼非常輕量;如果你的后端是強類型的,UI 代碼也可以一起受益;你可以對整個前后端同時做重構(gòu);頁面加載更快;而且因為有些組件對所有用戶都是一樣的,你完全可以只渲染一次,然后緩存起來……這些優(yōu)勢數(shù)都數(shù)不完。
當(dāng)然,你會因此失去那種“產(chǎn)品經(jīng)理一個想法,前端立馬能實現(xiàn)”的靈活交互能力。但這也不完全成立——我認(rèn)為,如果你愿意用點原生 JS 做“漸進(jìn)增強”,可以走得很遠(yuǎn),直到真的復(fù)雜到“非 React 不可”的程度才再引入它。
所以,我說 React 之所以被用,是因為大家過去就已經(jīng)用了。慣性,是種強大的力量。但這仍然不能解釋為什么 React 寫出的代碼,會變得這么不可理喻。
我的答案,其實并不是為了抨擊 React——而是為 Angular、jQuery 乃至所有曾經(jīng)的框架辯護(hù):
因為這類代碼糟糕的根本原因,是 “構(gòu)建一個任意組件都能影響任意其他組件的交互式 UI” 本身,就是軟件工程里最復(fù)雜的事之一。
你回想一下我們?nèi)粘=佑|的其他系統(tǒng):廚房水龍頭有兩個輸入(冷水和熱水),一個輸出(流水);電鉆可能就一個按鈕,影響一個電機;烤箱最多四五個旋鈕,對應(yīng)幾個加熱元件,已經(jīng)足夠復(fù)雜危險了。
但 Web UI 呢?輸入和輸出都可能是無限多個。你怎么可能指望能寫出“干凈的代碼”來支撐它?
所以,我這整篇關(guān)于 React 的吐槽……其實根本不是 React 的錯。也不是 Angular 的錯,更不是 jQuery 的錯。簡單來說,無論你選用哪種技術(shù)棧,最后都不可避免地會面對構(gòu)建交互式 UI 的復(fù)雜性壓垮一切的現(xiàn)實。
那怎么辦?
這個問題怎么解決?我也不是天才,給不出終極方案,但可以隨便聊聊思路。如果我們把網(wǎng)頁看成“輸入”和“輸出”的系統(tǒng),或許可以從源頭上做減法——減少輸入輸出的數(shù)量。
先說“輸入”。是的,我的意思就是:能少放按鈕就少放按鈕。雖然現(xiàn)實中這個事我們常常控制不了,但真的得說清楚:功能越少,代碼越簡單,維護(hù)越容易。
這話聽著像廢話,但真有產(chǎn)品經(jīng)理意識到這點嗎?他們知道一個額外的按鈕,可能會讓維護(hù)成本增加 30%,bug 概率提高 5% 嗎?沒人統(tǒng)計過,但我相信它是真的。
為什么我提議后端引個 Redis,你說“要控制技術(shù)復(fù)雜度”;可當(dāng)產(chǎn)品隨口提了個“全局篩選器,可以影響任意頁面”的需求,你就老老實實寫出了一坨十年都甩不掉的爛代碼?
別再隨便加按鈕了,求你們了。甚至能不能刪掉一些?
再說“輸出”。我寫這篇文章時突然意識到一件事:服務(wù)器端渲染其實就是只保留一個輸出。
用戶每操作一次,就重新渲染整個頁面。聽上去笨,但其實非常純粹:沒有前端狀態(tài)管理,代碼復(fù)雜度直接砍半。如果做得到,真的太值了。
當(dāng)然,完全沒有 JS 不現(xiàn)實,還是得加點交互。但我覺得更合理的方式是:只在最必要的地方,用最小的 JS 代碼。
我甚至想給這種做法起個名字:“交互小島(islands of interactivity)”。一查才發(fā)現(xiàn)這個詞早被用過了,雖然人家說的是 Preact、SSR、manifest 之類的技術(shù),我懷疑講的根本不是一回事。人類總有辦法把簡單事搞復(fù)雜。
但我真心覺得——以我們今天的網(wǎng)絡(luò)帶寬,完全可以在 SSR 頁面里嵌一個小型 React 應(yīng)用,只負(fù)責(zé)一個局部的交互“島嶼”。這種組合,一點也不可怕,甚至我打算下個項目就這么干。
所以,我想嘗試一種未經(jīng)驗證的前端開發(fā)方式:
全站用服務(wù)器渲染,只有必要的交互才引入 React 或其他庫。
不管怎么說,都不會比現(xiàn)在更混亂了。
2025 全球產(chǎn)品經(jīng)理大會
8 月 15–16 日
北京·威斯汀酒店
2025 全球產(chǎn)品經(jīng)理大會將匯聚互聯(lián)網(wǎng)大廠、AI 創(chuàng)業(yè)公司、ToB/ToC 實戰(zhàn)一線的產(chǎn)品人,圍繞產(chǎn)品設(shè)計、用戶體驗、增長運營、智能落地等核心議題,展開 12 大專題分享,洞察趨勢、拆解路徑、對話未來。
更多詳情與報名,請掃碼下方二維碼。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.