React开发全析:从生命周期到钩子函数
1. React组件生命周期钩子
React组件和Angular、Vue.js组件一样,有特定的生命周期。对于类组件而言,要接入这个生命周期,只需在组件中实现特定的方法。
以下是一个示例:
import React from 'react';
import ReactDOM from 'react-dom';
export class LifecycleAwareComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
// 渲染逻辑
}
componentDidMount() {
console.log('componentDidMount lifecycle hook called');
}
}
在这个例子中,使用了 componentDidMount 生命周期钩子,它会在组件初始化完成后立即被调用。
React还有许多其他的生命周期钩子,Wojciech Maj制作了一个很棒的交互式图表来展示这些钩子,可访问: http://projects.wojtekmaj.pl/react-lifecycle-methods-diagram 。
下面是常用生命周期钩子的简单说明:
| 生命周期钩子函数 | 描述 |
| — | — |
| componentDidMount | 组件实例化完成,props和state初始化并渲染(通常在DOM中)后调用。可在此发起网络请求加载数据、连接存储(如Redux)等。 |
| componentDidUpdate | 组件更新完成(如props或state改变)后调用。可根据组件新值实现副作用,如发起新的网络请求获取数据。该钩子接收前一个props和state作为参数,便于识别具体变化。 |
| componentWillUnmount | 组件即将销毁时调用。可用于清理操作,如移除存储订阅、取消正在进行的网络请求等。 |
若想尝试更全面的示例,可按以下步骤操作:
1. 安装所需依赖:使用 yarn install (或 npm install )。
2. 运行应用:使用 yarn start (或 npm start )。
3. 启动后,点击不同按钮时查看控制台,观察每个钩子的调用情况。
2. 事件的发射与处理
React组件间的通信不限于父组件向子组件传递props,子组件也能发射事件供祖先组件处理。
之前的例子中,我们已了解如何定义回调函数来响应事件。以 Calculator 组件为例:
render() {
return (
// ...
<button onClick={() => this.add(1)}>+1</button>
// ...
);
}
add(value) {
this.setState((state, props) => {
return {currentResult: state.currentResult + value}
});
}
在这个例子中,组件将按钮子组件的点击事件关联到本地函数。我们已知道父组件如何监听和响应子组件发射的事件,下面看看子组件如何发射自定义事件。
解决方案很简单:父组件可将回调函数作为props传递给子组件,在React中,这被称为渲染props。它们的工作方式与其他props类似,但期望接收一个函数并返回一个React元素,用于组件组合。
以下是父组件和子组件的示例:
// 父组件
import React from 'react';
import {ChildComponent} from "./ChildComponent";
export class ParentComponent extends React.Component {
render() {
return (
<ChildComponent onAdd={this.add}/>
);
}
add(value) {
console.log(`Parent has received the following value: ${value}`);
}
}
// 子组件
import React from 'react';
export class ChildComponent extends React.Component {
render() {
return (
<button onClick={() => this.props.onAdd(1)}>+1</button>
);
}
}
在这个简单的例子中, ParentComponent 定义了 add 方法,并通过 onAdd prop传递给 ChildComponent 。 ChildComponent 接收该方法,并在按钮点击时调用。
使用这类回调函数时, this 关键字的使用可能会有陷阱,可参考: https://reactjs.org/docs/handling-events.html 。实际上,使用TypeScript可避免此类问题。
若要修复问题,需将:
add(value) {
console.log(`Parent has received the following value: ${value}`);
}
改为:
add = (value) => {
console.log(`Parent has received the following value: ${value}`);
}
这样修改后,调用 this.add 就能按预期工作。否则,在 ChildComponent 的点击事件处理程序中调用时, this 会指向传递给 ChildComponent 的props对象。
渲染props是简单的props,但有特定用途。它们有助于在组件间复用逻辑,还能实现组件间的简单通信。
3. 远距离组件间的通信
前面展示了组件向子组件传递props的例子,但当需要交换数据的组件在组件树中距离较远时该怎么办呢?此时有几种选择:
- 继续通过props传递值,但这种方式在组件关系复杂时会变得繁琐,且会导致无关组件间的强耦合。
- 考虑使用React的上下文(Context)功能( https://reactjs.org/docs/context.html ),不过它会限制组件的可复用性,应谨慎使用。
- 使用状态管理解决方案,如Redux或MobX( https://github.com/mobxjs/mobx )。更多模式可参考: https://itnext.io/four-patterns-for-global-state-with-react-hooks-context-or-redux-cbc2dc787380 。
4. 渲染控制
在Vue.js和Angular中,需使用特定的模板语法来确定元素是否渲染、是否可见等。而在React中,由于主要使用JavaScript(或TypeScript)代码编写和操作组件,以编程方式控制渲染非常简单直观。借助JavaScript的强大功能,可将组件赋值给变量,使用 if-else 语句决定渲染内容和方式等。
以下是官方文档中的示例:
class LoginControl extends React.Component {
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
{button}
</div>
);
}
}
在这个例子中,使用 if-else 表达式决定是否渲染登录或注销按钮。
由于JSX代码中可使用任何有效表达式,也可直接在JSX代码中嵌入逻辑表达式,如使用三元运算符:
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
</div>
);
}
更多技巧可参考React官方文档: https://reactjs.org/docs/conditional-rendering.html 。
5. 列表渲染
在Angular和Vue.js中,渲染列表需要特定的语法。为提高渲染效率,可给渲染列表中的每个元素分配唯一的键,以便Angular和Vue能轻松识别并仅重新渲染真正改变的部分。React也有相同的概念。
在React中渲染列表非常简单,因为可使用标准的JavaScript代码,将React元素赋值给变量或常量,可通过 map 操作遍历数组元素来构建组件。
以下是示例:
const membersList = [
{id: 1, name: 'Sébastien'},
{id: 2, name: 'Alexis'},
{id: 3, name: 'John'},
{id: 4, name: 'Doe'}
];
const memberElements = membersList.map((member) =>
<li key={member.id}>{member.name}</li>
);
注意 key 属性,它能让React在DOM树中识别列表的特定元素。 key 的值应稳定且唯一,且这些值仅由React使用,不会添加到props对象中。更多关于 key 的信息可参考: https://medium.com/@adhithiravi/why-do-i-need-keys-in-react-lists-dbb522188bbb 。
下面是一个通过props接收列表并渲染的React组件示例:
function MembersList(props) {
const members = props.members.map((member) =>
<li key={member.id}>{member.name}</li>
);
return (
<ul>{members}</ul>
);
}
const membersList = [
{id: 1, name: 'Sébastien'},
{id: 2, name: 'Alexis'},
{id: 3, name: 'John'},
{id: 4, name: 'Doe'}
];
ReactDOM.render(
<MembersList members={membersList} />,
document.getElementById('root')
);
MembersList 组件期望接收一个成员数组作为props,然后进行渲染。将列表渲染到DOM只需调用 ReactDOM.render(...) 。
6. 内容投影/模板
在Vue和Angular中,组件可让其他组件提供子组件进行渲染。在Vue中,这一功能称为插槽,在Angular中称为内容投影或嵌入(该术语继承自AngularJS),React也有类似功能。
在React中,如果使用单个插槽(也称为空洞)来投影内容,通常使用名为 children 的prop。如果需要定义多个插槽,可自定义命名(如 header 、 content 、 footer 等)。
以下是单个插槽的示例:
// App.js
import React from 'react';
import './App.css';
import {Gruyere} from "./Gruyere";
function App() {
return (
<div className="App">
<Gruyere>
<h1>Hello</h1>
<span>World</span>
</Gruyere>
</div>
);
}
export default App;
// Gruyere.js
import React from 'react';
export class Gruyere extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.props.children}
</div>
);
}
}
Gruyere 组件简单地将传递的子元素作为其模板的一部分进行渲染。更多信息可参考: https://reactjs.org/docs/composition-vs-inheritance.html#containment 。
7. React钩子探索
过去,React类组件比函数组件功能更强大,类组件可以有内部状态、接入生命周期等,而函数组件没有等效的API,若要使用某些功能,需将函数组件转换为类组件,这显然不理想。
React钩子的引入填补了这一空白,它提供了简单的API,使函数组件能够处理状态、接入组件生命周期等。钩子不仅增强了函数组件的能力,还便于在组件间复用代码,尤其是有状态的逻辑。使用钩子可将组件拆分为更小但连贯的函数,避免类组件中相关逻辑分散在不同生命周期方法中的问题。需要注意的是,React钩子不能在类组件中使用。
通过降低函数组件的使用门槛,React团队旨在让新开发者更容易上手。因为类的概念相对复杂,开发者可能会过度追求面向对象,导致代码库变得复杂。钩子还可以组合使用,也能创建自定义钩子。
接下来,我们将介绍一些React内置的钩子。
8. 使用useState钩子定义状态
useState 是最常用的React钩子之一,它使函数组件能够拥有和使用内部状态,React会在组件重新渲染之间维护该内部状态。
在函数组件中调用 useState 钩子可定义一个状态变量和一个用于更新它的函数,也可在单个组件中多次调用 useState 来定义多个状态变量。
以下是一个简单的示例:
import React, {useState} from 'react';
export function Switch(props) {
const [currentSwitchStatus, switchStatus] = useState(false);
return (
<div>
<span>The switch is currently: {currentSwitchStatus? 'ON': 'OFF'} </span>
<button onClick={() => switchStatus(!currentSwitchStatus)}>Change the value!</button>
</div>
);
}
在这个示例中,创建了一个 Switch 组件,默认开关是关闭的,点击按钮可改变其状态。关键代码是 const [currentSwitchStatus, switchStatus] = useState(false); ,传递给 useState 函数的值是新状态变量的初始值。 useState 函数返回一个元组,包含当前状态和用于更新状态的函数。
这里使用了ES2015的数组解构特性,后续会详细解释。在组件的JSX代码中,使用当前状态常量显示 ON 或 OFF ,在按钮的 onClick 事件处理程序中调用状态更新函数来切换值。状态更新函数不会将新值合并到前一个值中,而是直接替换它,这体现了对不可变性的支持。
更多参考可查看: https://reactjs.org/docs/hooks-state.html 。
9. 数组解构
数组解构是ES2015的一个有用特性(TypeScript也支持),它与对象解构类似,但用于数组。
传统的代码可能如下:
const values = ['A', 'B', 'C'];
const first = values[0];
const second = values[1];
const third = values[2];
这种方式虽然可行,但代码冗长。使用数组解构可以更简洁地提取数组元素到其他变量或常量中:
const values = ['A', 'B', 'C'];
const [first, second, third] = values;
也可以提前定义要解构的变量,还能为提取的未定义值设置默认值:
const [first = 0, second = 1, third = 3] = ['A', undefined, 'B', 'C'];
console.log(second); // 1
数组解构还有一些有趣的技巧,例如不使用临时变量交换数组元素:
let x = 13;
let y = 37;
[x, y] = [y, x];
console.log(`${x} - ${y}`); // 37 - 13
还可以跳过数组元素进行解构:
const [x, , y] = [1, 2, 3]; // x === 1, y === 3
最后,还能提取数组的一部分值到变量中,将剩余的值提取到另一个变量中:
const values = ['A', 'B', 'C', 'D'];
const [first, ...allTheRest] = values;
console.log(allTheRest); // 'B', 'C', 'D'
10. 使用useEffect钩子处理副作用
useEffect 钩子是类组件React生命周期方法的函数式对应。它使函数组件能够定义在组件渲染或重新渲染后执行的逻辑。
以下是一个示例:
import React, {useState, useEffect} from 'react';
export function Switch(props) {
const [currentSwitchStatus, switchStatus] = useState(false);
useEffect(() => {
alert('The switch has been activated. Hopefully, this was not by mistake :)');
});
return (
<div>
<span>The switch is currently: {currentSwitchStatus? 'ON': 'OFF'} </span>
<button onClick={() => switchStatus(!currentSwitchStatus)}>Change the value!</button>
</div>
);
}
在这个示例中,调用了 useEffect 钩子并传递了一个函数,每当React更新该组件的DOM时(即每次渲染后),这个函数都会被调用,它实际上是前面提到的 componentDidMount 、 componentDidUpdate 和 componentWillUnmount 生命周期钩子的某种组合。
React开发全析:从生命周期到钩子函数
11. useEffect钩子的依赖项数组
在上面的 Switch 组件示例中, useEffect 每次组件渲染都会执行。但有时我们希望控制 useEffect 的执行时机,这时可以给 useEffect 传递第二个参数,即依赖项数组。
以下是一个带有依赖项数组的示例:
import React, {useState, useEffect} from 'react';
export function Switch(props) {
const [currentSwitchStatus, switchStatus] = useState(false);
useEffect(() => {
alert('The switch status has changed!');
}, [currentSwitchStatus]);
return (
<div>
<span>The switch is currently: {currentSwitchStatus? 'ON': 'OFF'} </span>
<button onClick={() => switchStatus(!currentSwitchStatus)}>Change the value!</button>
</div>
);
}
在这个例子中, useEffect 的第二个参数是 [currentSwitchStatus] ,这意味着只有当 currentSwitchStatus 的值发生变化时, useEffect 中的函数才会执行。如果依赖项数组为空 [] ,则 useEffect 只会在组件挂载时执行一次,类似于 componentDidMount 。
12. 使用useContext钩子进行上下文通信
前面提到远距离组件通信时可以考虑使用上下文(Context)功能,而 useContext 钩子可以让函数组件更方便地使用上下文。
首先,创建一个上下文对象:
import React from 'react';
const MyContext = React.createContext();
export default MyContext;
然后,在父组件中提供上下文值:
import React from 'react';
import MyContext from './MyContext';
import ChildComponent from './ChildComponent';
export function ParentComponent() {
const contextValue = 'This is the context value';
return (
<MyContext.Provider value={contextValue}>
<ChildComponent />
</MyContext.Provider>
);
}
最后,在子组件中使用 useContext 钩子获取上下文值:
import React, {useContext} from 'react';
import MyContext from './MyContext';
export function ChildComponent() {
const value = useContext(MyContext);
return (
<div>
<p>{value}</p>
</div>
);
}
通过这种方式,即使组件在组件树中距离较远,也可以方便地共享数据。
13. 使用useRef钩子
useRef 钩子可以创建一个可变的 ref 对象,它可以在组件的整个生命周期内保持不变。 ref 对象有一个 current 属性,可以存储任何值。
以下是一个使用 useRef 来访问DOM元素的示例:
import React, {useRef} from 'react';
export function InputComponent() {
const inputRef = useRef(null);
const handleClick = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus the input</button>
</div>
);
}
在这个例子中, useRef 创建了一个 inputRef 对象,将其赋值给 input 元素的 ref 属性。当点击按钮时,通过 inputRef.current 可以访问到 input 元素,并调用其 focus 方法。
14. 自定义钩子
除了使用React提供的内置钩子,还可以创建自定义钩子。自定义钩子可以封装可复用的逻辑,提高代码的可维护性和复用性。
以下是一个自定义钩子的示例,用于记录按钮的点击次数:
import React, {useState} from 'react';
function useClickCounter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return [count, increment];
}
export function ClickCounterButton() {
const [count, increment] = useClickCounter();
return (
<div>
<p>You clicked the button {count} times.</p>
<button onClick={increment}>Click me</button>
</div>
);
}
在这个例子中, useClickCounter 是一个自定义钩子,它封装了点击次数的状态和增加点击次数的逻辑。 ClickCounterButton 组件使用这个自定义钩子来实现点击计数的功能。
15. 钩子的使用规则
在使用React钩子时,需要遵循一些规则:
- 只在顶层调用钩子 :不要在循环、条件语句或嵌套函数中调用钩子,确保钩子总是以相同的顺序被调用。
- 只在函数组件或自定义钩子中调用钩子 :不要在普通的JavaScript函数中调用钩子。
以下是一个违反规则的示例:
import React, {useState} from 'react';
export function BadComponent(props) {
if (props.condition) {
const [value, setValue] = useState(0); // 错误:在条件语句中调用钩子
}
return <div>...</div>;
}
正确的做法是将钩子调用放在函数组件的顶层:
import React, {useState} from 'react';
export function GoodComponent(props) {
const [value, setValue] = useState(0);
if (props.condition) {
// 使用value进行一些操作
}
return <div>...</div>;
}
16. 钩子的组合使用
钩子可以组合使用,以实现更复杂的功能。例如,结合 useState 和 useEffect 来实现一个实时更新的时钟组件:
import React, {useState, useEffect} from 'react';
export function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
clearInterval(intervalId);
};
}, []);
return (
<div>
<p>{time.toLocaleTimeString()}</p>
</div>
);
}
在这个例子中, useState 用于存储当前时间, useEffect 用于每秒更新一次时间。 useEffect 返回一个清理函数,用于在组件卸载时清除定时器,避免内存泄漏。
17. React钩子的优势总结
React钩子为函数组件带来了很多优势,总结如下:
| 优势 | 说明 |
| — | — |
| 状态管理 | 让函数组件能够方便地管理内部状态,避免了将函数组件转换为类组件的麻烦。 |
| 生命周期管理 | 提供了类似于类组件生命周期方法的功能,如 useEffect 可以模拟 componentDidMount 、 componentDidUpdate 和 componentWillUnmount 。 |
| 代码复用 | 便于复用有状态的逻辑,通过自定义钩子可以将通用逻辑封装起来,提高代码的可维护性和复用性。 |
| 降低学习门槛 | 对于新开发者来说,函数组件和钩子的概念相对简单,避免了类和对象的复杂使用。 |
18. 总结
通过对React组件的生命周期、事件处理、渲染控制、列表渲染、内容投影以及各种钩子的学习,我们了解到React提供了丰富的功能和灵活的开发方式。从传统的类组件到现代的函数组件和钩子,React不断地在进化,让开发者能够更高效地构建用户界面。
在实际开发中,我们可以根据具体的需求选择合适的方式来开发组件。对于简单的组件,函数组件和钩子可能是更好的选择;而对于复杂的业务逻辑,类组件仍然有其用武之地。同时,合理使用各种钩子可以让代码更加简洁、可维护和可复用。
希望本文能够帮助你更好地理解和使用React,在开发过程中更加得心应手。
下面是一个简单的mermaid流程图,展示了 useEffect 钩子的执行流程:
graph TD;
A[组件渲染] --> B{是否有依赖项数组};
B -- 无 --> C[执行useEffect函数];
B -- 有 --> D{依赖项是否变化};
D -- 是 --> C;
D -- 否 --> E[不执行useEffect函数];
通过这个流程图,我们可以更清晰地看到 useEffect 钩子在不同情况下的执行逻辑。
超级会员免费看
881

被折叠的 条评论
为什么被折叠?



