Solid.js 就是我理想中的 React

語言: CN / TW / HK

作者 | 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 時間隔都會增加)。可以通過幾種方式來解決這個問題:

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

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

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

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

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!"

"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/

今日好文推薦

騰訊看點和微視開始裁員;俄羅斯資料儲存告急;中文編寫的作業系統“火龍”被質疑抄襲 | Q資訊

發現競爭對手程式碼中的低階Bug後,我被公司解僱並送上了法庭

我不認為Flutter比React Native好

以反戰為名,百萬周下載量node-ipc包作者進行供應鏈投毒

點個在看少個 bug   :point_down: