Solid-js 就是我理想中的 React

作者 | Nick Scialli

譯者 | 王強

策劃 | 閆園園

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

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

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

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

Hooks 很漂亮,但是容易出錯

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

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

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

我稍後會回到這個問題上。現在,我們把缺少的 count 變量添加到依賴數組中:

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

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

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

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

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

我們的計數器修好了!由於依賴數組中沒有任何內容,因此我們只創建了一個間隔。由於我們爲計數設置器使用了回調函數,因此永遠不會在 count 變量上有陳舊閉包。

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

假的響應性

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

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

深入研究 Solid.js

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

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

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

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

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

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

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

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

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

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

"The Counter function was called!"

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

但是 hooks 呢?

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

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

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

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

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

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

一些更有趣的 Solid 概念

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

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

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

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

細粒度的 DOM 更新

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

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

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

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

DOM update A
DOM update B
DOM update B
DOM update B
DOM update B
DOM update B
DOM update B

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

小結

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

原文鏈接:

https://typeofnan.dev/solid-js-feels-like-what-i-always-wanted-react-to-be/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0sGJ9r_9C9EPzliEkUuyTg