前置说明
已发布:
本篇要点:给UI增加交互性
Vue快速转React指南(二):
- state和props
开始
state、props、event等
1.Event事件
让UI动起来的第一步就是绑定事件,事件的使用上几乎和原生JS没什么区别。这里不多说。
export default function Button() {
function handleClick(e) {
console.log(e);
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
2.props
react里的props
和Vue里的props
都是一个意思,表示传入的参数。
比如上面的例子,需要在点击的时候弹出我们传入的message
:
export default function Button({ message }) {
function handleClick(e) {
console.log(e);
alert(message);
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
Button函数入参就是props
参数,我们使用ES6的解构语法,将它解构为{message}
。
这和Vue3的setup函数一致,setup的第一个入参也是props
。
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: ['msg'],
setup(props) {
// setup
}
})
</script>
3.state⚠️
react里state的意义和Vue2里的data
、Vue3里的ref
或者reactive
包裹产生的变量等价。
要想实现数据<->视图相互影响,在Vue里的规则就是数据必须是响应式数据,我们需要将它包裹在data
里。
那么,在react里同样,要想数据可以影响视图的变化,就需要useState
这个函数来申明变量,可以把这个简单对比理解为Vue3里的reactive
。
下面举例说明一下:
export default function Gallery() {
let index = 0; // 不使用setState申明
function handleClick() {
index = index + 1;
}
return (
<>
<button onClick={handleClick}>
+1
</button>
{index}
</>
);
}
此时界面会渲染出来:
但是点击+1
的按钮界面并不会再次渲染,因为此时的index
属于一个函数内的临时变量。然后,使用useState
来修改一下:
import {useState} from 'react'
export default function Gallery() {
let [index, setIndex] = useState(0);
function handleClick() {
setIndex(index +1);
}
return (
<>
<button onClick={handleClick}>
+1
</button>
{index}
</>
);
}
useState
函数的定义:function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
传入的参数是一个变量的初始值,返回的是使用数据构出两个变量,一个是index
,另一个是setIndex
函数用来更新index
值。这里setIndex
是在react里一个建议的规范,对变量进行更新的函数取名为setXxx
这样的名字。
这里变量index
的改变也是有规则的,直接使用index = index + 1
是没办法触视图更新的,需要使用框架内部导出的函数setInedx
来间接改变。
可以把导出的setXxx
函数理解成这样:
// 传入对Xxx进行修改的表达式
function setXxx(expression) {
Xxx = expression;
}
和Vue3里用ref
包裹的变量一样,不能使用index=index+1
更新而是要使用index.value
做改变一样,底层有一些自己的实现的规则,用户在使用的时候要遵守这个规范。
state和响应式变量的区别
这一部分主要讲state和响应式变量的区别。Vue里就用ref
来申明响应式变量。
对于上面的例子,在Vue里可以这么写: (这里采用jsx来写,因为这样和react更接近)
import { defineComponent, ref } from "vue";
export default defineComponent({
setup(props) {
let index = ref(0);
function handleClick() {
index.value = index.value + 1;
}
return () => (
<div>
<button onClick={handleClick}>
click
</button>
{index.value}
</div>
);
}
});
区别一:state是一次快照;响应式数据劫持数据的变化,不是一次的快照。
将上面的例子修改:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
function handleClick() {
setNumber(number + 1)
}
return (
<>
<h1>{number}</h1>
<button onClick={() => {
handleClick();
handleClick();
handleClick();
}}>+3</button>
</>
)
}
此时点击+3
,大家觉得number
会显示3吗?错❎,number
是1。一次快照的意思是在组件进行一次更新时就算一次快照,此时的number
变量会在当前这一次快照保持不变,哪怕使用了异步:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
handleClick();
handleClick();
handleClick();
setTimeout(() => {
alert(number); // 0
}, 3000);
}}>+5</button>
</>
)
哪怕此时是异步,number
返回的是执行那次的快照,所以number
在当前这一次快照里始终是0
。就像你在餐馆点餐一样,react是等你点完了菜之后才会把菜单传递给厨房,不会你点一个菜就传递给厨房一次。
在Vue里:
import { defineComponent, ref } from "vue";
export default defineComponent({
setup(props) {
let index = ref(0);
function handleClick() {
index.value = index.value + 1;
}
return () => (
<div>
<button onClick={() => {
handleClick();
handleClick();
handleClick();
setTimeout(() => {
alert(index.value); // 3
}, 3000);
}}>
click
</button>
{index.value}
</div>
);
}
});
此时点击+3
按钮,index
显示的是3! 并且在setTimeout
里返回的也是3。这就是Vue和react底层实现细节不同产生的不同表现。Vue的响应式系统可以劫持到数据的任何改变,然后将这些改变整合在一起里进行修改。
从react的角度,为什么state会表现成这样,正是因为没有响应式系统无法劫持到数据的改变,只能通过存储当前变量在上一次快照的值来完成下一次快照的取值。这就导致在每一次快照取到的都是上一次储存的state,自然执行三次setNumber(number + 1)
都是在执行setNumber(0 + 1)
,那么最终的结果就是1并不是3。
区别二:state为Object类型的数据,改变这类数据的方式state和Vue完全相反。
- 在react里,对比state有没有改变使用的是
Object.is
方法。
对于对象和数组这类数据,比较的是内存地址,所以只是单纯的修改对象里的属性是不会触发更新的,因为内存地址并没有变,Object.is
返回是true
。改变对象的内存地址才可以使得Object.is判断为false进而进行重新更新渲染,比如对对象进行重新赋值操作。
- 在Vue里是通过
Proxy
去跟踪某个对象的方式来实现更新,所以不能改变这个对象的内存地址。
因为你改变了之后Vue就无法继续跟踪这个对象了,那么你后续的修改就无法触发Proxy
的代理。因此,Vue3里对reactive
对象的修改只能通过修改属性的方式,不能进行赋值操作。
举例:
实现一个跟随鼠标移动的红点
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
setPosition(position);
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
);
}
上面代码里:
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
console.log(postion);
setPosition(position);
}}
虽然打印出position
已经改变,但是上面这种方式打印是无法触发更新的。因为上面的方式只是改变了position
属性的值,它的内存地址并没有改变,所以在react里使用Object.is
对比的话返回的是true
表示数据并没有改变,自然也不会更新。
所以需要创建一个新的对象数据去更新,那在JS里就需要实现对对象的深拷贝了。深拷贝之后再去修改需要修改的数据。
onPointerMove={e => {
const p = { ...postion };
p.x = e.clientX;
p.y = e.clientY;
setPosition(p);
}}
于是乎,在未来的每一个对象要修改属性都需要通过赋值的方式:
比如有一个嵌套的对象
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
如果你只是想要更新一下artwork
里的city
,你需要把整个对象深拷贝然后修改city
之后再赋值:
setPerson({
...person, // 先拷贝第一层
artwork: {
...person.artwork, // 再拷贝第二层
city: 'New Delhi' // 拷贝的同时修改我们需要修改的数据
})
而在Vue里只需要修改city
的属性就好了。可以看出对象类型的state在react里修改是一个非常极其麻烦的操作。其实在这里也可以看出,react的编写需要一定的JS基础,假如你对深拷贝并不熟悉的话很容易写错代码导致最终更新异常。
为此,react还专门写了一个包叫immutable.js去帮助用户简化这个过程。mutable
是可共享的意思,就像对象的地址一样,那immutable
就是不可共享的。使用这个去将对象类数据变为不可共享的,来代替用户自己做深拷贝的行为。
使用这个包之后,这个包内部的fromJS
方法会将对象变为Map
类型,将数组变为List
类型,然后toJS
方法会将数据恢复为JS类型的数据。具体用法大家可以查教程单独学习。
这个包很需要了,但是要手动引入,react并没有把它装入官方脚手架。
最后
回顾本篇文章要点: props、state与Vue的区别。
预告: 下一篇主要讲React Hooks vs. Vue Composition API