大家好,我卡頌。
最近,Angular
、Qwik
的作者MIŠKO HEVERY發文表示Signal是前端框架的未來,並考慮在Angular
中實現它。
在此之前,Vue
、Solid.js
、Preact
、Svelte
都已實現Signal
。實際上,signal
並不是一個新概念,他還有很多別名,比如:
- 響應式更新
- 細粒度更新
如果你瞭解過Vue
響應式更新的實現原理,對Signal
就不會陌生。
實際上,Signal
的技術在10年前Knockout
框架中就有應用。為什麼這項技術正受到越來越多前端框架的青睞?
本文,讓我們一起探討下這個話題。
歡迎加入人類高質量前端交流群,帶飛
signal的本質
signal
的本質,是將對狀態的引用以及對狀態值的獲取分離開。這麼說可能有點抽象,讓我們先看一個非signal
的例子。
以下是React
中定義狀態的方式:
function App() {
const [state, dispatch] = useState(0);
return <p onClick={
() => dispatch(state + 1)
}>{state}</p>
}
useState
的返回值包括兩部分:
state
:狀態的值dispatch
:狀態的setter
可以發現,state
耦合了對狀態的引用以及對狀態值的獲取這兩個含義。
再來看一個signal
的例子。以下是同一個例子用Solid.js
書寫的樣子:
function App() {
const [getState, dispatch] = createSignal(0);
return <p onClick={
() => dispatch(getState() + 1)
}>{getState()}</p>
}
createSignal
的返回值包括兩部分:
getState
:對狀態的引用dispatch
:狀態的setter
區別就體現在getState
上,其中:
getState
是對狀態的引用getState()
是對狀態值的獲取
也就是說,我們可以不必立刻獲取狀態的值,而是在需要的時候再獲取(即在需要時再執行getState()
)。
這麼做有什麼好處呢?如果我們在需要的時候再獲取狀態的值,就能感知當前的上下文環境。
舉個很粗糙的例子,在下面的程式碼中,元件例項(Component
例項)在render
時會將全域性變數cpnContext
指向自己:
let cpnContext = null;
class Component {
render() {
cpnContext = this;
// ...省略邏輯
}
}
那麼在createSignal
返回的getState
方法內部,可以獲取全域性變數cpnContext
來感知當前處於哪個元件的渲染流程:
function createSignal() {
// ...省略邏輯
function getState() {
const curContext = cpnContext;
// ...
}
function dispatch {}
return [getState, dispatch]
}
這麼做的目的是建立狀態變化與需要更新哪個元件之間的聯絡。
相比於React
,基於Signal
實現的框架會有兩個優勢:
- 更好的細粒度更新效能
- 更好的
DX
(開發者體驗)
更好的細粒度更新效能
由於Signal
建立了狀態與元件之間的聯絡,所以相比於React
更有效能優勢。
比如,在我的電腦上,用Svelte
渲染1w個li
,點選某個li
後改變他的內容:
<ul>
{#each items as item (item.id)}
<li on:click={() => items[item.id].name = 'change!'}>{item.name}</li>
{/each}
</ul>
從點選事件觸發,到Svelte
邏輯執行完,再到瀏覽器重排重繪,總用時18.88ms,其中Svelte
的邏輯執行只花了9.5ms:
同樣的例子用React
實現,觸發點選後總用時98.5ms,其中React
的邏輯執行了89.38ms:
在這個例子中,React
效能比Svelte
差了一個數量級。之所以會有這樣的差異,很大一部分原因在於Svlete在更新前就知道狀態變化時需要更新哪個元件。
而這一切的源頭就在於Signal
。
更好的DX
更好的開發者體驗主要體現在兩方面:
Signal
感知上下文環境的能力減少了程式碼心智負擔
比如在React
中,useEffect
在使用時需要指明依賴的狀態:
useEffect(() => {
// ...state1, state2變化後的邏輯
}, [state1, state2])
如果採用Signal
的實現,狀態能感知到自己在useEffect
上下文環境,可以自動建立兩者之間的聯絡,不用再擔心少寫依賴狀態、閉包陷阱等問題,減少心智負擔。
比如在Vue
中,類似useEffect
(僅僅是功能類似,兩者的用途其實是不同的)的watch
,就不需要顯式指明依賴:
<script setup>
import { ref, watch } from 'vue'
const name = ref('')
watch(name, (newName, oldName) => {
// ...省略邏輯
})
</script>
- 減少開發者效能最佳化的心智負擔
使用Signal
的框架通常能獲得不錯的執行時效能,所以不需要額外的效能最佳化API
。反觀React
,開發者如果遇到效能問題,需要手動呼叫效能最佳化API
(比如React.memo
、useMemo
、PureComponent
)。
總結
有以上這麼多優點,難怪很多框架都使用了Signal
。那麼React
對Signal
是什麼態度呢?
React
團隊成員對此的觀點是:
- 有可能引入類似
Signal
的原語 Signal
效能確實好,但他不太符合React
的理念
React
的理念可以用一句話概括:UI反映狀態在某一刻的快照。
既然是快照,那就不是區域性的,而是個整體概念。在React
中,狀態更新會引起整個應用重新render
,就是對React
快照理念的最好詮釋。
React
現階段的所有實現都是基於快照理念。所以,即使引入類似Signal
的原語,可能也是類似Mobx
這樣的上層實現,而不是從底層重構。
我個人比較傾向於認為:React
團隊承認Signal
的優點,但由於積重難返,而且現代裝置的效能通常是過剩的,所以效能問題並不是首要問題。
如果這個觀點是正確的,那麼React
可能會在開發者體驗(Signal
的另一個優點)方面努努力。比如去年提出的RFC: useEvent可能就是這方面的一次嘗試。