(一)响应事件
使用 React 可以在 JSX 中添加 事件处理函数。其中事件处理函数为自定义函数,它将在响应交互(如点击、悬停、表单输入框获得焦点等)时触发
1.给组件添加事件处理函数
就像vue的@click="handleClick"一样
但这里要特别注意的是,传入的是一个函数,而不是函数的调用
// 正确写法
<button onClick={handleClick}>点我</button>
// 错误写法 会导致渲染时直接调用函数
<button onClick={handleClick()}>点我</button>// 正确写法
<button onClick={()=>{alert('!')}}>点我</button>
// 错误写法
<button onClick={alert('!')}>点我</button>
2.将事件处理函数作为props传递
父组件的自定义函数可以通过prop传递给子组件,就像vue的@handleClick="handleClick"一样
function Button({ handleClick, children}) {return (<button onClick={handleClick}>{children}</button>);
}export default function App() {return (<div><Button handleClick={() => alert('点击!')}>点击按钮</Button></div>);
}
3.事件传播
和原生js一样,事件也会冒泡
当父组件和子组件都有点击事件时,点击子组件会先触发子组件事件,再触发父组件事件
阻止原生默认行为
当不想触发冒泡传播时,可以通过 e.stopPropagation() 阻止事件冒泡
function Button({ onClick, children }) {return (<button onClick={e => {e.stopPropagation();onClick();}}>{children}</button>);
}export default function Toolbar() {return (<div className="Toolbar" onClick={() => {alert('你点击了 toolbar !');}}><Button onClick={() => alert('你点击了button!')}>上传图片</Button></div>);
}
当点击表单提交按钮,不想触发浏览器自带的提交并刷新事件,可以通过 e.preventDefault() 来阻止
export default function Signup() {return (<form onSubmit={e => {e.preventDefault();alert('提交表单!');}}><input /><button>发送</button></form>);
}
(二)State
1.介绍
组件通常需要根据交互更改屏幕上显示的内容。输入表单应该更新输入字段,单击轮播图上的“下一个”应该更改显示的图片,单击“购买”应该将商品放入购物车。组件需要“记住”某些东西:当前输入值、当前图片、购物车。在 React 中,这种组件特有的记忆被称为 state
与vue的响应式原理不同,vue通过proxy劫持数据的getter和setter实现响应式,如vue3的reactive()
State的创建使用了 const [变量名,变量setter] = useState(初始值) 的形式定义响应式数据
State是隔离且私有的
渲染多个相同组件,其state值是独立的,不受其他组件的影响
2.渲染和提交
- 应用启动时进行第一次渲染,届时所有组件和节点都会进行渲染;
- 当数据改变后,react会重新渲染数据有所改变的组件,尽可能小的减少渲染量(diff算法)
- 提交阶段,将所有节点绘制到浏览器页面
当使用state变量setter函数改变数据后,会引起组件的重新渲染
设置 state 只会为下一次渲染变更 state 的值
如下代码,点击按钮后只会引起一次组件的渲染,number的值是1而不是3
const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 1);setNumber(number + 1);setNumber(number + 1);}}>+3</button></>)
因为渲染时的操作是进行了三次的 setNumber( 0 +1) ,即渲染后number=1!!!
使用异步函数,例如设置一个定时器,在其内部输出number的话,number该是多少呢?
const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 1);setTimeout(()=>{alert(number);},3000);}}>+3</button></>)
在3秒后,弹出的number值还是为0,这就意味着,state像是拍下的一张照片,在重新拍照片之前,state都不会发生改变
在下次渲染前多次更新同一个 state
那如何实现在统一渲染前就更改number的值呢?
const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(n => n + 1);setNumber(n => n + 1);setNumber(n => n + 1);}}>+3</button></>)
使用箭头表达式(叫作更新函数)就可以啦,但是这个操作并不常用
n只是一个形参,用来指代number,取别的名字也是ok的
3.更新State的对象
应当将state视为只读的,不能直接更改state本身,而是将state复制出来,对复制体进行更改后再替换state进行重新渲染
直接改变本身(如position.x=100)后,并不会引起组件的重新渲染
(1)用展开语法复制对象
setPerson({firstName: e.target.value, // 从 input 中获取新的 first namelastName: person.lastName,email: person.email
});
// 需要更改的数据不多时 使用展开语法
setPerson({...person, // 复制上一个 person 中的所有字段firstName: e.target.value // 但是覆盖 firstName 字段
});
展开语法(...)本质是浅拷贝,只会复制浅层数据,当对象嵌套过多时就不太适用了
(2)更新一个嵌套对象
const [person, setPerson] = useState({name: 'Niki de Saint Phalle',artwork: {title: 'Blue Nana',city: 'Hamburg',image: 'https://i.imgur.com/Sd1AgUOm.jpg',}
});// 使用展开语法
setPerson({...person, // 复制其它字段的数据 artwork: { // 替换 artwork 字段 ...person.artwork, // 复制之前 person.artwork 中的数据city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!}
});
使用Immer库实现扁平化
Immer可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。通过使用 Immer,你写出的代码看起来就像是你“打破了规则”而直接修改了对象
下载immer库:npm install use-immer
使用Immer:
import { useImmer } from 'use-immer';updatePerson(draft => {draft.artwork.city = 'Lagos';
});
Immer的作用就是使用proxy劫持person数据,将其包装为draft,当draft数据发生改变时Immer能感知到并作出对应的改变,和vue3的reactive函数原理相同
4.更新State的数组
在JS中,数组也是对象的一种,因此使用state时还是要保证只读
直接使用数组方法对数组进行修改就违背了只读规定了,如pop、shift、push等
(1)向数组添加元素
无需使用push、unshift方法,可以直接用展开运算符进行添加
setArtists( // 替换 state[ // 是通过传入一个新数组实现的...artists, // 新数组包含原数组的所有元素{ id: nextId++, name: name } // 并在末尾添加了一个新的元素]
);
(2)从数组中删除元素
无需使用shift、pop、slice等方法,可以通过filter将需要删除的数据过滤,生成的新数组即为目标数据
setArtists(artists.filter(a => a.id !== artist.id)
);
(3)转换数组、替换数组元素
使用map方法,根据需求修改数组元素后返回数组即可
const nextCounters = counters.map((c, i) => {if (i === index) {// 递增被点击的计数器数值return c + 1;} else {// 其余部分不发生变化return c;}});
setCounters(nextCounters);
(4)向数组中插入元素
通过展开运算符...和slice方法
const insertAt = 1; // 可能是任何索引
const nextArtists = [// 插入点之前的元素:...artists.slice(0, insertAt),// 新的元素:{ id: nextId++, name: name },// 插入点之后的元素:...artists.slice(insertAt)];
setArtists(nextArtists);
(5)拷贝数组
需要进行修改数组元素数据的操作时,可以先拷贝一份,对复制品进行修改再更新state
// 展开运算符
const newPerson = [...person]
// 使用slice方法
const newPerson = person.slice()
(6)使用Immer
不多说了,跟state对象是一样的操作,但是可以直接使用更新数组的方法了