前言
最近一段时间中接手了同事写的一些 React 代码,其中很多地方采用了 React hooks 钩子和函数式编程的思想来书写组件。刚开始理解起来或许有些困难,后面随着开发的深入,发现这些新的 API 和新思想,还挺方便数据状态和业务逻辑的理解、管理、维护和扩展。
尽管 React 16中的 hooks相对于 React 17已经是很久远的新鲜事物,奈何基础薄弱,接下来还是想从头开始梳理一下React hook 、函数式编程和函数式组件相关的知识。
一、React 的两套 API
React 有两套 API:类(class)API 和基于函数的钩子(hooks)API。任何一个组件,都可以通过刚刚所说的两种方式来书写。
类(class)的写法:
class Welcome extends React.Component {
render() {
return <h1>Hello,{this.props.name}</h1>
}
}
钩子(hook)的写法,就是函数:
function Welcome(props) {
return <h1>Hello,{props.name}</h1>
}
这两种写法,作用完全一样。但官方更推荐是用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较“轻”,而类比较“重”。而且,钩子是函数,更符合 React 函数式的本质。
例如类组件1与函数组件2的代码量对比。对于复杂的组件就差的更多了。但是对于初学者,钩子的灵活性较大,不太容易理解,很容易写出混乱不堪、无法维护的组件,不像类组件那样有很多强制的语法约束和规范。
// 类组件1
import React,{Component} from 'react';
class ClassComponent extends Component {
state = {number:0}
componentDidMount() {
this.setState({number:10})
}
render() {
const {number} = this.state
return (
<div>
<p>Class Component</p>
<p>State is {number}.</p>
</div>
)
}
}
export default ClassComponent
// 函数组件2
import React from 'react'
const FunctionComponent = ({number:10}) => {
return (
<div>
<p>Class Component</p>
<p>State is {number}.</p>
</div>
)
}
export default FunctionComponent
二、类和函数的差异
类组件和函数组件拥有不同的写法,也代表着不同的编程方法论。
类(class)是数据与逻辑的封装。组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都放在同一个 class 中。
而对于函数来说,只应该做一件事,就是返回一个值。如果你有多个操作,每个操作都应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事,即返回组件的 HTML 代码,而没有其他功能。
function Welcome(props) {
return <h1>Hello,{props.name}</h1>
}
这里的函数只做一件事,就是根据输入的参数,返回组件的 HTML 代码。这种只进行单纯的数据计算(换算)的函数,在函数式编程里面成为“纯函数”(pure function)。
三、副效应是什么?
纯函数只能进行计算,而对于不涉及计算的操作,比如生成日志、存储数据、改变应用状态等等,应该写在哪里呢?
函数式编程将那些跟数据计算无关的操作,都称为“负效应”(side effect)。如果函数内部直接包含产生副效应的操作,那么这个函数就不再是纯函数了,称之为不纯的函数。
纯函数内部只有通过间接的手段,即通过其他函数调用,才能包含副效应。
四、钩子(hook)的作用
钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。在函数组件的主体部分只应该用来返回组件的 HTML 代码,所有其他的操作即副效应都必须通过钩子引入。
由于副效应非常多,所以钩子有许多种。React 为许多常见的操作(副效应),都提供了专用的钩子。
- useState:保存状态
- useContext:保存上下文
- useRef:保存引用
上面的钩子,都是引入某种特定的副效应,而 useEffect() 是通用的副效应钩子。找不到对应的钩子时,就可以用它。
五、useEffect() 的用法
useEffect() 本身是一个函数,由 React 框架提供,在函数组件内部调用即可。举例来说,如果希望组件加载以后,网页标题(document.title)会随之改变。那么改变标题这个操作,就是副效应,必须通过 useEffect() 来实现。
import React,{useEffect} from 'react'
function Welcome(props) {
useEffect(() => {
document.title = "加载完成"
})
return <h1>Hello,{props.name}</h1>
}
上面的例子中,useEffect() 的参数是一个函数,它就是所要完成的副效应(改变网页标题)。组件加载以后,React 就会执行这个函数。
useEffect() 的作用就是指定一个副效应函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。
六、useEffect() 的第二个参数
当不希望 useEffect() 每次都渲染执行,这时可以使用它的第二个参数,使用一个数组指定副效应函数的依赖项,只有依赖项发生了变化,才会重新渲染。
function Welcome(props) {
useEffect(() => {
document.title = `Hello,${props.name}`
},[props.name])
return <h1>Hello,{props.name}</h1>
}
上面的例子中,useEffect() 的第二个参数是一个数组,制定了第一个参数即副效应函数的依赖项(props.name)。只有该变量发生变化时,副效应函数才会执行。
如果第二个参数是空数组,就表明副效应函数没有任何依赖项。因此,副效应函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再执行了。原因是:副效应不依赖任何变量,无论变量如何变化,副效应函数执行的结果都不会发生改变,所以运行一次就够了。
七、useEffect() 的用途
只要是副效应,都可以使用 useEffect() 引入,它常见的用途主要有以下几种。
- 获取数据(data fetching)
- 事件监听或订阅(setting up a subscription)
- 改变 DOM(changing the DOM)
- 输出日志(logging)
下面是从远程服务器获取数据的例子。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
const [data,setData] = useState({hits: []});
useEffect(() => {
const fetchData = asyn () => {
const result = await axios (url);
setData(result.data);
}
fetchData();
},[]);
return (
<ul>
{
data.hits.map(item => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))
}
</ul>
);
}
export default App;
上面的例子中,useState() 用来生成一个状态变量(data),保存获取的数据; useEffect() 的副效应函数内部有一个 async 函数,用来从服务器异步获取数据。拿到数据后,再用 setData() 触发组件的重新渲染。
由于获取数据只需要执行一次,所以上面 useEffect() 的第二个参数为一个空数组。
八、useEffect() 的返回值
副效应是随着组件的加载而发生的,那么组件卸载时,可能需要清理这些副效应。 useEffect() 允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect() 就不用返回任何值。
useEffect(() => {
const subscription = props.source.subscribe();
return () => {
subscription.unsubscribe();
}
},[props.source])
上面的例子中,useEffect() 在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。
实际使用过程中,由于副效应函数是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。
九、useEffect() 的注意点
使用 useEffect() 时,需要注意如果有多个副效应,应该调用多个 useEffect() ,而不应该合并写在一起。例如下面第一种,即为错误的写法。
function App() {
const [varA,setVarA] = useState(0);
const [varB,setVarB] = useState(0);
useEffect(() => {
const timeoutA = setTimeout(() => setVarA(varA + 1),1000);
const timeoutB = setTimeout(() => setVarA(varB + 2),2000);
return () => {
clearTimeout(timeoutA);
clearTimeout(timeoutB);
}
},[varA,varB]);
return <span>{varA}, {varB}</span>
}
上面的例子是错误的写法,副效应函数里面有两个定时器,但它们并没有关系,其实是两个不相关的副效应,不应该写在一起。正确的做法是将它们分开写成两个 useEffect() 。
function App() {
const [varA,setVarA] = useState(0);
const [varB,setVarB] = useState(0);
useEffect(() => {
const timeoutA = setTimeout(() => setVarA(varA + 1),1000);
return () => {
clearTimeout(timeoutA);
}
},[varA]);
useEffect(() => {
const timeoutB = setTimeout(() => setVarA(varB + 2),2000);
return () => {
clearTimeout(timeoutB);
}
},[varB]);
return <span>{varA}, {varB}</span>
}