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