HOOK简介
HOOK是React16.8.0之后出现的
HOOK(钩子)本质上是一个函数(命名上总是以use开头),该函数可以挂载任何功能
组件分为两种:无状态组件(函数组件)、类组件
类组件中的麻烦:
-
this指向问题
-
繁琐的生命周期
-
其他问题...
HOOK专门用于增强函数组件的功能(不能再类组件中使用HOOK),使之理论上可以成为类组件的替代品
官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件
一、State Hook
State Hook是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态
useState
- 使用该函数需要传递一个参数,这个参数的值表示状态的初始值
- 这个函数的返回值是一个数组,该数组长度一定为2
- 第一项:当前状态的值
- 第二项:改变状态的函数
一个函数组件中可以有多个状态,即可以多次使用useState,这种做法非常有利于横向切分关注点。
// 举个例子:
import React, { useState } from 'react' // 记得要先引入useState函数
export default function App() {
// 声明了一个叫"data"的状态
const [data, setData] = useState({ // 这里的const []是ES6中的解构赋值,和HOOK无关
x: 1,
y: 2
});
return <div>
<p >
x: {data.x},y:{data.y}
<button onClick={() => {
// 更新状态,直接替换掉之前的对象,而不是合并(混合)
setData({
...data,
x: data.x + 1
})
}}>x+1</button>
</p>
</div>
}
和类组件中setState的第一个参数可以传入函数一样。useState返回的函数的参数也可以传入一个函数,并且函数的参数是可信任的(实时的)
// 举个例子:
import React, { useState } from 'react'
export default function App() {
console.log("App render")
const [n, setN] = useState(0); //使用一个状态,该状态的默认值是0
return <div>
<span>{n}</span>
<button onClick={() => {
// 点击按钮我们需要多次调用setN来改变状态,即点击按钮一次后,n + 2
// 1.如果像下面这样写,点击按钮后,n只加1
// setN(n + 1) //不会立即改变,事件运行完成之后一起改变
// setN(n + 1) //此时,n的值仍然是0(不是实时的)
// 2.所以可以setN当中写入函数,函数的参数preN是可信任的
setN(prevN => prevN - 1); // 注意:该函数也不会立即执行
//传入的函数,在事件完成之后统一运行。但是这里的prevN是可信任的(实时的)
setN(prevN => prevN - 1);
}}>+</button>
</div>
}
注意:
- useState最好写到函数的起始位置,便于阅读
- useState严禁出现在代码块(判断、循环)中
- useState返回的函数(数组的第二项),引用不变(节约内存空间)
- 使用函数改变状态,若数据和之前的状态完全相等(使用Object.is比较),不会导致重新渲染,以达到优化效率的目的。
- 使用函数改变状态,传入的值不会和原来的数据进行合并,而是直接替换。
- 如果要实现强制刷新组件
- 类组件:使用forceUpdate函数
- 函数组件:使用一个空对象的useState
- 如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象。这一点也是useState的优点。如果使用类组件,不管状态之间有没有联系,都要写在一起
- 和类组件的状态一样,函数组件中改变状态可能是异步的(在DOM事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。
二、Effect Hook
Effect Hook:用于在函数组件中处理副作用
副作用:
- ajax请求
- 计时器
- 其他异步操作
- 更改真实DOM对象
- 本地存储
- 其他会对外部产生影响的操作
函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数
import React, { useState, useEffect } from 'react'
export default function App() {
const [n, setN] = useState(10);
useEffect(() => {
if (n === 0) {
return;
}
//没一次渲染完成后,需要根据当前n的值,1秒后重新渲染
setTimeout(() => { // 特别注意:这里不能使用setInterval,好好理解为什么不能
setN(n - 1);
}, 1000)
}, [n])
return (
<div>
<h1>{n}</h1>
<button onClick={() => {
setN(n + 1);
}}>n+1</button>
</div>
)
}
细节
- 副作用函数的运行时间点,是在页面完成真实的UI渲染之后。因此它的执行是异步的,并且不会阻塞浏览器
- 与类组件中componentDidMount和componentDidUpdate的区别
- componentDidMount和componentDidUpdate,更改了真实DOM,但是用户还没有看到UI更新,同步的。
- useEffect中的副作用函数,更改了真实DOM,并且用户已经看到了UI更新,异步的。
- 每个函数组件中,可以多次使用useEffect,但不要放入判断或循环等代码块中。
- useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
- 该函数运行时间点,在每次运行副作用函数之前
- 首次渲染组件不会运行
- 组件被销毁时一定会运行
- useEffect函数,可以传递第二个参数
- 第二个参数是一个数组
- 数组中记录该副作用的依赖数据
- 当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
- 所以,当传递了依赖数据之后,如果数据没有发生变化
- 副作用函数仅在第一次渲染后运行
- 清理函数仅在卸载组件后运行
- 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
- 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。
三、自定义Hook
State Hook: useState Effect Hook:useEffect
自定义Hook:将一些常用的、跨越多个组件的Hook功能,抽离出去形成一个函数,该函数就是自定义Hook,自定义Hook,由于其内部需要使用Hook功能,所以它本身也需要按照Hook的规则实现:
- 函数名必须以use开头
- 调用自定义Hook函数时,应该放到顶层
例如:
- 很多组件都需要在第一次加载完成后,获取所有学生数据
- 很多组件都需要在第一次加载完成后,启动一个计时器,然后在组件销毁时卸载
使用Hook的时候,如果没有严格按照Hook的规则进行,eslint的一个插件(eslint-plugin-react-hooks)会报出警告
四、Context Hook
useContext:
- 接收一个context对象(React.createContext的返回值)
- 返回该context的当前值
以前的写法:
function Test() {
return <ctx.Consumer>
value => <h1>{value}</h1>
</ctx.Consumer>
}
const ctx = React.createContext();
export default function App() {
return (
<div>
<ctx.Provider value="abc">
<Text />
</ctx.Provider>
</div>
)
}
现在的写法:
import React, { useContext } from 'react';
function Test() {
const value = useContext(ctx); // 使用上下文
return <h1>{value}</h1>
}
五、Callback Hook
函数名:useCallback
用于得到一个固定引用值的函数,通常用它进行性能优化
该函数有两个参数:
- 第一个参数为函数,useCallback会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
- 第二个参数为数组,记录依赖项
该函数返回:引用相对固定的函数地址
// 举个例子:先看一个问题
import React, { useState } from 'react'
class Test extends React.PureComponent { // 这里继承纯组件,属性不变不渲染
render() {
console.log("Test Render") // 渲染时输出
return <div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
}
}
export default function App() {
console.log("App Render") // 渲染时输出
const [txt, setTxt] = useState(1)
const [n, setN] = useState(0)
return (
<div>
<Test text={txt} onClick={() => {
setText(txt + 1)
}} />
<input type="number"
value={n}
onChange={e => {
setN(parseInt(e.target.value))
}}
/>
</div>
)
}
// 以上代码运行之后,我们改变改变input框中的值,打印以下内容:
// App render
// Test render
// 为啥?Test组件的属性text没有变化,为什么还会重新渲染?这是因为onClick的值变了
// 当App重新渲染时,onClick的函数引用变了。这完全不是我们想要的效果!
// 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,
// 则可能导致优化失效
// 使用useCallback:
import React, { useState, useCallback } from 'react'
class Test extends React.PureComponent {
render() {
console.log("Test Render") // 渲染时输出
return <div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
}
}
export default function App() {
console.log("App Render") // 渲染时输出
const [txt, setTxt] = useState(1)
const [n, setN] = useState(0)
const handleClick = useCallback(() => {
setText(txt + 1)
},[txt])
return (
<div>
<Test text={txt} onClick={handleClick} />
<input type="number"
value={n}
onChange={e => {
setN(parseInt(e.target.value))
}}
/>
</div>
)
}
// 改变input框中n的值,值打印:App render
// 因为useCallback中函数的依赖项txt没有发生变化,所以handleClick的引用没有发生变化
六、Memo Hook
用于保持一些比较稳定的数据,通常用于性能优化。有助于比避免在每次渲染时都进行高开销的计算。
useMemo函数:
- 第一个参数传入一个函数,当依赖项不发生变化时,函数不会执行。如果变化,会将函数执行后的返回值作为useMemo自身的返回值
- 第二个参数传入依赖项
如果React元素本身的引用没有发生变化,一定不会重新渲染
import React, {useState} from 'react';
function Item(props) {
return (
<li>{props.value}</li>
)
}
export default function App() {
const [range, ] = useState({
min: 1,
max: 10000
})
const [n, setN] = useState(0);
const list = [];
for(let i = range.min; i <= range.max; i ++) {
list.push(<Item key={i} value={i}></Item>)
}
return (<div>
<ul>{list}</ul>
<input type="number"
value={n}
onChange={e => {
setN(parseInt(e.target.value));
}}
/>
</div>)
}
// 每次改变输入框中的值,都会重新渲染10000次<Item>组件,这显然会消耗大量性能
// 使用useMemo:
import React, { useState, useMemo } from 'react'
function Item(props) {
return <li>{props.value}</li>
}
export default function App() {
const [range,] = useState({ min: 1, max: 10000 })
const [n, setN] = useState(0)
const list = useMemo(() => {
const list = [];
for (let i = range.min; i <= range.max; i++) {
list.push(<Item key={i} value={i}></Item>)
}
return list;
}, [range.min, range.max])
// 当依赖的值不发生变化时,useMemo里面的函数就不会执行。还是使用原来的数据
return (
<div>
<ul>
{list}
</ul>
<input type="number"
value={n}
onChange={e => {
setN(parseInt(e.target.value))
}}
/>
</div>
)
}
大家可能会发现:Memo Hook 和 Callback Hook 很相似,参数第一项都传递函数,第二项都传递依赖项。那它们有什么区别嘛?
Callback Hook 只能用来固定函数,当依赖项不变时,传入的函数的引用不会变化;而Memo Hook虽然第一个参数也是函数,但是固定的是函数的返回值,所以Memo Hook 可以固定任何数据。
七、Ref Hook
useRef函数:
- 一个参数:默认值
- 返回一个固定的对象,对象为
{current: 值}
import React, { useState, useRef } from 'react'
// 如果用React.createRef创建一个ref,那么每次重新渲染
// ref都被得到一个新的对象。
// 但是,其实ref完全没必要是一个新的对象,这样降低效率。是同一个对象即可。
// 所以,可以通过useRef生成一个固定的对象
export default function App() {
const inpRef = useRef();
const [n, setN] = useState(0)
return (
<div>
<input ref={inpRef} type="text" />bei
<button onClick={() => {
console.log(inpRef.current.value)
}}>得到input的值</button>
<input type="number"
value={n}
onChange={e => {
setN(e.target.value)
}} />
</div>
)
}
import React, { useState, useRef, useEffect } from 'react'
// 本来可以在函数外面定义一个timer,然后在函数里面使用
// 但是这样的话,如果这个组被使用了很多次,那么这些组件将共用一个timer,
// 当其中一个timer被注销,其他组件就都不能使用timer了。
// 但是写在里面,每次重新渲染,都会重新生成一个计时器
// 为了解决这个问题,写在里面,还要让每次渲染的计时器是相同的。
// 这就用到了useRef,他就是用来生成一个固定的对象的
export default function App() {
const [n, setN] = useState(10)
const timerRef = useRef()
useEffect(() => {
if (n === 0) {
return;
}
timerRef.current = setTimeout(() => {
console.log(n)
setN(n - 1)
}, 1000)
return () => {
clearTimeout(timerRef.current);
}
}, [n])
return (
<div>
<h1>{n}</h1>
</div>
)
}
八、ImperativeHandle Hook
可以让你在使用ref时自定义暴露给父组件的实例值。
函数:useImperativeHandleHook
import React, { useRef, useImperativeHandle } from 'react'
function Test(props, ref) {
useImperativeHandle(ref, () => {
//如果不给依赖项,则每次运行函数组件都会调用该方法
//如果使用了依赖项,则第一次调用后,会进行缓存,只有依赖项发生变化时才会重新调用函数
//相当于给 ref.current = 1
return {
method(){
console.log("Test Component Called")
}
}
}, [])
return <h1>Test Component</h1>
}
const TestWrapper = React.forwardRef(Test)
export default function App() {
const testRef = useRef();
return (
<div>
<TestWrapper ref={testRef} />
<button onClick={() => {
testRef.current.method();
}}>点击调用Test组件的method方法</button>
</div>
)
}
九、LayoutEffect Hook
useEffect:浏览器渲染完成后,用户看到新的渲染结果之后运行 useLayoutEffectHook:完成了DOM改动,但还没有呈现给用户就开始运行。其用法和useEffect用法、用途都一致,只是运行时间点不同
应该尽量使用useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用useLayoutEffectHook
// 举个例子:
import React, { useState, useLayoutEffect, useRef } from 'react'
export default function App() {
const [n, setN] = useState(0)
const h1Ref = useRef();
useLayoutEffect(() => {
h1Ref.current.innerText = Math.random().toFixed(2);
})
return (
<div>
<h1 ref={h1Ref}>{n}</h1>
<button onClick={() => {
setN(n + 1)
}}>+</button>
</div>
)
}
十、DebugValue Hook
useDebugValue:用于将自定义Hook的关联数据显示到调试栏
如果创建的自定义Hook通用性比较高,可以选择使用useDebugValue方便调试