Solid.js 就是我理想中的 React

語言: CN / TW / HK

我大約在三年前開始在工作中使用 React。巧合的是,當時正好是 React Hooks 出來的時候。我當時的專案程式碼庫有很多類元件,總讓我覺得很笨重。

我們來看看下面的例子:一個每秒遞增一次的計數器。

<code data-type="codeline">class Counter extends React.Component {</code><code data-type="codeline">  constructor() {</code><code data-type="codeline">    super();</code><code data-type="codeline">    this.state = { count: 0 };</code><code data-type="codeline">    this.increment = this.increment.bind(this);</code><code data-type="codeline">  }</code><code data-type="codeline">  increment() {</code><code data-type="codeline">    this.setState({ count: this.state.count + 1 });</code><code data-type="codeline">  }</code><code data-type="codeline">  componentDidMount() {</code><code data-type="codeline">    setInterval(() => {</code><code data-type="codeline">      this.increment();</code><code data-type="codeline">    }, 1000);</code><code data-type="codeline">  }</code><code data-type="codeline">  render() {</code><code data-type="codeline">    return <div>The count is: {this.state.count}</div>;</code><code data-type="codeline">  }</code><code data-type="codeline">}</code>

複製程式碼

對於一個自動遞增的計數器來說要寫這麼多程式碼可不算少。更多的模板和儀式意味著出錯的可能性更大,開發體驗也更差。

Hooks 很漂亮,但是容易出錯

當 hooks 出現的時候我非常興奮。我的計數器可以簡化為以下寫法:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = useState(0);</code><code data-type="codeline">  useEffect(() => {</code><code data-type="codeline">    setInterval(() => {</code><code data-type="codeline">      setCount(count + 1);</code><code data-type="codeline">    }, 1000);</code><code data-type="codeline">  }, []);</code><code data-type="codeline">  return <div>The count is: {count}</div>;</code><code data-type="codeline">}</code>

複製程式碼

等等,這其實是不對的。我們的 useEffect hook 在 count 周圍有一個陳舊閉包,因為我們沒有把 count 包含在 useEffect 依賴陣列中。從依賴陣列中省略變數是 React hooks 的一個常見錯誤,如果你忘記了,有一些 linting 規則會警告你的。

我稍後會回到這個問題上。現在,我們把缺少的 count 變數新增到依賴陣列中:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = useState(0);</code><code data-type="codeline">  useEffect(() => {</code><code data-type="codeline">    setInterval(() => {</code><code data-type="codeline">      setCount(count + 1);</code><code data-type="codeline">    }, 1000);</code><code data-type="codeline">  }, [count]);</code><code data-type="codeline">  return <div>The count is: {count}</div>;</code><code data-type="codeline">}</code>

複製程式碼

但現在我們遇到了另一個問題,看看應用程式的執行效果:

精通 React 的人們可能知道發生了什麼事情,因為你每天都在與這種問題作鬥爭:我們建立了太多的間隔(每次重新執行效果時都會建立一個新間隔,也就是每次我們增加 count 時間隔都會增加)。可以通過幾種方式來解決這個問題:

  • 從清除間隔的 useEffect hook 返回一個清理函式

  • 使用 setTimeout 代替 setInterval(還是要使用清理函式)

  • 使用 setCount 的函式形式來避免直接引用當前值

事實上哪種辦法都行得通。我們在這裡實現最後一個選項:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = useState(0);</code><code data-type="codeline">  useEffect(() => {</code><code data-type="codeline">    setInterval(() => {</code><code data-type="codeline">      setCount((count) => count + 1);</code><code data-type="codeline">    }, 1000);</code><code data-type="codeline">  }, []);</code><code data-type="codeline">  return <div>The count is: {count}</div>;</code><code data-type="codeline">}</code>

複製程式碼

我們的計數器修好了!由於依賴陣列中沒有任何內容,因此我們只建立了一個間隔。由於我們為計數設定器使用了回撥函式,因此永遠不會在 count 變數上有陳舊閉包。

這是一個人為做出來的例子,但除非你已經使用 React 一段時間,否則它仍然很令人困惑。我們中有許多人每天都會遇到更復雜的情況,即使是最有經驗的 React 開發人員也會為之頭痛不已。

假的響應性

我思考了很多關於 hooks 的事情,想知道為什麼它們感覺不太對勁。結果我通過探索 Solid.js 找到了答案。

React hooks 的問題在於 React 並不是真正的響應式設計。如果 linter 知道一個效果(或回撥或 memo)hook 何時缺少依賴項,那麼為什麼框架不能自動檢測依賴項並對這些更改做出響應呢?

深入研究 Solid.js

關於 Solid,首先要注意的是它沒有嘗試重新發明輪子:它看起來很像 React,因為 React 有一些顯眼的模式:單向、自上而下的狀態;JSX;元件驅動的架構。

如果我們用 Solid 重寫 Counter 元件,會這樣開始:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = createSignal(0);</code><code data-type="codeline">  return <div>The count is: {count()}</div>;</code><code data-type="codeline">}</code>

複製程式碼

到目前為止我們看到了一個很大的不同點:count 是一個函式。這稱為訪問器(accessor),它是 Solid 工作機制的重要組成部分。當然,我們這裡沒有關於按間隔遞增 count 的內容,所以下面把它新增進去:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = createSignal(0);</code><code data-type="codeline">  setInterval(() => {</code><code data-type="codeline">    setCount(count() + 1);</code><code data-type="codeline">  }, 1000);</code><code data-type="codeline">  return <div>The count is: {count()}</div>;</code><code data-type="codeline">}</code>

