天天看點

Signal:更多前端架構的選擇

作者:魔術師卡頌

大家好,我卡頌。

最近,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:

Signal:更多前端架構的選擇

同樣的例子用React實作,觸發點選後總用時98.5ms,其中React的邏輯執行了89.38ms:

Signal:更多前端架構的選擇

在這個例子中,React性能比Svelte差了一個數量級。之是以會有這樣的差異,很大一部分原因在于「Svlete在更新前就知道狀态變化時需要更新哪個元件」。

而這一切的源頭就在于Signal。

更好的DX

更好的開發者體驗主要展現在兩方面:

  1. 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>
           
  1. 減少開發者性能優化的心智負擔

使用Signal的架構通常能獲得不錯的運作時性能,是以不需要額外的性能優化API。反觀React,開發者如果遇到性能問題,需要手動調用性能優化API(比如React.memo、useMemo、PureComponent)。

總結

有以上這麼多優點,難怪很多架構都使用了Signal。那麼React對Signal是什麼态度呢?

React團隊成員對此的觀點是:

  1. 有可能引入類似Signal的原語
  2. Signal性能确實好,但他不太符合React的理念
Signal:更多前端架構的選擇

React的理念可以用一句話概括:「UI反映狀态在某一刻的快照」。

既然是快照,那就不是局部的,而是個整體概念。在React中,狀态更新會引起整個應用重新render,就是對React快照理念的最好诠釋。

React現階段的所有實作都是基于快照理念。是以,即使引入類似Signal的原語,可能也是類似Mobx這樣的上層實作,而不是從底層重構。

我個人比較傾向于認為:React團隊承認Signal的優點,但由于積重難返,而且現代裝置的性能通常是過剩的,是以性能問題并不是首要問題。

如果這個觀點是正确的,那麼React可能會在開發者體驗(Signal的另一個優點)方面努努力。比如去年提出的RFC: useEvent可能就是這方面的一次嘗試。