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