作者|jump--jump
Signals 在目前前端框架的選型中遙遙領(lǐng)先!
國慶節(jié)前最后一周在 Code Review 新同學(xué)的 React 代碼,發(fā)現(xiàn)他想通過 memo 和 useCallback 只渲染被修改的子組件部分。事實上該功能在 React 中是難以做到的。因為 React 狀態(tài)變化后,會重新執(zhí)行 render 函數(shù)。也就是在組件中調(diào)用 setState 之后,整個函數(shù)將會重新執(zhí)行一次。
React 本身做不到。但是基于 Signals 的框架卻不會這樣,它通過自動狀態(tài)綁定和依賴跟蹤使得當(dāng)前狀態(tài)變化后僅僅只會重新執(zhí)行用到該狀態(tài)代碼塊。
個人當(dāng)時沒有過多的解釋這個問題,只是匆匆解釋了一下 React 的渲染機(jī)制。在這里做一個 Signals 的梳理。
優(yōu)勢
對比 React,基于 Signals 的框架狀態(tài)響應(yīng)粒度非常細(xì)。這里以 Solid 為例:
import { createSignal, onCleanup } from "solid-js"; const CountingComponent = () => { // 創(chuàng)建一個 signal const [count, setCount] = createSignal(0); // 創(chuàng)建一個 signal const [count2] = createSignal(666); // 每一秒遞增 1 const interval = setInterval(() => { setCount((c) => c + 1); }, 1000); // 組件銷毀時清除定時器 onCleanup(() => clearInterval(interval)); return (上面這段代碼在 count 單獨(dú)變化時,只會打印 count,壓根不會打印 count2 數(shù)據(jù)。 控制臺打印如下所示:); };count: {count()} {console.log("count is", count())}count2: {count2()} {console.log("count2 is", count2())}
count is 0
count2 is 666
count is 1
count is 2
...
從打印結(jié)果來看,Solid 只會在最開始執(zhí)行一次渲染函數(shù),后續(xù)僅僅只會渲染更改過的 DOM 節(jié)點(diǎn)。這在 React 中是不可能做到的,React 是基于視圖驅(qū)動的,狀態(tài)改變會重新執(zhí)行整個渲染函數(shù),并且 React 完全無法識別狀態(tài)是如何被使用的,開發(fā)者甚至可以通過下面的代碼來實現(xiàn) React 的重新渲染。
const [, forceRender] = useReducer((s) => s + 1, 0);
除了更新粒度細(xì)之外,使用 Signals 的框架心智模型也更加簡單。其中最大的特點(diǎn)是:開發(fā)者完全不必在意狀態(tài)在哪定義,也不在意對應(yīng)狀態(tài)在哪渲染。如下所示:
import { createSignal } from "solid-js"; // 把狀態(tài)從過組件中提取出來 const [count, setCount] = createSignal(0); const [count2] = createSignal(666); setInterval(() => { setCount((c) => c + 1); }, 1000); // 子組件依然可以使用 count 函數(shù) const SubCountingComponent = () => { return{count()}; }; const CountingComponent = () => { return (); };count: {count()} {console.log("count is", count())}count2: {count2()} {console.log("count2 is", count2())}
上述代碼依然可以正常運(yùn)行。因為它是基于狀態(tài)驅(qū)動的。開發(fā)者在組件內(nèi)使用 Signal 是本地狀態(tài),在組件外定義 Signal 就是全局狀態(tài)。
Signals 本身不是那么有價值,但結(jié)合派生狀態(tài)以及副作用就不一樣了。代碼如下所示:
import { createSignal, onCleanup, createMemo, createEffect, onMount, } from "solid-js"; const [count, setCount] = createSignal(0); setInterval(() => { setCount((c) => c + 1); }, 1000); // 計算緩存 const doubleCount = createMemo(() => count() * 2); // 基于當(dāng)前緩存 const quadrupleCount = createMemo(() => doubleCount() * 2); // 副作用 createEffect(() => { // 在 count 變化時重新執(zhí)行 fetch fetch(`/api/${count()}`); }); const CountingComponent = () => { // 掛載組件時執(zhí)行 onMount(() => { console.log("start"); }); // 銷毀組件時執(zhí)行 onCleanup(() => { console.log("end"); }); return (從上述代碼可以看到,派生狀態(tài)和副作用都不需要像 React 一樣填寫依賴項,同時也將副作用與生命周期分開 (代碼更好閱讀)。); };Count value is {count()}doubleCount value is {doubleCount()}quadrupleCount value is {quadrupleCount()}
實現(xiàn)機(jī)制
細(xì)粒度,高性能,同時還沒有什么限制。不愧被譽(yù)為前端框架的未來。那么它究竟是如何實現(xiàn)的呢? 本質(zhì)上,Signals 是一個在訪問時跟蹤依賴、在變更時觸發(fā)副作用的值容器。 這種基于響應(yīng)性基礎(chǔ)類型的范式在前端領(lǐng)域并不是一個特別新的概念:它可以追溯到十多年前的 Knockout observables 和 Meteor Tracker 等實現(xiàn)。Vue 的選項式 API 也是同樣的原則,只不過將基礎(chǔ)類型這部分隱藏在了對象屬性背后。依靠這種范式,Vue2 基本不需要優(yōu)化就有非常不錯的性能。
依賴收集
React useState 返回當(dāng)前狀態(tài)和設(shè)置值函數(shù),而 Solid 的 createSignal 返回兩個函數(shù)。即:
type useState = (initial: any) => [state, setter]; type createSignal = (initial: any) => [getter, setter];為什么 createSignal 要傳遞 getter 方法而不是直接傳遞對應(yīng)的 state 值呢?這是因為框架為了具備響應(yīng)能力,Signal 必須要收集誰對它的值感興趣。僅僅傳遞狀態(tài)是無法提供 Signal 任何信息的。而 getter 方法不但返回對應(yīng)的數(shù)值,同時執(zhí)行時創(chuàng)建一個訂閱,以便收集所有依賴信息。
模版編譯
要保證 Signals 框架的高性能,就不得不結(jié)合模版編譯實現(xiàn)該功能,框架開發(fā)者通過模版編譯實現(xiàn)動靜分離,配合依賴收集,就可以做到狀態(tài)變量變化時點(diǎn)對點(diǎn)的 DOM 更新。所以目前主流的 Signals 框架沒有使用虛擬 DOM。而基于虛擬 DOM 的 Vue 目前依靠編譯器來實現(xiàn)類似的優(yōu)化。 下面我們先看看 Solid 的模版編譯:
const CountingComponent = () => { const [count, setCount] = createSignal(0); const interval = setInterval(() => { setCount((c) => c + 1); }, 1000); onCleanup(() => clearInterval(interval)); returnCount value is {count()}; };
對應(yīng)編譯后的的組件代碼。
const _tmpl$ = /*#__PURE__*/ _$template(`Count value is `); const CountingComponent = () => { const [count, setCount] = createSignal(0); const interval = setInterval(() => { setCount((c) => c + 1); }, 1000); onCleanup(() => clearInterval(interval)); return (() => { const _el$ = _tmpl$(), _el$2 = _el$.firstChild; _$insert(_el$, count, null); return _el$; })(); };執(zhí)行 _tmpl$ 函數(shù),獲取對應(yīng)組件的靜態(tài)模版
提取組件中的 count 函數(shù),通過 _$insert 將狀態(tài)函數(shù)和對應(yīng)模版位置進(jìn)行綁定
調(diào)用 setCount 函數(shù)更新時,比對一下對應(yīng)的 count,然后修改對應(yīng)的 _el$ 對應(yīng)數(shù)據(jù)
其他
大家可以看一看使用 Signals 的主流框架:
Vue Ref
Angular Signals
Preact Signals
Solid Signals
Qwik Signals
Svelte 5 (即將推出)
不過目前來看 React 團(tuán)隊可能不會使用 Signals。
Signals 性能很好,但不是編寫 UI 代碼的好方式
計劃通過編譯器來提升性能
可能會添加類似 Signals 的原語
PREACT 作者編寫了@preact/signals-react 為 React 提供了 Signals。不過個人不建議在生產(chǎn)環(huán)境使用。
編輯:黃飛
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
-
API
+關(guān)注
關(guān)注
2文章
1507瀏覽量
62219 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4344瀏覽量
62810 -
代碼
+關(guān)注
關(guān)注
30文章
4809瀏覽量
68823 -
Signals
+關(guān)注
關(guān)注
0文章
2瀏覽量
1026
原文標(biāo)題:聊聊前端框架的未來Signals
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論