前言
之前有讲过如何从0开始入门React,今天我们来进阶一下,学习React 的钩子:useState、useEffect 。本篇文章结合 原生JS 来讲解。
一、useState
在 React 中,useState
是一个用于给函数组件添加状态的 Hook。它允许你在不编写类的情况下使用状态这一概念。
1.1 原生JS修改状态
假设我们需要创建一个简单的计数器功能,使用原生 JavaScript 实现如下:
js
function createState(initialState) {
let state = initialState;
function getState() {
return state;
}
function setState(newState) {
state = newState;
render(); // 模拟状态更新后的重新渲染
}
function render() {
console.log(`当前状态: ${state}`);
}
return { getState, setState };
}
// 创建一个计数器状态实例
const counter = createState(0);
console.log(counter.getState()); // 输出: 当前状态: 0
counter.setState(1); // 输出: 当前状态: 1
console.log(counter.getState()); // 输出: 当前状态: 1
在这个例子中,我们定义了一个
createState
函数,它接受一个初始状态作为参数,并返回两个方法:getState
和setState
。getState
方法用于获取当前的状态值,而setState
方法用于更新状态并触发一次"重新渲染"(这里通过调用render
函数模拟)。
1.2 useState响应式更新
jsx
import React, { useState } from 'react';
function ExampleComponent() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在这个例子中,我们初始化了一个名为
count
的状态变量,并设置其初始值为 0。setCount
函数用于更新这个状态。
对比原生js可以看出 useState 提供了多项优势:
响应式更新 :
- 自动重新渲染 :当使用
useState
更新状态时,React 会自动检测到这些变化,并触发组件的重新渲染。这与手动使用原生 JavaScript 更新 DOM 相比,极大地简化了状态管理和UI同步的过程。 - 减少副作用:在原生 JavaScript 中,你需要自己管理DOM更新的逻辑,包括何时以及如何更新。而 React 通过其声明式的编程模型帮助你减少了这种复杂性。
二、useEffect
React 组件也有"出生、长大、死亡"的过程,useEffect
是控制这些阶段的关键工具,你可以用它来模拟原生中的各种行为(比如加载完执行代码、卸载前清理)。
2.1 原生 JS 中的"生命周期"是什么?
我们先回顾一下原生 JS:
html
<div id="app"></div>
<script>
// 创建一个元素
const div = document.createElement('div');
div.innerText = 'Hello';
// 添加到页面上(挂载)
document.getElementById('app').appendChild(div);
// 后续可能会修改内容(更新)
div.innerText = 'World';
// 最后可能移除它(卸载)
div.remove();
</script>
在这个过程中,我们能观察到几个关键阶段:
- 创建元素 → 类似于组件被创建
- 添加到页面中 → 组件挂载(mounted)
- 内容发生变化 → 组件更新(updated)
- 从页面中移除 → 组件卸载(unmounted)
在 React 中,组件也有类似的过程,叫做"生命周期"。
2.2 React 函数组件的生命周期 -> 类比原生JS
2.2.1 挂载阶段(Mount)→ 第一次显示
类比:把新元素插入到 DOM 中
jsx
function App() {
console.log("组件开始执行");
return <div>Hello, React!</div>;
}
当
App
组件第一次被渲染时,函数就会执行。 就像在原生里写了一个document.createElement()
并把它插入到了页面中。
2.2.2 首次渲染完成(Did Mount)→ 已经显示出来了
类比:元素已经出现在页面上,可以做些操作了
jsx
useEffect(() => {
console.log("组件首次渲染完成");
}, []);
这个时候你可以操作 DOM、发送请求、设置定时器等。 类似于在原生中写
document.getElementById(...)
然后修改样式或绑定事件。
2.2.3 更新阶段(Update)→ 数据变了,界面要刷新
类比:DOM 内容发生了变化
jsx
const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>点我</button>
<p>当前计数:{count}</p>
每次点击按钮,
count
变了,组件会重新执行(整个函数重新运行一遍 )页面上的数字也会更新,就像在原生里写了element.innerText = newValue
2.2.4 清理副作用(Will Unmount)→ 要离开页面前做一些清理
类比:删除定时器、取消
jsx
useEffect(() => {
const timer = setInterval(() => {
console.log("定时器运行中...");
}, 1000);
return () => {
clearInterval(timer); // 组件卸载前清除定时器
console.log("组件即将卸载");
};
}, []);
这个返回的函数会在组件被销毁前调用 。类似于在原生中做了
timerId && clearInterval(timerId)
来防止内存泄漏。
React 生命周期总结
React 生命周期阶段 | 对应原生行为 | Hook 写法 |
---|---|---|
挂载(Mount) | 元素被插入 DOM | 组件函数执行 |
首次渲染完成(DidMount) | 元素已插入成功,可操作 DOM | useEffect(() => {}, []) |
更新(Update) | 数据改变,视图更新 | 修改 state,组件重新执行 |
卸载(Unmount) | 元素被移除 | useEffect(() => { return () => {} }, []) |
2.3 依赖数组
依赖数组控制着 useEffect
内部逻辑何时执行 。如果提供了依赖项(变量或状态),当这些依赖项发生改变 时,useEffect
将会重新运行。
jsx
useEffect(() => {
console.log('effect triggered');
}, [count]);
在这个例子中,每当
count
变化时,效果就会被触发。如果没有提供依赖项或者依赖项为空数组[]
,那么这个useEffect
只会在组件挂载和卸载的时候运行一次。
2.4 异步函数处理
React 期望 useEffect
的返回值要么是 undefined
(即不返回任何东西),要么是一个清理函数。
jsx
useEffect(() => {
// 要重新声明一个异步函数,不可以直接在useEffect的回调函数中使用
const fetchData = async () => {
try {
const response = await fetch('your-api-url');
const data = await response.json();
// 使用数据更新组件状态
} catch (error) {
console.error("Error fetching data: ", error);
}
};
fetchData();
}, []); // 空数组意味着仅在组件挂载和卸载时运行
如果尝试直接将
useEffect
的回调定义为异步函数(async 函数),它会隐式地返回一个** Promise**,而不是预期的函数或undefined
,这就是为什么不能直接在useEffect
中使用 async 函数的原因。
总结
通过探讨 useState 和 useEffect,我们看到了 React 如何简化状态管理和副作用处理,使得代码更加清晰、易于维护。无论是响应式的数据更新还是生命周期管理,Hooks 提供了一种强有力的方式来增强我们的应用开发流程。