Solid.js感觉就像我一直希望的React一样

260 阅读6分钟

我大约在三年前开始专业地使用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>;
}

但现在我们有另一个问题,因为我们看一下运行的应用程序。 image.png

那些对React比较熟悉的人可能知道发生了什么,因为你每天都在处理这种事情:我们创造了太多的间隔(每次效果重新运行时都有一个新的间隔,也就是每次我们增加count )。我们可以用几种不同的方法解决这个问题。

  1. useEffect 钩子中返回一个清理函数,清除区间
  2. 使用setTimeout ,而不是setInterval (好的做法还是使用一个清理函数)
  3. 使用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,自己探索这个框架。