渲染和提交
组件显示到屏幕之前,必须被 React 渲染。主要需要经历以下三个步骤:
步骤1: 触发一次渲染
有两种原因会导致组件的渲染:
- 组件的初次渲染
- 组件(或其父组件)的状态发生改变而触发重新渲染
当应用启动时,会触发初次渲染。一旦组件被初次渲染,就可以调用 set 函数(useState)更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。
可以把这种情况想象成餐厅客人在第一次下单之后又点了茶、点心和其他东西,每次点单到厨房都相当于触发一次渲染。
步骤2:React 渲染组件
在触发一次渲染后,React 会调用组件来确定在屏幕上显示的内容。
这个过程是递归的,如果更新后的组件会返回某个另外的组件(子组件),React 会继续渲染这个子组件。以此类推,这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
如果更新的组件在树中的位置非常高,默认会渲染更新组件中所有的嵌套组件,这种行为可能会影响性能。
在渲染组件的过程中,React 会将更新后的虚拟 DOM 与旧的虚拟 DOM 进行对比,通过 diff 算法找出需要更新的节点,从而在提交阶段应用最少的必要操作来操作真实 DOM,提交性能。
- 真实DOM:浏览器在解析HTML文档时,会将每个标签元素抽象成DOM节点,按照标签元素层次分明的结构,将HTML文档构建成一棵DOM树。
- 虚拟DOM:虚拟DOM本质上是一个js对象,通过对象来表示真实的DOM结构。tag用来描述标签,props用来描述属性,children用来表示嵌套的层级关系。在状态变化时,会重新生成一个新的虚拟DOM,通过将新旧两个虚拟DOM树进行比较,只将差异的节点应用在真实DOM树上。
步骤3:React 把更新提交到 DOM 上
在渲染组件之后,React 仅在渲染之间存在差异时才会更改 DOM 节点。在渲染完成并且 React 更新 DOM 之后,浏览器就会重新绘制屏幕。
state
想要页面对输入做出反应,需要设置 state 触发重新渲染。
state 如同一张快照,React 在调用(渲染)组件时,组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照,其中所有的值都是根据这一次渲染中 state 中的值计算出来的。
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。
js
const [count, setCount] = useState(0);
const onClick = () => {
setCount(count + 1);
setCount(count + 1);
setTimeout(() => {
alert(count);
}, 3000);
}
// 最终结果:点击按钮之后,页面触发重新渲染 count会变为 1,3秒后 alert显示内容为 0
在这次渲染的过程中,state 变量 count 的值为 0,使用替代法可以将事件处理函数变成下面这样:
setCount(0 + 1);
setCount(0 + 1);
setTimeout(() => { alert(0) }, 3000);
除此之外,state 变量还有一个重要的特性:批处理
React 会将一系列的 state 更新加入队列,等到事件处理函数中的所有代码都运行之后再遍历队列,进行 state 更新。这使得可以一次更新多个 state 变量而不会触发太多次的重新渲染。
const [count, setCount] = useState(0);
js
setCount(count + 1);
setCount(count + 1); // 1
// setCount(0 + 1);
// setCount(0 + 1);
js
setCount(count + 1);
setCount(count + 5); // 5
// setCount(0 + 1);
// setCount(0 + 5);
js
setCount(count => count + 1);
setCount(count => count + 1); // 2
// setCount(0 => 0 + 1);
// setCount(1 => 1 + 1);
js
setCount(count => count + 1);
setCount(count + 5); // 5
// setCount(0 => 0 + 1);
// setCount(0 + 5);
js
setCount(count + 1);
setCount(count => count + 5); // 6
// setCount(0 + 1);
// setCount(1 => 1 + 5);
state 更新
state 中可以存储任意类型的 js 值。对于基础数据类型,可以通过直接替换它们的值来触发一次渲染。对于对象或数组,则需要创建一个新的对象或数组并将其传递给 state 的设置函数。
对象:使用 ... 展开语法进行复制
js
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!
}
});
数组
js
const [ products, setProducts ] = useState([
{ name: 'Baklava', count: 1},
{ name: 'Cheese', count: 5},
])
新增:... 数组展开
js
setProducts([
...products,
{ name: 'Spaghetti', count: 2}
]); // 可以加在开头或末尾
插入中间:slice
js
setProducts([
...products.slice(0, 1),
{ name: 'Spaghetti', count: 2},
...products.slice(1),
])
删除:filter
js
setProducts(
products.filter(p => p.count <= 1);
)
更新:map
js
setProducts(
products.map(p => {
if (p.name === 'Cheese'){
return { ...p, count: p.count + 1};
}
return p;
})
)
排序:先拷贝一份
js
const newProducts = [ ...products];
newProducts.sort((a, b) => b.count - a.count);
setProducts(newProducts);