SolidJS 硬氣的說:我比 React 還 react
大家好,我是卡頌。
最近刷推時,有個老哥經常出現在**「前端框架」**相關推文下。
一副憨厚的樣貌
我想:“老哥你哪位?”
一查,原來是個框架作者,作品叫 SolidJS[1]。
翻翻框架介紹,這句話成功吸引我的注意:
支持現代前端特性,例如:JSX, Fragments, Context, Portals, Suspense, Streaming SSR, Progressive Hydration, Error Boundaries 和 Concurrent Rendering
我琢磨您不會是React
在逃公主吧?這不能說和React
類似,只能說完全一樣吧?
作爲傳統中國人,秉承**「來都來了」**思想,我試用了一天,又看了下源碼,結果發現這個框架真是個寶藏框架。
本文會比較SolidJS
與React
的異同,闡述他的獨特優勢,看完後不知道你會不會和我發出同樣的感嘆:
這簡直比 React 還 react(react 譯爲響應)
相信看完本文後,不僅能認識一個新框架,還能對React
有更深的認識。
開整!
初看很相似
讓我們從一個**「計數器」**的例子看看與React
語法的差異:
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
const increment = () => setCount(count() + 1);
return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}
render(() => <Counter />, document.getElementById("app"));
和React
不同的地方:
-
useState
改名成createSignal
-
獲取
count
狀態從React
中直接使用count
變爲通過方法調用,即:count()
難道僅僅是一個類React
框架?
別急,讓我們從**「編譯時」**、**「運行時」**、**「響應原理」**三方面來看看。
編譯時大不同
React
的編譯時很**「薄」**,基本只是編譯JSX
語法。
而SolidJS
則採用了類似Svelte
的方案:在編譯時,將狀態更新編譯爲獨立的DOM
操作方法。
這樣做有什麼好處?主要有兩點。
一定條件下的體積優勢
你不需要爲你沒使用的代碼付出代價
使用React
時,即使沒有用到Hooks
,其代碼也會出現在最終編譯後的代碼中。
而在SolidJS
中,未使用的功能不會出現在編譯後的代碼中。
舉個例子,上面計時器的例子中,編譯後的代碼有一行是這樣:
delegateEvents(["click"]);
這行代碼的目的是在document
上註冊click
事件代理。
如果在計時器中沒有使用onClick
,那麼編譯後代碼中就不會有這一行。
有熱心網友對比了類似編譯時方案的Svelte
與React
之間**「源代碼」**與**「編譯後代碼」**的體積差異。
其中橫軸代表源代碼體積,縱軸代表編譯後代碼體積,紅色線條代表Svelte
,藍色代表React
:
可見,在臨界值(業務源代碼體積達到 120kb)之前,編譯時方案有一定體積優勢。
由於SolidJS
使用JSX
描述視圖,比Svelte
使用類似Vue
的模版語法更靈活,所以在編譯時沒法做到Svelte
一樣的極致編譯優化,使得其相比Svelte
運行時更重一點。
這爲他帶來了額外的好處:在真實項目(>120kb)中,SolidJS
的代碼體積比Svelte
小 25% 左右。
還真是,因禍得福?
更快的更新速度
我們知道,在React
與Vue
中存在一層**「虛擬 DOM」**(React
中叫Fiber
樹)。
每當發生更新,**「虛擬 DOM」**會進行比較(Diff
算法),比較的結果會執行不同的DOM
操作(增、刪、改)。
而SolidJS
與Svelte
在發生更新時,可以直接調用編譯好的DOM
操作方法,省去了**「虛擬 DOM 比較」**這一步所消耗的時間。
舉個例子,上文的計時器,當點擊後,從觸發更新到視圖變化的調用棧如下:
觸發事件,更新狀態,更新視圖,一路調用走到底,清晰明瞭。
同樣的例子放到React
中,調用棧如下:
左中右紅、綠、藍框調用棧分別對應:
-
處理事件
-
對比並生成
Fiber
樹 -
根據對比結果執行
DOM
操作
可見,SolidJS
的更新路徑比React
短很多。
你問憑什麼?這還得從其特殊的**「響應原理」**聊起。
響應原理
假設有個狀態name
,初始值爲KaSong
。我們希望根據name
渲染一個div
。
SolidJS
編譯後的代碼類似:
const [name, setName] = createSignal("KaSong");
const el = document.createElement("div");
createEffect(() => el.textContent = name());
其中createEffect
類似React
的useEffect
。
由於其回調內依賴了name
,所以當name
改變後會觸發createEffect
回調,改變el.textContent
,造成DOM
更新。
類似React
的:
useEffect(() => {
el.textContent = name;
}, [name])
首屏渲染結果:
<div>KaSong</div>
接下來,觸發更新:
setName("XiaoMing")
更新後結果:
<div>XiaoMing</div>
爲什麼更新name
後會觸發createEffect
?
這裏也沒有什麼黑魔法,就是**「訂閱發佈」**。
createEffect
回調依賴name
,所以會訂閱name
的變化。
由於篇幅有限,實現細節咱下回細聊。
這裏的關鍵在於,SolidJS
的狀態具有**「原子性」**。
即狀態互相之間有依賴關係,他們形成局部的依賴圖。當改變一個狀態後,依賴圖中的其他狀態也會改變。
createEffect
中如果使用了這些依賴,就會訂閱他們的變化。
當狀態改變後,createEffect
回調會執行,進而執行具體的DOM
方法,更新視圖。
「真」。「響應式更新」,指哪打哪,李雲龍直呼內行。
有同學會問,React
不是這樣麼?
那我問你個問題:
爲什麼Hooks
會有調用順序不能變的要求?
爲什麼useEffect
回調會有閉包問題?
答案已經呼之欲出了:React
只有在這些限制下才能實現**「響應式」**。
辛勞苦幹 React
有一個可能反直覺的知識:React
並不關心哪個組件觸發了更新。
在React
中,任何一個組件觸發更新(如調用this.setState
),所有組件都會重新走一遍流程。因爲需要構建一棵新的Fiber
樹。
爲了減少無意義的render
,React
內部有些優化策略用來判斷組件是否可以複用上次更新的Fiber
節點(從而跳過render
)。
同時,也提供了很多API
(比如:useMemo
、PureComponent
...),讓開發者告訴他哪些組件可以跳過render
。
如果說,SolidJS
的更新流程像一個畫家,畫面中哪兒需要更新就往哪兒畫幾筆。
那麼React
的更新流程像是一個人拿相機拍一張照片,再拿這張照片和上次拍的照片找不同,最後把不同的地方更新了。
總結
今天,我們聊了SolidJS
與React
的差異,主要體現在三方面:
-
編譯時
-
運行時
-
響應原理
不知道你喜歡這款:沒有Hooks
順序限制、沒有useEffect
閉包問題、沒有Fiber
樹、比React
更react
的框架麼?
如果你問我選哪個?當然,哪個給工資高我用哪個。
參考資料
[1]
SolidJS: https://github.com/solidjs/solid
我是卡頌,《React 技術揭祕》作者,全球開發者資訊觀察者
加我個人微信,我會:
-
每天在朋友圈分享
全球最新開發者資訊
-
拉你進
React源碼級進階羣
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/A_IYySoUmTQ7MLYp2IS_VA