我大约在三年前开始在工作中使用React。巧合的是,当时正好是ReactHooks出来的时候。我当时的项目代码库有很多类组件,总让我觉得很笨重。
我们来看看下面的例子:一个每秒递增一次的计数器。
classCounterextendsReact.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
Thecountis:{this.state.count}
对于一个自动递增的计数器来说要写这么多代码可不算少。更多的模板和仪式意味着出错的可能性更大,开发体验也更差。
Hooks很漂亮,但是容易出错
当hooks出现的时候我非常兴奋。我的计数器可以简化为以下写法:
functionCounter(){const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{setCount(count+1);},1000);},[]);return
Thecountis:{count}div>;
等等,这其实是不对的。我们的useEffecthook在count周围有一个陈旧闭包,因为我们没有把count包含在useEffect依赖数组中。从依赖数组中省略变量是Reacthooks的一个常见错误,如果你忘记了,有一些linting规则会警告你的。
我稍后会回到这个问题上。现在,我们把缺少的count变量添加到依赖数组中:
functionCounter(){const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{setCount(count+1);},1000);},[count]);return
但现在我们遇到了另一个问题,看看应用程序的运行效果:
事实上哪种办法都行得通。我们在这里实现最后一个选项:
functionCounter(){const[count,setCount]=useState(0);useEffect(()=>{setInterval(()=>{setCount((count)=>count+1);},1000);},[]);return
我们的计数器修好了!由于依赖数组中没有任何内容,因此我们只创建了一个间隔。由于我们为计数设置器使用了回调函数,因此永远不会在count变量上有陈旧闭包。
假的响应性
Reacthooks的问题在于React并不是真正的响应式设计。如果linter知道一个效果(或回调或memo)hook何时缺少依赖项,那么为什么框架不能自动检测依赖项并对这些更改做出响应呢?
深入研究Solid.js
关于Solid,首先要注意的是它没有尝试重新发明轮子:它看起来很像React,因为React有一些显眼的模式:单向、自上而下的状态;JSX;组件驱动的架构。
如果我们用Solid重写Counter组件,会这样开始:
functionCounter(){const[count,setCount]=createSignal(0);return
Thecountis:{count()}div>;
到目前为止我们看到了一个很大的不同点:count是一个函数。这称为访问器(accessor),它是Solid工作机制的重要组成部分。当然,我们这里没有关于按间隔递增count的内容,所以下面把它添加进去:
functionCounter(){const[count,setCount]=createSignal(0);setInterval(()=>{setCount(count()+1);},1000);return
这肯定行不通,对吧?每次组件渲染时不会设置新的间隔吗?
没有。它就这么正常运行了。
但为什么会这样?好吧,事实证明Solid不需要重新运行Counter函数来重渲染新的计数。事实上,它根本不需要重新运行Counter函数。如果我们在Counter函数中添加一个console.log语句,就会看到它只运行一次。
functionCounter(){const[count,setCount]=createSignal(0);setInterval(()=>{setCount(count()+1);},1000);console.log('TheCounterfunctionwascalled!');return
在我们的控制台中,只有一个孤独的日志语句:
"TheCounterfunctionwascalled!"
"TheCounterfunctionwascalled!"在Solid中,除非我们明确要求,否则代码不会多次运行。
但是hooks呢?
于是我在Solid中解决了ReactuseEffecthook的问题,而无需编写看起来像hooks的东西。我们可以扩展我们的计数器例子来探索Solid效果。
如果我们想在每次计数增加时console.logcount怎么办?你的第一反应可能是在我们的函数中使用console.log:
functionCounter(){const[count,setCount]=createSignal(0);setInterval(()=>{setCount(count()+1);},1000);console.log(`Thecountis${count()}`);return
但这不起作用。请记住,Counter函数只运行一次!但我们可以使用Solid的createEffect函数来获得想要的效果:
functionCounter(){const[count,setCount]=createSignal(0);setInterval(()=>{setCount(count()+1);},1000);createEffect(()=>{console.log(`Thecountis${count()}`);return
这行得通!而且我们甚至不必告诉Solid,说这个效果取决于count变量。这才是真正的响应式设计。如果在createEffect函数内部调用了第二个访问器,它也会让效果运行起来。
一些更有趣的Solid概念
响应性,而不是生命周期hooks
const[count,setCount]=createSignal(0);setInterval(()=>{setCount(count()+1);},1000);createEffect(()=>{console.log(`Thecountis${count()}`);functionCounter(){return
并且代码仍然是有效的。我们的count信号不需要存在于一个组件函数中,依赖它的效果也不需要。一切都只是响应式系统的一部分,“生命周期hooks”实际上并没有起到太大的作用。
细粒度的DOM更新
考虑对我们的计数器进行以下调整:
functionCounter(){const[count,setCount]=createSignal(0);setInterval(()=>{setCount(count()+1);},1000);return(
The{(console.log('DOMupdateA'),false)}countis:{''}{(console.log('DOMupdateB'),count())}
运行它会在控制台中获得以下日志:
DOMupdateADOMupdateBDOMupdateBDOMupdateBDOMupdateBDOMupdateBDOMupdateB
换句话说,每秒更新的唯一内容是包含count的一小部分DOM。Solid甚至没有重新运行同一div中较早的console.log。