我大约在三年前开始专业地使用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>;
}
}
对于一个自动递增的计数器来说,要写很多的代码。更多的模板和仪式意味着出错的可能性更大,开发者的体验更差。
钩子是非常漂亮的,但容易出错
当钩子出现的时候,我非常兴奋。我的计数器可以简化为以下内容。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return <div>The count is: {count}</div>;
}
等等,实际上这是不对的。我们的useEffect 钩子在count 周围有一个陈旧的闭合,因为我们还没有把count 包括在useEffect 的依赖数组中。从依赖关系数组中省略变量是React钩子的一个常见错误,以至于如果你忘记了一个,有一些提示规则会对你大喊。
我稍后会回到这个问题上。现在,我们将把我们丢失的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钩子中返回一个清理函数,清除区间 - 使用
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开发者也会感到困惑。
假的反应性
我想了很多关于钩子的问题,以及为什么它们感觉不大对劲。事实证明,我通过对Solid.js的探索找到了答案。
React钩子的问题在于,React并不是真正的反应性。如果一个linter知道效果(或回调,或备忘录)钩子缺少一个依赖性,那么为什么框架不能自动检测依赖性并对这些变化做出反应?
深入了解 Solid.js
关于Solid,首先要注意的是,它并不试图重新发明轮子:从远处看,它很像React,因为React有一些巨大的模式:单向的、自上而下的状态;JSX;组件驱动的架构。
如果我们开始用Solid重写我们的Counter 组件,我们会像这样开始。
function Counter() {
const [count, setCount] = createSignal(0);
return <div>The count is: {count()}</div>;
}
到目前为止,我们看到一个很大的区别:count 是一个函数。这被称为访问器,它是Solid工作方式的一个重要部分。当然,我们这里没有任何关于在间隔时间内递增count ,所以让我们添加这个。
function Counter() {
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount(count() + 1);
}, 1000);
return <div>The count is: {count()}</div>;
}
这肯定是行不通的,对吗?每次组件渲染时不是要设置新的间隔吗?
不是的。它就是有效的。
但为什么呢?嗯,事实证明,Solid 不需要重新运行Counter 函数来重新渲染新的count 。事实上,它根本不需要重新运行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中,除非我们明确要求,否则代码不会运行超过一次。
但是钩子呢?
事实证明,我解决了我们的ReactuseEffect 钩子,而不必在Solid中实际写一些看起来像钩子的东西。糟糕!"。但是没关系,我们可以扩展我们的计数器例子来探索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 概念
反应性,而不是生命周期钩子
如果你已经在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 信号不需要住在一个组件函数中,依赖它的效果也不需要。一切都只是反应式系统的一部分,"生命周期钩子 "真的没有发挥什么作用。
细粒度的DOM更新
到目前为止,我一直很关注开发者的体验(例如,更容易写出没有漏洞的代码),但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。甚至连同一div 中早先的console.log 也没有被重新运行。
结束语
在过去的几年里,我很喜欢与React一起工作;它总是感觉是从实际的DOM工作中抽象出来的正确层次。话虽如此,我也对React钩子的代码经常变得很容易出错感到警惕。Solid.js感觉它使用了很多React的人体工程学部分,同时最大限度地减少了混乱和错误。我试图向你展示Solid中给我带来 "啊哈!"时刻的一些部分,但我建议你查看www.solidjs.com,自己探索这个框架。