Vue3推出了composition api,并且官方支持了jsx,这使得vue3与react变得很相似,熟悉React的同学基本可以直接上手Vue3。我可能会写一个系列,去介绍React和vue3功能上的区分点。
状态hooks
什么是状态就不解释了,下面是用react和vue3分别实现一个自增的按钮的例子
// React
const Button = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(prev => prev+1)}>{count}</button>
}
// Vue
const Button = defineComponent({
setup() {
const count = ref(0);
return () => <button onClick={() => count.value++}>{count.value}</button>
}
})
这是一个简单的例子。React通过useState hook来维护count,vue通过ref api来维护count
来看看区别
immutable VS mutable
react的状态是immutable的,就是你不能直接修改它。在这个例子中,你只能通过setCount来修改count,下次渲染时,count的引用地址已经变了。
vue的状态是mutable的,你可以直接修改它,这个例子中,count.value++就可以工作了,下次渲染时,count的引用地址并没有变。
我个人是喜欢vue的mutable特性的。
vue对深层次的状态修改更加方便。
让我们把count升级为 bookList[0].store.count
// React
const Button = () => {
const [bookList, setBookList] = useState([{
store: {
count: 0,
}
}]);
const handleClick = useCallback(() => {
setBookList(prev => prev.map((book, index) => index !== 0 ? book : ({
...book,
store: {
...book.store,
count: book.store.count + 1
}
})));
}, []);
return <button onClick={handleClick}>{count}</button>
}
React在不借助第三方工具的情况下,写起来很繁琐。
// Vue
const Button = defineComponent({
setup() {
const bookList = reactive([{
store: {
count: 0,
}
}])
return () => (
<button onClick={() => bookList[0].store.count++}>
{count.value}
</button>
)
}
})
Vue这里引入了另一个composition api: reactive. 相对react,写起来就简单的多了,直接赋值就行。
Vue可以同步的获取状态的最新值。
还是这个例子:
// React
const Button = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
// or setCount(prev => prev + 1)
console.log(count); // 这里输出的count值是滞后的,
}, [count]);
return <button onClick={handleClick}>{count}</button>
}
更新完count之后,再获取count的值,获取到的是旧的值。这是immutable+闭包的必然结果。
闭包导致里面变量的引用不会变,immutable导致变量的值也不会变,所以就不可能获取状态的最新值。
退而求其次,我们只能间接的获得,比如这种写法 setCount(prev => prev + 1),这里prev的值一定是最新的。但是它限制了,你只能获取一个状态的最新值,当组件变复杂时,这个上面的心智还是挺高的,你需要调整状态设定,调整执行顺序,设置临时变量等等。
于是有些同学喜欢把状态进行统一管理,比如用redux,这个问题也是原因之一。不过我一直是反对状态的中心化管理的,它其实就是把很多组件的逻辑代码写到了一起,使得代码变得难以阅读和维护。我们应该去中心化,也就是组件化。
// Vue
const Button = defineComponent({
setup() {
const count = ref(0);
const handleClick = () => {
count.value++;
console.log(count.value); //这里是同步的
}
return () => <button onClick={handleClick}>{count.value}</button>
}
})
vue中就完全没有这种烦恼了,状态一直是同步的。
no setup vs setup
我们看到vue的代码里面有一个setup函数,这个和react不一样,这个函数不是每次渲染都执行,而是在组件第一次渲染之前进行的初始化,只会执行一次。
与react的函数对应的其实setup函数的返回值,它也是一个函数,也就是渲染函数。它每次渲染都会执行。
有setup的好处显而易见,因为只执行一次,速度上一定会有所有提升。
React中你经常需要写useCallback,来缓存一个函数。在vue3中,这完全没有必要,所有定义在setup里面的函数都只会执行一次,不需要刻意缓存。
Proxy vs raw object
相对于React,Vue为了追踪状态的变化,使用了Proxy,来监听状态的使用和修改。这样的好处是,降低了在何时更新上的心智负担。所有都是由vue内部控制的。React中你可能就要经常考虑要不要加memo,要不要加useCallback,要不要加useMemo
但是Proxy也带来了一些编写上的麻烦:
ref状态要不停的写.value
由于Proxy只能作用在object上,无法应用到基础类型上。对于基本类型,vue3给出了ref作为解决方案,用一个对象的value属性来存储这些基本类型。但是带来的问题就是,引用和修改时要写.value。这确实很麻烦。
当然,vue3在一些特殊情况下是可以不用写.value的
- 在模板中不用写.value。这点对我来说,没什么用,因为相对于jsx,我不是很喜欢模本,它的最大问题是,模板不能直接使用js中定义的变量,而是需要你把用到的变量return一下,这就不是很爽。
- 通过reactive访问一个ref类型时,不需要写.value。
const count = ref(10);
const data = reactive({ count });
console.log(data.count) // 10
console.log(isRef(toRaw(data).count)) // true
不能任意解构
解构是指这种语法:const { name, id } = user
原因还是因为Proxy只能作用在object上面,所以当你解构出一个基本类型的变量时,这个变量就不可能是一个Proxy了,它也就失去了追踪的能力。
当你从reactive中解构出了object类型的变量,这个变量将仍然是一个proxy,它仍然可以响应追踪,只是过它不一定是正确的,因为这个变量有可能被上层删除。也因为这个原因,vue3的eslint规则有一条是不要解构props。
const data = reactive({
a : {
b: 1
}
});
const { a } = data;
data.a = { b: 10 };
console.log(data.a === a) // false
// no change after click.
return () => <button onClick={() => a.b = 100}>{data.a.b}</button>
当然了,在一些条件下,是可以安全的进行结构的:比如不需要进行赋值的情况下,那么可以在return的render函数中解构,也可以在computed,watchEffect等中解构。
const data = reactive({
count: 0
});
setInterval(() => {
data.count++;
}, 1000);
return () => {
const { count } = data;
return <div>{count}</div>
}
总结
vue3使用了Proxy,它解决了React的一些问题,比如不容易获得状态的最新值,对深层次object修改麻烦,在处理更新上的心智负担等等。同时它也有一些不足,ref麻烦的.value,解构上存在潜在问题。