相比类组件,函数组件有一些自己的缺陷:
- 类组件可以定义自己的 State,用来保存组件内部的状态;函数组件不可以,因为函数每次调用都会产生新的临时变量,并且改变变量的值的时候,组件也不会重新渲染。
import React from 'react' function FuncComponent() { let message = 'Hello JS' const handleMessageChange = () => { // 修改 message 之后,函数组件无法知道自己是需要重新渲染的 message = 'Hello React' } return ( <h1 onClick={handleMessageChange}>{message}</h1> ) } export default FuncComponent
- 类组件有自己的生命周期,可以在对应的生命周期中完成需要的逻辑;函数组件没有生命周期。
- 类组件在重新渲染时,只会重新执行 render 函数;函数组件在重新渲染时,整个函数都会被重新执行。
针对上面出现的情况,开发者通常都会编写类组件。但是类组件也存在自己的问题,简而言之,就是类组件比较复杂,学习成本和编写成本都比较高。
Hooks 的出现,可以解决上面提到的问题。
Hooks 是 React V16.8 新增的特性。Hooks 的意思是钩子,本质上其实就是 JavaScript 函数,由于函数组件本身是没有 State、生命周期等特性的,Hooks 可以在另外一个地方保存好这些东西,在函数组件需要的时候把这些特性钩进来,可以让开发者在函数组件中就可以使用 State 以及生命周期等其他 React 的特性。
其实 Hooks 就是把类组件中的优势吸收到了函数组件中,对函数组件进行了增强;同时还保持了函数组件的简洁性。
Hooks 的使用规则:
- Hooks 只能在 React 的函数组件或者自定义 Hook 中使用,不能在类组件或者其他普通的 JS 函数中使用。
- Hooks 只能在 React 函数组件的顶层调用,不能在循环、条件判断或者子函数中调用。
import React, {useState} from 'react' function Count() { // 正确 const [count, setCount] = useState(0) // 错误 if (true) { const [count, setCount] = useState(0) } return ( <h1>{count}</h1>) } export default Count
React 默认提供了一些常用的钩子,也可以封装自己的钩子。钩子一律使用 use 前缀命名,便于识别。
最重要的钩子:
React 中最重要的钩子是 useState()
和 useEffect()
。
useState()
状态钩子:
useState()
:给函数组件钩入状态,与类组件中的 State 的功能完全相同。接收一个状态的值作为参数,在函数组件首次渲染时作为初始值,如果不传的话默认为 undefined。返回值是一个数组,数组的第一个元素是当前状态的值,第二个元素是设置状态值的函数,设置状态值的函数更新 State 变量时,是替换它而不是合并它。
在函数执行完退出后,正常来说变量都会消失,但是 State 中的变量会被 React 内部管理保留下来。
State 只在函数组件首次渲染时被创建,在下一次重新渲染时,useState 会返回之前保存的 State。
组件的重新渲染和组件的卸载是两回事。
import React, {useState} from 'react'
function Count() {
console.log('渲染组件') // 函数组件初始渲染会打印;点击 button 按钮,调用 setCount 函数组件重新渲染会打印
// 1. 调用 useState。参数传入 0 作为状态的初始值。返回值是一个数组,利用数组的解构,将状态的值命名为 count,设置状态值的函数命名为 setCount
const [count, setCount] = useState(0)
// useState 也可以接收一个函数作为参数,返回一个状态的初始值
// const [count, setCount] = useState(() => {
// return 0
// })
const handleCountChange = () => {
// 3. 修改状态的值
// 调用 setCount,会做两件事:1.为 count 设置一个新的值;2.重新渲染函数组件,useState 会维护组件内部的状态,所以会根据最新的值返回 DOM
setCount(count + 1)
// 参数也可以是一个函数,接收原来的状态值,返回新的状态值
// setCount((count => {
// return count +1
// }))
}
return (
<>
{/* 2. 获取状态的值 */}
<button onClick={handleCountChange}>{count}</button>
</>
)
}
export default Count
可以在函数组件中多次使用 useState()
定义多个变量,会按照声明的顺序依次执行。
function FuncComponent() {
const [count, setCount] = useState(0)
const [message, setMessage] = useState('Hello JS')
return (
<>
<h1>{count}</h1>
<h1>{message}</h1>
</>
)
}
useEffect()
副作用钩子:
useEffect()
:给函数组件钩入类似于类组件中的生命周期的功能。接收一个函数作为参数,在函数组件渲染完成后会自动回调这个函数,就可以在这个函数中执行一些事件监听、网络请求、手动更新 DOM 等的副作用操作。
可以把
useEffect()
看做componentDidMount()
,componentDidUpdate()
和componentWillUnmount()
这三个生命周期钩子的组合。
import React, {useState, useEffect} from 'react'
function Count() {
const [count, setCount] = useState(0)
// 告知 React,在函数组件渲染完成后要执行的有副作用的代码。相当于给函数组件钩入了 componentDidMount()、componentDidUpdate()
useEffect(() => {
console.log('传入 useEffect 的函数被执行') // 函数组件初始渲染会打印;点击 button 按钮,函数组件重新渲染会打印
document.title = count
})
const handleCountChange = () => {
setCount(count + 1)
}
return (
<>
<button onClick={handleCountChange}>{count}</button>
</>
)
}
export default Count
默认情况下,函数组件无论是第一次渲染,还是更新后重新渲染,都会执行传入 useEffect()
中的那个回调函数。可以给 useEffect()
传入第二个参数,是一个数组,来决定该 useEffect()
在哪些 State 发生变化时才执行。如果只想初始渲染时执行一次,那么第二个参数可以传入一个空数组。
import React, {useState, useEffect} from 'react'
function FuncComponent() {
const [count, setCount] = useState(0)
const [message, setMessage] = useState('Hello JS')
useEffect(() => {
console.log('只有更新 count 时才执行,更新 message 不会执行')
}, [count])
return (
<>
<button onClick={() => setCount(count +1)}>{count}</button>
<button onClick={() => setMessage('Hello React')}>{message}</button>
</>
)
}
export default FuncComponent
在一个函数组件中,可以存在多个 useEffect()
,会按照声明的顺序依次执行。
import React, {useEffect} from 'react'
function FuncComponent() {
useEffect(() => {
document.title = '多个 Effect'
})
useEffect(() => {
const timer = setTimeout(() => {
// ...
}, 1000)
return () => {
clearTimeout(timer)
}
})
return (<h1>多个 Effect</h1>)
}
export default FuncComponent
需要清除的 Effect:
在类组件中,某些副作用的操作,例如定时器等,需要在 componentWillUnmount()
中清除。
在函数组件中,传入 useEffect()
中的函数也可以返回一个清除函数,返回的清除函数会在函数组件重新渲染或者卸载的时候自动执行(初始渲染不会),就可以在这个清除函数中执行一些清除副作用的操作。
返回的清除函数在函数组件重新渲染执行时,会先执行清除上一个 Effect 的副作用。
useEffect(() => { ChatAPI.subscribe(props.id, handleStatusChange) return () => { ChatAPI.subscribe(props.id, handleStatusChange) } })
假如第一次渲染的时候 props 是
{ id: 10 }
,第二次渲染的时候是{ id: 20 }
。
执行的顺序并不是:
- React 清除了
{id: 10}
的 Effect。- React 渲染
{id: 20}
的 UI。- React 运行
{id: 20}
的 Effect。而是:
- React 渲染
{id: 20}
的 UI。- React 清除了
{id: 10}
的 Effect。- React 运行
{id: 20}
的 Effect。
import React, {useEffect} from 'react'
function FuncComponent() {
useEffect(() => {
const timer = setTimeout(() => {
// ...
}, 1000)
// 返回的清除函数会在函数组件重新渲染或者卸载的时候自动执行,可以在其中执行一些清除副作用的操作。相当于给函数组件钩入了 componentWillUnmount()
return () => {
console.log('返回的清除函数被执行') // 函数组件重新渲染会打印;函数组件卸载会打印
clearTimeout(timer)
}
})
return ( <h1>清除机制</h1>)
}
export default FuncComponent