ReactHooks:渲染与useState

渲染和提交

组件显示到屏幕之前,必须被 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);
相关推荐
转转技术团队10 分钟前
多代理混战?用 PAC(Proxy Auto-Config) 优雅切换代理场景
前端·后端·面试
南囝coding11 分钟前
这几个 Vibe Coding 经验,真的建议学!
前端·后端
gnip25 分钟前
SSE技术介绍
前端·javascript
yinke小琪39 分钟前
JavaScript DOM节点操作(增删改)常用方法
前端·javascript
枣把儿43 分钟前
Vercel 收购 NuxtLabs!Nuxt UI Pro 即将免费!
前端·vue.js·nuxt.js
望获linux44 分钟前
【Linux基础知识系列】第四十三篇 - 基础正则表达式与 grep/sed
linux·运维·服务器·开发语言·前端·操作系统·嵌入式软件
爱编程的喵1 小时前
从XMLHttpRequest到Fetch:前端异步请求的演进之路
前端·javascript
喜欢吃豆1 小时前
深入企业内部的MCP知识(三):FastMCP工具转换(Tool Transformation)全解析:从适配到增强的工具进化指南
java·前端·人工智能·大模型·github·mcp
豆苗学前端1 小时前
手把手实现支持百万级数据量、高可用和可扩展性的穿梭框组件
前端·javascript·面试
不见_1 小时前
不想再写周报了?来看看这个吧!
前端·命令行