雙向綁定與單向數(shù)據(jù)流之爭(zhēng),Solid會(huì)取代React嗎
現(xiàn)在有一種觀點(diǎn)聲音逐漸大了起來(lái),認(rèn)為市面上出現(xiàn)了許多比 React 性能更好的框架,是不是意味著,React 將要被淘汰了?
所以有人就在群里問(wèn)我,他覺(jué)得 Solid.js 性能比 React 更好,以后會(huì)不會(huì)取代 React?
談?wù)勎业目捶ǎ瑏?lái)做一個(gè)深入一點(diǎn)的分析。
先說(shuō)結(jié)論:Solid.js 要取代 React 很難。
雙向綁定
雙向綁定的概念并非一個(gè)新的詞,因此對(duì)應(yīng)的解決方案 Signal ,也并非一個(gè)新的技術(shù)方案,他比 react 的存在要早得多。
Signal 是一個(gè)傳統(tǒng)技術(shù)方案。
恰恰相反,單向數(shù)據(jù)流反而是一種技術(shù)創(chuàng)新。
在雙向綁定的建立過(guò)程中,有一個(gè)理想的結(jié)果:我們可以輕易的知道數(shù)據(jù)與 DOM 節(jié)點(diǎn)的對(duì)應(yīng)關(guān)系。如果這個(gè)理想的結(jié)果能夠輕松達(dá)成,那么通過(guò)數(shù)據(jù)驅(qū)動(dòng) UI 的形式來(lái)開(kāi)發(fā)代碼將會(huì)變得非常容易。
但是真實(shí)情況是,數(shù)據(jù)與 UI 的對(duì)應(yīng)關(guān)系很難建立。
雙向綁定采取的措施是遞歸遍歷監(jiān)聽(tīng)所有數(shù)據(jù),依次建立與對(duì)應(yīng) UI 的綁定關(guān)系。這種解決方案所花費(fèi)的成本主要體現(xiàn)在對(duì)數(shù)據(jù)的處理上。
他面臨兩個(gè)問(wèn)題。
一是數(shù)據(jù)的變化需要監(jiān)聽(tīng),但是某些數(shù)據(jù)類型的監(jiān)聽(tīng)在實(shí)現(xiàn)上有難度。
以數(shù)組為例,在以前的 vue 版本中,Object.defineProperty() 因?yàn)闊o(wú)法監(jiān)聽(tīng)數(shù)組長(zhǎng)度的變化,導(dǎo)致 vue 不得不重寫(xiě)數(shù)組方法
即便如此,由于改變數(shù)組內(nèi)容的方式實(shí)在有點(diǎn)多,要把數(shù)組設(shè)計(jì)成響應(yīng)式數(shù)據(jù)反而會(huì)導(dǎo)致更多的性能損耗。因此 Vue 不得不提供更多的其他的方式來(lái)監(jiān)聽(tīng)數(shù)組的變化。
比如 forceUpdate,比如大量的 Watcher,還有性能損耗更嚴(yán)重的 Deep Watcher。
另一個(gè)問(wèn)題就是數(shù)據(jù)的層級(jí)與變化問(wèn)題。
數(shù)據(jù)層級(jí)越深,我們想要深度監(jiān)聽(tīng),就得使用遞歸的方式。當(dāng)數(shù)據(jù)發(fā)生變化時(shí),部分?jǐn)?shù)據(jù)與 UI 的綁定關(guān)系需要重新建立「在 vue 中,就是重復(fù)依賴收集的過(guò)程」,如果數(shù)據(jù)量過(guò)大,或者數(shù)據(jù)變化頻繁,就會(huì)有性能風(fēng)險(xiǎn)。
因此 vue 官方文檔也會(huì)建議大家簡(jiǎn)化數(shù)據(jù)層級(jí),減少深度監(jiān)聽(tīng)的成本。
基于這兩個(gè)原因,導(dǎo)致了 vue1.x 的時(shí)候,不敢過(guò)于大聲宣稱自己性能更好。
為什么我要說(shuō) React 的解決方案是一種創(chuàng)新呢?
原因是他打破了傳統(tǒng)的雙向綁定監(jiān)聽(tīng)數(shù)據(jù)的思路,放棄關(guān)注數(shù)據(jù),從而繞開(kāi)了上面的問(wèn)題。
react 把所有的精力都放在了 UI 層。使用我們現(xiàn)在熟知的 diff 算法,當(dāng)數(shù)據(jù)發(fā)生變化時(shí),react 會(huì)創(chuàng)建一個(gè)新的虛擬DOM樹(shù),與之前的樹(shù)做對(duì)比,找出需要改變的元素。
這樣的好處就是完美繞開(kāi)了所有的數(shù)據(jù)類型、數(shù)據(jù)復(fù)雜度、依賴收集等一系列問(wèn)題,react 不僅不用頭疼某些類型監(jiān)聽(tīng)不到,也不需要擔(dān)心數(shù)據(jù)量太大導(dǎo)致更多的性能問(wèn)題。
因此在 vue2.x 的版本中,也部分借鑒了虛擬DOM的解題思路來(lái)緩解 1.x 在數(shù)據(jù)側(cè)的壓力。
從總體思路上來(lái)說(shuō),vue 的主要壓力在于處理數(shù)據(jù),react 的主要壓力在于處理 UI。
react 不建立數(shù)據(jù)與 UI 的對(duì)應(yīng)關(guān)系,那么也就意味著另外一個(gè)壓力的產(chǎn)生,那就是當(dāng)數(shù)據(jù)發(fā)生變化時(shí),react 并不知道哪一個(gè) UI 發(fā)生了變化,于此同時(shí) react 為了保持自己對(duì)于 JavaScript 的弱侵入性,也沒(méi)有在 setState 上進(jìn)行任何魔改,例如綁定當(dāng)前上下文從而得知具體哪個(gè)組件的 state 發(fā)生了變化。
如果進(jìn)行了這個(gè)魔改,diff 的壓力會(huì)小一些。
因此,每一次的 state 變化,都是整棵 DOM 樹(shù)的 diff,這也成為了現(xiàn)在其他框架在輿論宣傳上攻擊 react 性能不好的重要手段和依據(jù),也是許多人覺(jué)得 react 必將被取代的重要原因。
從解決方案來(lái)說(shuō),雙向綁定方案「例如 vue,solid」的努力方向,在于如何降低數(shù)據(jù)側(cè)的性能壓力。
而 react 的努力方向,在于如何減少 UI diff 的性能壓力。
Solid 的底氣在哪?
后來(lái) Vue3 宣傳自己性能高于 react 的聲音逐漸開(kāi)始大起來(lái)了。原因就在于 Proxy 的出現(xiàn)。
defineProperty 在監(jiān)聽(tīng)數(shù)據(jù)上有不少缺陷,因此基于此來(lái)實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)壓力確實(shí)很大,也會(huì)給使用者在數(shù)據(jù)側(cè)帶來(lái)不小的心智負(fù)擔(dān)。而 Proxy 在很大程度上解決了這個(gè)問(wèn)題。
Proxy 能夠監(jiān)聽(tīng)數(shù)組的變化,能夠監(jiān)聽(tīng)刪除對(duì)象字段的變化... 于是 Vue3 的底層實(shí)現(xiàn),在數(shù)據(jù)側(cè)的代碼會(huì)簡(jiǎn)潔很多,并且與此同時(shí),Vue 的后續(xù)版本,也可以徹底放棄虛擬 DOM 來(lái)進(jìn)一步提高自己的運(yùn)行性能。
因此,基于 Proxy 來(lái)實(shí)現(xiàn)雙向綁定成為了許多框架的選擇。這也使得許多框架有了冒頭的理由和機(jī)會(huì),Solid 的底氣也來(lái)自于此。
但是,依然有一個(gè)問(wèn)題沒(méi)有解決。
那就是深度監(jiān)聽(tīng)仍然需要遞歸。當(dāng)數(shù)據(jù)量很大的時(shí)候,依賴追蹤的壓力也會(huì)逐漸變大。
當(dāng)你的項(xiàng)目比較輕量的時(shí)候,你能夠獲得很強(qiáng)的性能體驗(yàn)。但是當(dāng)你的項(xiàng)目變得越來(lái)越大,全局?jǐn)?shù)據(jù)變得越來(lái)越復(fù)雜,層級(jí)越來(lái)越深,他的性能壓力也會(huì)逐漸變大。
當(dāng)然,通常情況下,我們的大多數(shù)項(xiàng)目也達(dá)不到這個(gè)級(jí)別。
React 項(xiàng)目的性能表現(xiàn)
React 的性能壓力主要來(lái)源于 UI 側(cè)的 diff。
當(dāng)項(xiàng)目變得越來(lái)越大,全局狀態(tài)里的數(shù)據(jù)越來(lái)越復(fù)雜。UI 側(cè)的 diff 壓力會(huì)越變?cè)酱髥幔?/p>
答案是:不會(huì)。
這是一個(gè)很有意思的思考。假如我有一個(gè)超大型的項(xiàng)目,一共有 3000 個(gè)頁(yè)面,似乎從理論的角度來(lái)說(shuō),UI diff 的壓力也會(huì)增加到 3000 個(gè)頁(yè)面的量級(jí),但是事實(shí)上我們永遠(yuǎn)只會(huì)在可視區(qū)域里展示一個(gè)頁(yè)面。也就是說(shuō),就算你的項(xiàng)目體量非常大,但是我們只會(huì)渲染一個(gè)頁(yè)面。
虛擬 DOM 的 diff 壓力,也只會(huì)限制在一個(gè)頁(yè)面的量級(jí),這個(gè)壓力不會(huì)隨著項(xiàng)目體量的增加而增加。這個(gè)前提,實(shí)際上就已經(jīng)表明了 React 的性能不會(huì)差到哪里去。
因此在實(shí)踐中,其實(shí)我們也不太需要過(guò)多的關(guān)注 react 的性能問(wèn)題,哪怕是在 Fiber 架構(gòu)出來(lái)之前,也不需要過(guò)多的關(guān)注。
而有意思的是,在許多文章中為了體現(xiàn) solid.js 擁有巨大的性能優(yōu)勢(shì),往往會(huì)構(gòu)建一個(gè)實(shí)踐中幾乎不存在的場(chǎng)景,例如渲染一萬(wàn)個(gè)數(shù)據(jù)的列表比比誰(shuí)花的時(shí)間更少。
然而事實(shí)上,即使我們不使用任何框架,就用原生 JavaScript 來(lái)渲染一萬(wàn)條數(shù)據(jù),也會(huì)采用虛擬列表的方式進(jìn)行優(yōu)化才能確保相對(duì)流暢不卡頓。
我們一定要明白,任何框架的性能都是不可能突破原生 JavaScript 的。
react 性能瓶頸
高頻率的交互往往會(huì)導(dǎo)致明顯的性能問(wèn)題。
例如表單輸入時(shí),我們期望內(nèi)容的任何變化都有對(duì)應(yīng)的 UI 響應(yīng),實(shí)踐項(xiàng)目中容易出現(xiàn)明顯的卡頓和延遲。常規(guī)的優(yōu)化手段是使用防抖。
當(dāng)然,在 antd 的 Form 組件也使用了將數(shù)據(jù)下放到每一個(gè) Item 的方式來(lái)優(yōu)化性能,store 中用 useRef 存儲(chǔ)數(shù)據(jù)而不是 useState,antd 內(nèi)部為每個(gè) Form.Item 定義了 forceUpdate 來(lái)強(qiáng)制更新 Item UI。
又例如拖拽/resize等事件。此時(shí)我們只需要通過(guò)操作原生 DOM 的方式來(lái)實(shí)現(xiàn)對(duì)應(yīng)的邏輯即可。從而繞開(kāi)高頻率的 diff 邏輯
這些性能瓶頸,大概率在 vue 和 Solid 中,也會(huì)存在。解決問(wèn)題的思路也相差不大。
事實(shí)上,原生 DOM 本身在高頻交互上也存在明顯的性能瓶頸。因此許多前端項(xiàng)目不得不采用拋棄 DOM 渲染的方式來(lái)完成整個(gè)項(xiàng)目。但是這些項(xiàng)目我們?nèi)匀豢梢越Y(jié)合 react 來(lái)完成,例如著名的前端項(xiàng)目 Figma,或者國(guó)內(nèi)有的團(tuán)隊(duì)使用 react + skia 的方式來(lái)完成一些對(duì)性能要求很高的項(xiàng)目。
一個(gè)好的思路是,不要試圖用框架解決所有事情,而是讓他解決他擅長(zhǎng)的事情。
小痛點(diǎn)
在使用 vue 時(shí),我們常常需要警惕對(duì)數(shù)據(jù)進(jìn)行一些操作時(shí),讓數(shù)據(jù)失去響應(yīng)性。在 Solid 中同樣如此。
Solid 的官方文檔案例中有這樣一段代碼。
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(
() => setCount(c => c + 1),
1000
);
onCleanup(() => clearInterval(interval));
return <div>Count value is {count()}</div>;
};
當(dāng)我們使用 createSignal 定義了一個(gè)響應(yīng)式數(shù)據(jù)時(shí),此時(shí)返回的 count,并不是一個(gè)數(shù)據(jù),而是一個(gè)獲取數(shù)據(jù)的方法,注意這種差別。
在使用中,我們必須以執(zhí)行該方法的形式來(lái)當(dāng)成數(shù)據(jù)使用。如果 JSX 中有多處使用了該數(shù)據(jù),我們也必須以執(zhí)行該方法的形式來(lái)當(dāng)成數(shù)據(jù)使用,count() 而不是 count。
如果我使用一個(gè)變量來(lái)緩存他的執(zhí)行結(jié)果,然后使用該變量嵌入 JSX 中使用,該數(shù)據(jù)就會(huì)失去響應(yīng)性。
var c = count()
// 失去響應(yīng)性
<div>Count value is {c}</div>;
這里存在的問(wèn)題就是語(yǔ)義與語(yǔ)法的錯(cuò)位,讓我覺(jué)得不太舒服。
vue3 實(shí)際上也存在類似的問(wèn)題,他為了避免這種語(yǔ)義與語(yǔ)法的錯(cuò)位,分別采用了 ref 來(lái)監(jiān)聽(tīng)基礎(chǔ)數(shù)據(jù)類型, reactive 來(lái)監(jiān)聽(tīng)引用數(shù)據(jù)類型,雖然在 ref 的使用上任然需要借助 .value 來(lái)達(dá)到響應(yīng)性,但好在實(shí)踐項(xiàng)目中單獨(dú)把基礎(chǔ)數(shù)據(jù)類型作為響應(yīng)式數(shù)據(jù)的場(chǎng)景非常少。
也就是說(shuō),在解決這個(gè)問(wèn)題上,反而 vue3 比 solid 更加優(yōu)雅,當(dāng)然,即便如此,在 vue3 中,一些操作也會(huì)讓數(shù)據(jù)失去響應(yīng)性,例如解構(gòu),這是我們?cè)趯W(xué)習(xí)的時(shí)候需要特別注意的地方。
react hooks 的痛點(diǎn),閉包。
react 常常因?yàn)殚]包問(wèn)題,被各種攻擊。認(rèn)為這是 react 的缺陷。如果你沒(méi)有掌握好閉包,視閉包為洪水猛獸,你多半也會(huì)認(rèn)同這樣的說(shuō)法。
因?yàn)閺谋憩F(xiàn)結(jié)果上來(lái)看,閉包帶來(lái)的緩存問(wèn)題確實(shí)會(huì)導(dǎo)致使用者在理解上存在很多疑問(wèn)。然而事實(shí)上,閉包問(wèn)題不是 react 的問(wèn)題,而是 JavaScript 本身的特性,閉包是學(xué)習(xí) JavaScript 本應(yīng)該掌握好的基礎(chǔ)之一,只不過(guò)很多前端開(kāi)發(fā)沒(méi)有做到而已。
新人朋友估計(jì)在面試時(shí),也常常被閉包相關(guān)的面試題虐哭 ~
react 提供了一個(gè)實(shí)踐場(chǎng)景,讓我們能夠直面閉包帶來(lái)的困擾,從而對(duì)閉包更加有掌控度,我認(rèn)為這反而應(yīng)該成為 react 的優(yōu)勢(shì)之一,而不是痛點(diǎn)。
但是 vue3 和 solid 都在極力的避免讓開(kāi)發(fā)者感受到閉包的存在,甚至把這種行為當(dāng)成自己的優(yōu)勢(shì)來(lái)大力宣傳,從我個(gè)人的角度來(lái)說(shuō),我并不贊同這樣的觀點(diǎn),因?yàn)槲覀兘K究是要理解并掌握閉包的呀,對(duì)吧。
跨平臺(tái)
Solid 為了極致的性能體驗(yàn),完全棄用了虛擬 DOM,也就意味著,他放棄了跨平臺(tái)的特性。只把主要精力集中在 web 項(xiàng)目上。也就是說(shuō),他的全局生態(tài)建設(shè),永遠(yuǎn)也趕不上 react
到目前為止,React 已經(jīng)把觸手伸向了后端開(kāi)發(fā)... 已經(jīng)不滿足簡(jiǎn)單的服務(wù)端渲染了,甚至還想要連接數(shù)據(jù)庫(kù)...
這也是 Solid 無(wú)法取代 react 最重要的原因。
我們也可以自己擴(kuò)展 react 的生態(tài)。比如在我的 2d 可視化課程中,我們基于 canvas 封裝了一套類 DOM 的渲染引擎,然后接入 react-reconciler,就能輕松得到一個(gè) react-echarts 的圖表組件,在使用層還是 react 組件,但是在底層已經(jīng)被我瞧瞧的把 DOM 換成了 canvas,或者 webGPU... ,此時(shí),我的項(xiàng)目性能,將會(huì)遠(yuǎn)超 Solid.
總結(jié)
雙向綁定是一種傳統(tǒng)的解決方案,與之相對(duì)比,在前端領(lǐng)域 react 的解決方案是一個(gè)巨大的創(chuàng)新。單向數(shù)據(jù)流,Diff算法,雙緩存策略,優(yōu)先級(jí)隊(duì)列,任務(wù)中斷,瀏覽器空閑時(shí)間,并發(fā),函數(shù)式編程,自定義hook... 等等許多概念都極大的擴(kuò)展了前端開(kāi)發(fā)的技術(shù)視野。
并不確定 react 是否借鑒了其他領(lǐng)域的方案,認(rèn)真看過(guò)我 JavaScript 核心進(jìn)階的同學(xué)就應(yīng)該知道,F(xiàn)iber 架構(gòu)在很大程度上借鑒了 V8 垃圾回收的底層機(jī)制。
而 Solid 作為模仿者,與 React 相比,他并沒(méi)有什么突出的優(yōu)勢(shì),也沒(méi)有什么技術(shù)和理念上的創(chuàng)新。他只是滿足了部分前端開(kāi)發(fā)對(duì)于雙向綁定 + 函數(shù)式的美好愿景而已,至于 vue 和 angular 最終都會(huì)采用 Signal 重構(gòu)底層代碼,那只不過(guò)是因?yàn)樗麄儽旧韽囊婚_(kāi)始就是雙向綁定的基因。
因此在做技術(shù)選型時(shí),任何一個(gè)成熟的前端架構(gòu)師都沒(méi)有理由放棄 react 而選擇 Solid,無(wú)論是從性能上考慮,還是從生態(tài)上考慮,理由都不夠充分,F(xiàn)lutter 尚且做不到取締 React Native,Solid 要走的路還很長(zhǎng)。
而有的人寫(xiě)文章聲稱 Solid 比 React 還 React,Solid 教 React 寫(xiě)函數(shù)式,降維打擊... 那只是常規(guī)的宣傳用語(yǔ),當(dāng)不得真。