功能介绍
React Hooks对函数型组件进行增强, 比如让函数型组件可以存储状态,又比如 可以拥有处理副作用的能力. 让开发者在不使用类组件的情况下, 实现相同的功能.
在一个组件当中,只要不是把数据转化视图的代码就都是副作用。 比如: 获取dom元素, 为dom元素添加事件,设置定时器, 以及发送ajax请求。
在类组件中我们通常使用生命周期函数中处理副作用, 在函数是组件中,我们就要使用hooks来处理。
类组件的不足 (hooks要解决的问题)
- 缺少逻辑复用机制 //TODO 待补充
- 类组件通常会变的复杂难以维护
将一组相干的业务逻辑拆分到了多个生命周期函数中 在一个生命周期函数内存在多个不相干的业务逻辑 3. 类成员方法不能保证this指向的正确性
常用的一些钩子函数
1. useState 用于为函数组件引入状态
在我们的认知中,在一个函数中定义的变量,在函数调用完成后就会被释放,所以函数型组件原本是不可保存状态数据的。
有了 useState之后,函数型组件,就可以保存状态了。
useState 内部是使用了闭包来保存数据的。
当状态发生改变时,组件会被重新渲染。
基本使用不再赘述,我们一起来看下使用细节。
- 接收唯一的参数即状态初始值. 初始值可以是任意数据类型.
- 返回值为数组. 数组中存储状态值和更改状态值的方法. 方法名称约定以set开头, 后面加上状态名称.
- 方法可以被调用多次. 用以保存不同状态值.
- 参数可以是一个函数, 函数返回什么, 初始状态就是什么, 函数只会被调用一次, 用在初始值是动态值的情况.
function App() {
const [state, setState] = useState(() => 100) // 参数可以是一个函数
return (
<div className="App">
<span>{state}</span>
</div>
);
}
参数是函数主要是用在初始值是动态值的情况.
function App(props) {
// 假如调用App组件时传入了props,
// 如果传了count 我们就用props的值,如果没传,我们就用默认值
// 那么我们可以这样写
const propsCount = props.count || 0
const [count, setCount] = useState(propsCount)
return (
<div className="App">
<span>{count}</span>
<button onClick={()=>setCount(count + 1)}>button</button>
</div>
);
}
1.1 useState 的使用细节
虽然按照如上写法功能会实现,但是代码其实是有问题的。
当我们点击按钮去改变状态时,App组件是会被重新渲染的。那么“const propsCount = props.count || 0” 这句代码在每次组件重新渲染时都会执行。这是没有意义的,因为这句代码是在获取状态的初始值,只有在组件初次渲染时才有意义。也就是说这句代码只需要被执行一次。这个时候,我们就需要用到useState传递函数的方式。
function App(props) {
const [count, setCount] = useState(() => {
return props.count || 0
// 这样这句代码就只会在初始渲染时执行
//后续组件重新渲染时就不会执行了
})
return (
<div className="App">
<span>{count}</span>
<button onClick={()=>setCount(count + 1)}>button</button>
</div>
);
}
1.2 设置状态值的使用细节
- 设置状态值方法的参数可以是一个值,也可以是一个函数
- 设置状态值方法的方法本身是异步的 🌰
// 设置状态值方法的参数是一个函数
function App(props) {
const [count, setCount] = useState(() => props.count || 0)
function handleCount(){
setCount((count) => {
return count + 1 // 函数的返回值是什么,count就是什么
})
}
return (
<div className="App">
<span>{count}</span>
<button onClick={handleCount}> button </button>
</div>
);
}
🌰 设置状态值方法是异步的,还是上面的handleCount方法
function handleCount(){
setCount((count) => {
return count + 1 // 函数的返回值是什么,count就是什么
})
document.title = count
//假如setCount是同步的,那么此刻我们拿到的count应该是改过的count,
//如果是异步的那么这句代码是不会等到setCount执行完才执行的,
//那么这个时候得到的count就是设置之前的count
}
切回浏览器看代码执行的结果很明显,证明setCount是异步的。 那么怎么解决呢,很简单我们把代码放在回调函数里即可
function handleCount(){
setCount((count) => {
const newCount = count + 1;
document.title = newCount
return newCount
})
}
2. useReducer 是另一种让函数组件保存状态的方式.
使用方法类似redux,这里不再赘述。 直接看代码
import { useReducer } from "react";
export default function App () {
function reducer(state, action){
switch(action.type){
case 'increment':
return state + 1;
case 'decrement':
return state - 1
default:
return state
}
}
const [count, dispatch] = useReducer(reducer, 0);
return (
<div>
<button onClick={()=> dispatch({type:'increment'})}>➕</button>
<span>{count}</span>
<button onClick={()=> dispatch({type:'decrement'})}>➖ </button>
</div>
)
}
相对于useState的优点:
假如App的某个子组件想要改变count值,这个时候我们就不需要去传递多个修改数据的方法, 比如让数据➕1, ➖1等。我们可以直接把dispatch传递给子组件。
子组件通过调用dispatch方法,就可以触发任意一个action对state进行修改。
3. useContext
在跨组件层级获取数据时简化获取数据的代码.
先来看下createContext的写法
const appContext = createContext()
function Foo(){
return (
<appContext.Consumer>
{/* 传递一个回调函数,接收一个形参即为context中存储的值 */}
{
value => {
return <div> {value}</div>
}
}
</appContext.Consumer>
)
}
function App () {
return (
<appContext.Provider value={100}>
<Foo/>
</appContext.Provider>
)
}
export default App
对比useContext的写法
import { useContext } from "react";
const appContext = createContext()
function Foo(){
const value = useContext(appContext)
return (
<div>{value}</div>
)
}
function App () {
return (
<appContext.Provider value={100}>
<Foo/>
</appContext.Provider>
)
}
4. useEffect
让函数型组件拥有处理副作用的能力,类似生命周期函数。
4.1 执行时机
可以把 useEffect 看做 componentDidMount、componentDidUpdate 、componentWillUnmount 这三个函数的组合.
| 写法 | 执行时机 |
|---|---|
| useEffect(() => {}) | componentDidMount,componentDidUpdate |
| useEffect(() => {}, []) | componentDidMount |
| useEffect(() => () => {}) | componentDidUpdate,componentWillUnMount |
接下来一起来验证下吧:
function App () {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(123)
//可以看到组件挂载完成后控制台输出了123,
// 点击按钮更新组件状态,控制台也会输出123
})
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
)
}
export default App
function App () {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(123)
// 组件挂载完成后控制台输出了123,
// 点击按钮更新组件状态,控制台没有输出123
},[])
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
)
}
import ReactDOM from 'react-dom'
function App () {
const [count, setCount] = useState(0);
useEffect(() => {
return () => {
console.log('123')
//点击第一个button更新状态控制台输出
// 点击第二个button卸载组件控制台输出
}
})
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
<button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById("root"))}> 点击卸载组件 </button>
</div>
)
}
4.2 使用方式
只简单介绍useEffect结合异步函数的使用
...
function getData(){
return new Promise((reslove) => {
reslove({count: 0})
})
}
useEffect(async () => {
let result = await getData();
console.log(result);
})
...
可以看到控制台会有报错,我们来分析下出现报错的原因
原本在useEffect函数中是可以返回一个函数的,返回的这个函数是用来做清理操作的,会在组件卸载之前执行,但是按照上面的代码来写的话,我们就把它变成一个异步函数,异步函数的返回值时一个promise对象,这样就改变了useEffect函数原有返回值的类型,所以会有报错。
正确的写法: 我们要在useEffect里写上一个函数自执行,把这个自执行函数变成一个异步函数,然后在内部才能使用await关键字
useEffect(() => {
(async () => {
await axios.get()
})()
})
所以,修改代码如下:
function getData(){
return new Promise((reslove) => {
reslove({count: 0})
})
}
useEffect(() => {
(async function(){
const result = await getData();
console.log(result)
})()
})
5. useMemo
useMemo 的行为类似Vue中的计算属性, 可以监测某个值的变化, 根据变化值计算新值.
useMemo 会缓存计算结果. 如果监测值没有发生变化, 即使组件重新渲染, 也不会重新计算. 此行为可以有助于避免在每个渲染上进行昂贵的计算.
🌰:
function App () {
const [count, setCount] = useState(0);
const result = useMemo(() => {
//点击第一个button修改bool时,虽然视图发生改变,但监测值count没有发生变化,函数并未执行。
console.log(1111);
return count * 2
},[count])
const [bool, setBool] = useState(true)
return (
<div>
<div>
<span>{bool? '真':'假'}</span>
<button onClick={() => setBool(!bool)}>点击修改bool </button>
</div>
<div>
<span>{result}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
</div>
</div>
)
}
6. memo 方法
性能优化, 如果本组件中的数据没有发生变化, 阻止组件更新.
类似类组件中的 PureComponent 和 shouldComponentUpdate
当组件发生重新渲染时,组件的数据有没有发生变化,如果没有就不让组件重新渲染难。
🌰
一般来说,点击button改变App组件的count值,Foo组件也会重新渲染。
代码如下
function Foo(){
console.log('Foo组件重现渲染了') //控制台会有输出
return (
<div>我是Foo组件</div>
)
}
function App () {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
<Foo/>
</div>
)
}
引入memo方法后, 点击修改count值可以看到控制台不再有信息打印。也就是说修改了count值,Foo组件未被重新渲染。这样就提高了应用的性能。
const Foo = memo(function Foo(){
console.log('Foo组件重现渲染了')
return (
<div>我是Foo组件</div>
)
})
function App () {
const [count, setCount] = useState(0);
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
<Foo/>
</div>
)
}
7. useCallback
对函数进行缓存。使组件重新渲染时得到相同的应用实例。 🌰:
const Foo = memo(function Foo(props){
console.log('Foo组件重现渲染了')
return (
<div>
<div>我是Foo组件</div>
<button onClick={props.resetCount}>reset count</button>
</div>
)
})
function App () {
const [count, setCount] = useState(0);
const resetCount = () => {
setCount(0)
}
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
<Foo resetCount={resetCount}/>
</div>
)
}
问题:
点击按钮改变count,可以看到控制台也在打印信息,也就是Foo组件重现渲染了。那么究竟是为什么呢?
因为点击按钮的时候改变了count值,当改变count值时App组件会被重新渲染,组件被重新渲染时每次都会生成不同的resetCount函数,也就是每次给Foo组件传递的都是不同的resetCount函数,所以Foo组件就被重新渲染了。那么我们要怎么优化下呢?
很简单,如果App组件重新渲染时都能得到一个相同的resetCount函数,这样Foo组件就不会被重新渲染了。
const Foo = memo(function Foo(props){
console.log('Foo组件重新渲染了')
return (
<div>
<div>我是Foo组件</div>
<button onClick={props.resetCount}>reset count</button>
</div>
)
})
function App () {
const [count, setCount] = useState(0);
const resetCount = useCallback(() => {
setCount(0)
},[setCount])
return (
<div>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>点击</button>
<Foo resetCount={resetCount}/>
</div>
)
}
8. userRef 获取dom元素 & 保存数据(跨组件周期)
8.1 获取dom元素
使用方法不再赘述
8.2 保存数据
- 使用useState保存数据是组件的状态数据,修改useState里保存的状态数据,组件会重新渲染
- 使用useRef保存数据,即使组件重新渲染,保存的数据还在。修改useRef保存的数据不会触发组件的重新渲染
那么要怎么使用呢? 🌰
function App () {
const [count, setCount] = useState(0);
let timerId = null;
useEffect(() => {
timerId = setInterval(() => {
setCount(count => count + 1)
},1000)
},[])
const stopCount = () => {
console.log(timerId)
clearInterval(timerId)
}
return (
<div>
<span>{count}</span>
<button onClick={stopCount}>点击</button>
</div>
)
}
我们点击按钮想清除timerId,但是点击按钮后发现计时器并未清除,count值还在累加。
因为计时器改变了count值,状态发生变化组件就会重新渲染,那么let timerId = null,这句代码就会重新执行,所以我们拿到的timerId一直都是null。
修改代码如下:
function App () {
const [count, setCount] = useState(0);
let timerId = useRef(null);
useEffect(() => {
timerId.current = setInterval(() => {
setCount(count => count + 1)
},1000)
},[])
const stopCount = () => {
console.log(timerId)
clearInterval(timerId.current)
}
return (
<div>
<span>{count}</span>
<button onClick={stopCount}>点击</button>
</div>
)
}
由此可以证明 使用useRef保存数据,即使组件重新渲染,保存的数据还在。