複製程式碼

這肯定行不通,對吧?每次元件渲染時不會設定新的間隔嗎?

沒有。它就這麼正常運行了。

但為什麼會這樣?好吧,事實證明 Solid 不需要重新執行 Counter 函式來重渲染新的計數。事實上,它根本不需要重新執行 Counter 函式。如果我們在 Counter 函式中新增一個 console.log 語句,就會看到它只執行一次。

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = createSignal(0);</code><code data-type="codeline">  setInterval(() => {</code><code data-type="codeline">    setCount(count() + 1);</code><code data-type="codeline">  }, 1000);</code><code data-type="codeline">  console.log('The Counter function was called!');</code><code data-type="codeline">  return <div>The count is: {count()}</div>;</code><code data-type="codeline">}</code>

複製程式碼

在我們的控制檯中,只有一個孤獨的日誌語句:

"The Counter function was called!"

複製程式碼

在 Solid 中,除非我們明確要求,否則程式碼不會多次執行。

但是 hooks 呢?

於是我在 Solid 中解決了 React useEffect hook 的問題,而無需編寫看起來像 hooks 的東西。我們可以擴充套件我們的計數器例子來探索 Solid 效果。

如果我們想在每次計數增加時 console.log count 怎麼辦?你的第一反應可能是在我們的函式中使用 console.log:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = createSignal(0);</code><code data-type="codeline">  setInterval(() => {</code><code data-type="codeline">    setCount(count() + 1);</code><code data-type="codeline">  }, 1000);</code><code data-type="codeline">  console.log(`The count is ${count()}`);</code><code data-type="codeline">  return <div>The count is: {count()}</div>;</code><code data-type="codeline">}</code>

複製程式碼

但這不起作用。請記住,Counter 函式只執行一次!但我們可以使用 Solid 的 createEffect 函式來獲得想要的效果:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = createSignal(0);</code><code data-type="codeline">  setInterval(() => {</code><code data-type="codeline">    setCount(count() + 1);</code><code data-type="codeline">  }, 1000);</code><code data-type="codeline">  createEffect(() => {</code><code data-type="codeline">    console.log(`The count is ${count()}`);</code><code data-type="codeline">  });</code><code data-type="codeline">  return <div>The count is: {count()}</div>;</code><code data-type="codeline">}</code>

複製程式碼

這行得通!而且我們甚至不必告訴 Solid,說這個效果取決於 count 變數。這才是真正的響應式設計。如果在 createEffect 函式內部呼叫了第二個訪問器,它也會讓效果執行起來。

一些更有趣的 Solid 概念

響應性,而不是生命週期 hooks

如果你已經在 React 領域有一段時間的經驗了,那麼下面的程式碼更改可能真的會讓你大跌眼鏡:

<code data-type="codeline">const [count, setCount] = createSignal(0);</code><code data-type="codeline">setInterval(() => {</code><code data-type="codeline">  setCount(count() + 1);</code><code data-type="codeline">}, 1000);</code><code data-type="codeline">createEffect(() => {</code><code data-type="codeline">  console.log(`The count is ${count()}`);</code><code data-type="codeline">});</code><code data-type="codeline">function Counter() {</code><code data-type="codeline">  return <div>The count is: {count()}</div>;</code><code data-type="codeline">}</code>

複製程式碼

並且程式碼仍然是有效的。我們的 count 訊號不需要存在於一個元件函式中,依賴它的效果也不需要。一切都只是響應式系統的一部分,“生命週期 hooks”實際上並沒有起到太大的作用。

細粒度的 DOM 更新

前面我主要關注的是 Solid 的開發體驗(例如更容易編寫沒有錯誤的程式碼),但 Solid 的效能表現也得到了很多讚譽。其強大效能的一個關鍵來源是它直接與 DOM 互動(無虛擬 DOM)並執行“細粒度”的 DOM 更新。

考慮對我們的計數器進行以下調整:

<code data-type="codeline">function Counter() {</code><code data-type="codeline">  const [count, setCount] = createSignal(0);</code><code data-type="codeline">  setInterval(() => {</code><code data-type="codeline">    setCount(count() + 1);</code><code data-type="codeline">  }, 1000);</code><code data-type="codeline">  return (</code><code data-type="codeline">    <div></code><code data-type="codeline">      The {(console.log('DOM update A'), false)} count is:{' '}</code><code data-type="codeline">      {(console.log('DOM update B'), count())}</code><code data-type="codeline">    </div></code><code data-type="codeline">  );</code><code data-type="codeline">}</code>

複製程式碼

執行它會在控制檯中獲得以下日誌:

<code data-type="codeline">DOM update A</code><code data-type="codeline">DOM update B</code><code data-type="codeline">DOM update B</code><code data-type="codeline">DOM update B</code><code data-type="codeline">DOM update B</code><code data-type="codeline">DOM update B</code><code data-type="codeline">DOM update B</code>

複製程式碼

換句話說,每秒更新的唯一內容是包含 count 的一小部分 DOM。Solid 甚至沒有重新運行同一 div 中較早的 console.log。

小結

在過去的幾年裡我很喜歡使用 React;在處理實際的 DOM 時,我總感覺它有著正確的抽象級別。話雖如此,我也開始注意到 React hooks 程式碼經常變得容易出錯。我感覺 Solid.js 使用了 React 的許多符合人體工程學的部分,同時最大程度減少了混亂和錯誤。本文向你展示的是 Solid 的一些讓我驚歎的部分,感興趣的話我建議你檢視 https://www.solidjs.com 並自己探索這個框架。