这几天无意间看见一个新版react官方文档,进去看了看,文档还未完成,主要以介绍hooks为主,看了一章之后,感觉讲的还是很不错的,比老版的文档要条理清晰,还是很有收获的。下面就把自己看的这章的收获与总结分享给大家。
要给大家分享的这章的标题是Add Interactivity,它下面包含了几小节,分别是:
- Responding to Events
- State: A component's Memory
- Render and Commit
- State as a Snapshot
- Queueing a Series of State Updates
- Updating Objects in State
- Updating Arrays in State
一. Responding to Events
这一节主要讲的是组件的事件,事件冒泡与事件捕获。
1. 定义事件处理函数
我们常见的事件定义是定义在函数体中,然后在JSX中当作一个属性传递给组件,但这里有一点需要注意:
| 正确 | 错误 | 错误原因 |
|---|---|---|
<button onClick={handleClick} > | <button onClick={handleClick()} > | 这样定义,事件会在渲染时自动执行,而不是点击按钮时执行,因为JSX的{}中给函数加上(),函数是会立即执行。 |
我们除了可以把事件定义在函数体中,也可以定义在JSX中,推荐使用剪头函数定义一个匿名函数
| 正确 | 错误 | 错误原因 |
|---|---|---|
<button onClick={() => alert('...')} > | <button onClick={alert('...')} > | 这个函数也会渲染的时候立即执行 |
2. 事件传播
事件处理器会捕获来自子组件的事件,这种行为是事件的'传播或冒泡',它开始于事件定义的地方,然后向上传递。 如下这段代码,点击每个按钮,触发完当前事件之后,都会触发父级div的事件click
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('You clicked on the toolbar!');
}}>
<button onClick={() => alert('Playing!')}>
Play Movie
</button>
<button onClick={() => alert('Uploading!')}>
Upload Image
</button>
</div>
);
}
All events propagate in React except
onScroll, which only works on the JSX tag you attach it to.在react中除onScroll外的所有事件,都有向上冒泡的特性,onScroll事件只触发在你定义的jsx中
阻止冒泡
function Button({ onClick, children }) {
return (
<button onClick={e => {
e.stopPropagation();
// 这种方式的好处是:可以在这里添加自己的一些额外逻辑,并且可以跟踪整个代码链
onClick();
}}>
{children}
</button>
);
}
export default function Toolbar() {
return (
<div className="Toolbar" onClick={() => {
alert('You clicked on the toolbar!');
}}>
<Button onClick={() => alert('Playing!')}>
Play Movie
</Button>
<Button onClick={() => alert('Uploading!')}>
Upload Image
</Button>
</div>
);
}
事件捕获
即使在子组件阻止冒泡,有时候父组件想捕获每个子组件的事件点击情况,这时就要用到事件捕获,例如:
<div onClickCapture={() => { /* this runs first */ }}>
<button onClick={e => e.stopPropagation()} />
<button onClick={e => e.stopPropagation()} />
</div>
实现方法就是在父组件的事件名称末尾加上'Capture',这个例子中,每个事件都分为三个传播阶段
- 点击按钮时,先向下传播,会执行父组件的
onClickCapture事件 - 然后执行每个子元素的
onClick事件 - 然后会向上传播,执行
onClick事件,这里写了阻止冒泡,这个事件不会再执行
3. 阻止默认事件
export default function Signup() {
return (
<form onSubmit={e => {
e.preventDefault(); // 阻止表单默认提交
alert('Submitting!');
}}>
<input />
<button>Send</button>
</form>
);
}
二. State: A Component's Memory
这一章介绍了第一个hook-useState
1. 组件会记住你设置的state
思考一下const [index, setIndex] = useState(0);这段代码在组件渲染时都发生了什么?
- 初始化时,给index赋了默认值0,会返回
[0,setIndex],并在组件上渲染出来。 - 当点按钮更新
setIndex(index + 1)时,index会变成1,这告诉react触发另一个渲染 - 第二次渲染时,react仍然会看见初始值
useState(0),但发现组件内已经记住了index的最新的值是1,就会取最新的值[1,setIndex],渲染最新的值。
2. 每个组件内的state都是独立的,私有的。
如果在同一个页面引用了一个组件多次,那么每个组件内的state是私有的,不会互相影响。
三. Render and Commit
这一章主要介绍了react是如何渲染组件的
第一步:触发渲染
有两个原因会引起组件的渲染:
- 组件的初始化。应用程序启动时会触发程序的初始渲染,主要依靠
ReactDOM.render来实现 - 组件内的状态理新。更新组件内状态会自动触发组件的更新排队
第二步:渲染组件(渲染就是react调用你的组件)
在触发渲染之后,react会调用你的组件在屏幕上显示
- 初始化渲染时,react会调用根组件。这时会渲染一些html的标签、dom节点,比如ul、li、div等。
- 后续渲染,react会调用其状态更新触发渲染的组件函数。这个过程是递归的,如果一个触发渲染的组件包含另一个组件,react会渲染被包含的组件,以此类推。react会自动计算自上次渲染以来,组件内的属性发生了什么变化,但是直到下次提到交到DOM前,不会处理这些信息。
第三步:提交到DOM上
渲染组件之后,react会修改DOM
- 初始渲染,react会用
appendChild这个API,把创建的DOM节点,显示在屏幕上。 - 后续渲染,react会使用最小的DOM操作(在渲染时计算),使DOM与最新的渲染匹配。
react仅在渲染差异的地方更改DOM节点
最终:浏览器绘制屏幕
渲染完成并更新DOM后,浏览器将重新绘制屏幕。
四. State as a Snapshot
这一章主要讲组件内的state在组件渲染时的变化。
state可能看起来像是可以读写的常规javascript变量,但是它的行为更像是一个快照。设置它,并不会更新当前组件内的state,而是触发组件的更新。
1. 设置状态来触发渲染
想象一下下面的代码渲染的过程是怎么样的?
import { useState } from 'react';
export default function Form() {
const [isSent, setIsSent] = useState(false);
const [message, setMessage] = useState('Hi!');
if (isSent) {
return <h1>Your message is on its way!</h1>
}
return (
<form onSubmit={(e) => {
e.preventDefault();
setIsSent(true);
sendMessage(message);
}}>
<textarea
placeholder="Message"
value={message}
onChange={e => setMessage(e.target.value)}
/>
<button type="submit">Send</button>
</form>
);
}
- 首先
onSubmit执行 setIsSent(true)设置isSent为true,让当前组件更新进入一个更新队列- react根据
isSent的最新值来重新渲染组件
2. 渲染会及时获取快照
Rendering就意味着react正在调用你的组件,组件就是你的函数。从该函数返回的JSX就像是及时的UI快照,它里面的props、事件、变量都是使用它的状态及时计算的。
与照片或者电影不同,返回的UI快照是有交互性的。它包括事件处理函数、指定输入的响应事件等。然后react更新屏幕以匹配当前快照并连接事件处理。所以,当你按下按钮,将从jsx触发click程序。
所以,当react重新渲染时,发生了以下事情:
- react再次调用你的函数
- 你的函数返回一个新的JSX快照
- react更新屏幕用来匹配你返回的JSX快照
作为组件的内存,state在函数执行后并不会消失。
看下面的例子:当我点击+3这个按钮时,number会加几呢?
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);
}}>+3</button>
</>
)
}
结果是每次点击,number只会+1
这里有两点要记住
- 组件会在执行完当前函数,才会执行更新
- 组件更新时,才会拿到最新的值
虽然这个例子执行了3次 setNumber(number + 1),但其实是执行了3次setNumber(0 + 1) ,计算出number的最新值是1,setNumber(0 + 1)会把当前的值替换掉,所以在更新之后拿到的number是1。以此类推,每次点击,都只是+1。
3.随着时间推移的状态
随着时间的变化,状态的值在当前渲染时不会更改,即使是在异步代码中
看下面的例子,点击按钮时,页面和alert分别显示几呢?
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
</>
)
}
答案是: alert显示0,页面显示5
五. Queueing a Series of State Updates
这一章主要讲state的批量更新。
一个状态的更新,将会发起一个渲染的队列。有时你想在更新之前对该值做一些操作,那么这时更新队列就是很有用的。
1. react是批量更新状态
在处理状态更新之前,react会等待当前事件处理程序中的所有代码都已运行。
比如上面的执行3次setNumber(number + 1),它并不是碰见setNumber(number + 1)就更新状态,而是所有setNumber(number + 1)都执行完毕之后,才更新状态,重新渲染。
这很像你去一个餐厅去点菜的场景,你去点菜时,肯定是把所有菜跟服务员说完,服务员都记下了,才会让厨房一起去做这些菜,而不是说点一个菜就去让厨房做一个。
这可以让你更新多个状态,也不会触发太多的重新渲染。也意味着事件处理程序和其中的代码在执行完毕之前,UI不会被更新,这种行为称为批量更新。这让你的程序渲染更新更快,从而也避免了处理仅更新了一半状态的混乱。
react不会跨多个有意义的事件(如单击事件)批量处理,而是保证每个单击事件都是单独处理的。
react只会在相对安全的情况下进行批量处理,例如:不会第一次点击按钮时禁用表单,第二次点击按钮时提交表单。
2. 在下次渲染前多次更新同一个状态
如果想在下次更新前多次修改当前状态,而不是传递下一个状态,可以使用setState(n => n + 1),给更新函数中传入一个回调函数,该函数基于队列中的上一个状态来计算下一个状态。这是一种告诉react用状态值做某事的方法,而不仅仅是替换它。
例如:
import { useState } from 'react';
export default function Counter() {
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 => n + 1被称为更新函数,当你把这个函数传递给状态设置函数时:
- react将在事件处理程序中的其它代码执行完毕后,处理此函数,放入更新队列
- 在下次更新时,react遍历队列并提供最终更新状态
3. 如果你在替换状态后更新状态,会发生什么?
例如:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>Increase the number</button>
</>
)
}
点击事件处理将会执行以下操作:
setNumber(number + 5),初始化时number是0,通过setNumber(0 + 5),计算出放入队列的值是 5- 将
setNumber(n => n + 1)函数放入队列
最终react将更新number为 6
4. 如果在更新状态后替换它,会发生什么?
例:
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>Increase the number</button>
</>
)
}
这个事件处理程序的执行顺序是这样的:
setNumber(number + 5)通过计算后,把number = 5,放入更新队列setNumber(n => n + 1)把n => n + 1 这个更新函数放入更新队列- 把number = 42 放入更新队列
在下次更新时,react会遍历更新队列并执行:
| 队列更新 | n | return |
|---|---|---|
| 替换为5 | 0(未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
| 替换为42 | 6(未使用) | 42 |
所以最终,react存储number的最新值是42,输出在页面上。
你可能发现
setState(x)与setState(n => x)几乎一样工作,只不过这里n没有被用到
最后总结一下:
- 给一个状态更新函数传递函数时,将会把一个函数传入队列中
- 给一个状态更新函数传入替换的值时,会忽略已进入队列的内容
事件处理程序执行完毕后,会触发重新渲染。在重新渲染期间,react会处理更新队列。更新程序函数在渲染间运行,所以更新函数必须是纯函数,并且只返回结果。不要从其内部设置其他状态或者运行其它副作用。
5. 命名约定
通常命名更新函数的变量名为状态的首字母
例:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
六. 更新的状态是对象
状态可以是任何的javascript数据,当然也可以是对象。但不要直接更改对象,相反,当你想要更改对象时,要建立一个新对象(或该对象的副本),然后将状态设置为该新对象。
1. 什么是改变或突变?
到目前为止,我们一直使用数字、字符串和布尔值。这些值是不可变的,意思是不可更改或者只读的,你可以触发重新渲染来替换它们的值。
例如setX(5)x的值从0更新到5,但是数字0本身并没有更改,我们不能对JS内置的变量进行任何的更改。
当我给状态设置一个对象时
例如:const [position, setPosition] = useState({ x: 0, y: 0 }); 从技术上讲,可以更改对象本身的内容,这叫做改变或突变:position.x = 5
尽管react中的状态对象在技术上是可变的,但你应该将它们视为不可变的。就像是字符串或者数字一样。你应该永远替换它们,而不是改变它们。
2. 将状态视为只读
换句话说,你应该将任何处于状态的javascript对象视为只读对象。
3. 使用扩展运算符来复制对象
例如:有一个对象,每次只更新其中某个字段,可以用这种扩展运算符(...)来复制一个新的对象,并且只更改更新属性。
setPerson({
...person, // 复制旧字段
firstName: e.target.value // 重新要改变的字段
});
但是...运算符只是浅复制,只能复制一层的对象属性,如果对象有多层属性,就要使用多次。
用一个事件处理程序,来更改多个表单项
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com'
});
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value // es6的计算属性来动态计算当前值
});
}
return (
<>
<label>
First name:
<input
name="firstName"
value={person.firstName}
onChange={handleChange}
/>
</label>
<label>
Last name:
<input
name="lastName"
value={person.lastName}
onChange={handleChange}
/>
</label>
<label>
Email:
<input
name="email"
value={person.email}
onChange={handleChange}
/>
</label>
<p>
{person.firstName}{' '}
{person.lastName}{' '}
({person.email})
</p>
</>
);
}
4. 更新一个嵌套的对象
例如这样一个嵌套对象,我只想修改 person.artwork.city = 'New Delhi'
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
可以这样来实现
setPerson({
...person, // Copy other fields
artwork: { // but replace the artwork
...person.artwork, // with the same one
city: 'New Delhi' // but in New Delhi!
}
});
其实嵌套这个词并不准备,因为在代码执行的时候,并没有嵌套对象,其实是两个不同的对象
例如:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
let obj3 = {
name: 'Copycat',
artwork: obj1
};
在这里,如果你修改了obj3.artwork.city,这将影响到obj2.artwork.city 、obj1.city。这是因为
obj3.artwork, obj2.artwork和 obj1 是一个相同的对象。如果你以嵌套对象来理解的话,就很难体会到这一点。
七. 更新的状态是数组
数组是另一个可以存储在状态中的可变对象,也应该视为不可变的。与对象一样,当要更新存储在状态中的数组时,可以创建一个新数组或者复制一个数组,然后将状态设置为新数组。
1. 更新数组时,不要改变它
不要用arr[0] = 'bird' 或者用 pop()、push() 来变更数组
| avoid (mutates the array) | prefer (returns a new array) | |
|---|---|---|
| adding | push, unshift | concat, [...arr] spread syntax (example) |
| removing | pop, shift, splice | filter, slice (example) |
| replacing | splice, arr[i] = ... assignment | map (example) |
| sorting | reverse, sort | copy the array first (example) |
2. 给数组添加值
import { useState } from 'react';
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
setName('');
setArtists(
[ // 新建一个数组
...artists, // that contains all the old items
{ id: nextId++, name: name } // and one new item at the end
]
);
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
数组展开运算符,还可以让你把项目放置在原始位置之前来设置默认值
setArtists([
{ id: nextId++, name: name },
...artists // Put old items at the end
]);
这样就可以模仿在数组结尾添加一个数据(push())和在数组开头添加一个数据(unshift())
3. 从数组中删除一个值
从数组中删除一个元素最简单的方法是filter(),换句话说,你将生成一个不包含要删除值的新数组
4. 变换数组
如果要更改数组的某些或所有项,可以使用map()来生成一个新数组
5. 替换数组中的项
通常情况要替换数据中某个值,例如arr[0] = 'bird'这样,可以用map()来实现
6. 插入到数组
有时想要在数组中间插入一个值,可以使用...扩展运算符配合slice()来实现,先用slice()把要插入值之前的数组复制,然后插入值,再用slice()把插入值之后的数组复制,在拼装成一个数组。
import { useState } from 'react';
let nextId = 3;
const initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState(
initialArtists
);
function handleClick() {
const insertAt = 1; // Could be any index
const nextArtists = [
// Items before the insertion point:
...artists.slice(0, insertAt),
// New item:
{ id: nextId++, name: name },
// Items after the insertion point:
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={handleClick}>
Insert
</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
7. 对数组进行其它的更改
有些操作不能直接使用扩展运算符和map(),比如你想要实现反转和排序。你可以先把数组复制一份,再对他们进行更改。
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies' },
{ id: 1, title: 'Lunar Landscape' },
{ id: 2, title: 'Terracotta Army' },
];
export default function List() {
const [list, setList] = useState(initialList);
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
return (
<>
<button onClick={handleClick}>
Reverse
</button>
<ul>
{list.map(artwork => (
<li key={artwork.id}>{artwork.title}</li>
))}
</ul>
</>
);
}
8. 更新数组内对象
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(
initialList
);
function handleToggleMyList(artworkId, nextSeen) {
setMyList(myList.map(artwork => { // 用map返回一个新的数组
if (artwork.id === artworkId) {
// 要改变的对象,返回一个新对象
return { ...artwork, seen: nextSeen };
} else {
// 没有变化的,默认返回原始对象
return artwork;
}
}));
}
function handleToggleYourList(artworkId, nextSeen) {
setYourList(yourList.map(artwork => {
if (artwork.id === artworkId) {
// Create a *new* object with changes
return { ...artwork, seen: nextSeen };
} else {
// No changes
return artwork;
}
}));
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}