import{_ as s,o as n,c as a,R as l}from"./chunks/framework.a304f0f7.js";const A=JSON.parse('{"title":"React Hooks","description":"","frontmatter":{},"headers":[],"relativePath":"note/React Hooks.md","lastUpdated":1682869658000}'),p={name:"note/React Hooks.md"},o=l(`
Hooks 是 React16.8 推出的新特性
在没有Hooks时,类组件能够完成的大部分工作,函数式组件都无法胜任:
componentDidMount
在类组件生命周期只会执行一次类组件存在的问题:
this
的指向问题需要花费额外的心智负担redux
中的 connect
或者 react-router
中的 withRouter
,高阶组件的目的就是为了状态复Hooks带来的优势:
分别使用Hooks和类组件编写一个计数器:
// CounterClass.jsx
import React, { PureComponent } from 'react'
export default class CounterClass extends PureComponent {
constructor() {
super()
this.state = {
count: 0
}
}
setCount(num) {
this.setState({
count: num
})
}
render() {
const { count } = this.state
return (
<div>
<div>CounterClass</div>
<div>{count}</div>
<button onClick={() => this.setCount(count + 1)}>+1</button>
<button onClick={() => this.setCount(count - 1)}>-1</button>
</div>
)
}
}
// CounterFunctional.jsx
import React, { useState } from 'react'
export default function CounterFunctional() {
const [count, setCount] = useState(0)
return (
<div>
<div>CounterFunctional</div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
</div>
)
}
Hook本质上就是一个JavaScript函数,这个函数可以帮你钩入(Hook Into) React State以及其他的生命周期回调
只允许在函数顶层调用Hook,而不能再循环、条件判断或子函数中调用
只能在React的函数组件中调用Hook,不能在其他JavaScript函数中调用
从react
导入useState
undefined
setCount
函数,将状态设置为新的值为什么叫useState
而不叫createState
?
useEffect
这个Hook可以帮我们处理一些副作用,每次组件渲染完成后,React会自动帮我们调用这些副作用
网络请求/手动更新DOM/事件监听
完成上述这些功能的Hook被称为 Effect Hook
通过useEffect
,可以告诉React需要在组件渲染完成后执行哪些副作用
这里的组件重新渲染,指的是组件对应的DOM更新完毕后,回调这些副作用函数
默认情况下,无论是第一次渲染之后,还是每次更新之后,都会执行这个副作用函数
以下面的代码为例,我们希望每次对状态title
进行修改后,都能反映到当前页面的标题上
如果我们将document.title = title
放到函数顶层执行,那么
// TitleChanger.jsx
import React, { useEffect, useState } from 'react'
export default function TitleChanger() {
const [title, setTitle] = useState('Title')
useEffect(() => {
document.title = title
})
return (
<div>
<div>Title: {title}</div>
<button onClick={() => setTitle(new Date().getTime())}>Change Title</button>
</div>
)
}
使用useEffect可以帮我们在组件执行重新渲染后添加额外的副作用,那怎样清理这些副作用呢?
useEffect
添加了事件监听回调的副作用addEventListener <=> removeEventListener
传入useEffect的函数的返回值是一个函数,组件被重新渲染或组件被卸载时,React会调用这个函数
我们可以借助这个功能来完成清理副作用的操作
下面的案例展示了一个输入框,当焦点处于输入框时,按下ESC键时会对输入框进行清空并使其失焦:
// KeydownListener.jsx
import React, { useEffect } from 'react'
export default function KeydownListener() {
const handleKeydown = (e) => {
if (e.key === 'Escape') {
e.target.value = ''
e.target.blur()
console.log('Escape Pressed')
}
}
useEffect(() => {
const input = document.querySelector('input')
input.addEventListener('keydown', handleKeydown)
console.log('Effect Ran')
return () => {
input.removeEventListener('keydown', handleKeydown)
console.log('Effect Cleaned Up')
}
})
return (
<div>
<div>document</div>
<input type="text" />
</div>
)
}
为什么要在effect中返回一个函数?
如果我们有较为复杂的逻辑,我们也可以在同一个组件中使用多个useEffect,并分别在不同的回调函数中返回清理副作用函数
同时,我们也会发现一些问题,尽管我们保证了监听回调是时刻唯一、不会冗余的
但是组件每次重新渲染都会:先删除原来事件监听回调,再添加新的监听回调,这造成了额外的性能浪费
我们可以对此进行额外的优化
useEffect
实际上有两个参数 默认情况下,如果不传第二个参数,没有指定更新依赖项,则只要组件重新渲染,Effect都会重新执行
如果我们为第二个参数传递一个空数组,则证明:此Effect没有依赖state,仅在首次渲染时被回调
// KeydownListener.jsx
...
useEffect(() => {
const input = document.querySelector('input')
input.addEventListener('keydown', handleKeydown)
console.log('Effect Ran')
return () => {
input.removeEventListener('keydown', handleKeydown)
console.log('Effect Cleaned Up')
}
}, [])
...
在之前的开发中,要在组件中使用共享的Context有两种方式
ClassName.contextType = SomeContext
绑定上下文this.context.xxx
获取上下文中共享的状态SomeContext.Consumer
的方式共享上下文状态其中最大的问题就是:多个Context在同时使用时会引入大量的嵌套,而useContext
可以帮我们解决这个问题
通过useContext
可以直接获取到某个上下文中共享的状态变量
// Profile.jsx
import React, { useContext } from 'react'
import { UserContext, ThemeContext } from '../context'
export default function Profile() {
const userContext = useContext(UserContext)
const themeContext = useContext(ThemeContext)
return (
<div>
<div>Profile</div>
<div>userName: {userContext.userName}</div>
<div>age: {userContext.age}</div>
<div>theme: {themeContext.theme}</div>
</div>
)
}
// context/index.js
import { createContext } from 'react'
export const UserContext = createContext({
userName: '',
userAge: 0
})
export const ThemeContext = createContext({
theme: 'light'
})
// App.jsx
import React from 'react'
import { UserContext, ThemeContext } from './context'
import Profile from './components/Profile'
export default function App() {
return (
<div>
<UserContext.Provider value={{ userName: 'Ziu', age: 18 }}>
<ThemeContext.Provider value={{ theme: 'dark' }}>
<Profile />
</ThemeContext.Provider>
</UserContext.Provider>
</div>
)
}
当组件上层最近的SomeContext.Provider
提供的值发生更新时,useContext
会使用上下文中最新的数据触发组件的重新渲染
useReducer
并不是Redux的替代品
useReducer
是useState
在某些场景下的替代方案useReducer
对其进行拆分useReducer
下面举一个例子:用户信息包含多个复杂的字段,当用户执行操作后需要同时对多个字段进行修改
我们分别用useState
和useReducer
来实现:
// UserInfoWithReducer.jsx
import React, { useReducer } from 'react'
function reducer(state, action) {
switch (action.type) {
case 'addAge':
return {
...state,
age: state.age + 1
}
case 'pushLikes':
return {
...state,
likes: [...state.likes, action.payload]
}
case 'modifyName':
return {
...state,
name: action.payload
}
default:
return state
}
}
export default function UserInfoWithReducer() {
const [userInfo, dispatch] = useReducer(reducer, {
id: 1,
name: 'Ziu',
age: 18,
likes: [
{ id: 5, name: 'Why', age: 19 },
{ id: 8, name: 'ZIU', age: 20 }
]
})
const addAge = () => dispatch({ type: 'addAge' })
const pushLikes = (user) => dispatch({ type: 'pushLikes', payload: user })
const modifyName = (name) => dispatch({ type: 'modifyName', payload: name })
return (
<div>
<div>UserInfoWithReducer</div>
<div>{JSON.stringify(userInfo)}</div>
<button onClick={addAge}>Add Age</button>
<button onClick={() => pushLikes({ id: 10, name: 'ZiuChen', age: 21 })}>Push Likes</button>
<button onClick={() => modifyName('ZiuChen')}>Modify Name</button>
</div>
)
}
// UserInfo.jsx
import React, { useState } from 'react'
export default function UserInfo() {
const [userInfo, setUserInfo] = useState({
id: 1,
name: 'Ziu',
age: 18,
likes: [
{ id: 5, name: 'Why', age: 19 },
{ id: 8, name: 'ZIU', age: 20 }
]
})
const addAge = () =>
setUserInfo({
...userInfo,
age: userInfo.age + 1
})
const pushLikes = (user) =>
setUserInfo({
...userInfo,
likes: [...userInfo.likes, user]
})
const modifyName = (name) =>
setUserInfo({
...userInfo,
name
})
return (
<div>
<div>UserInfo</div>
<div>{JSON.stringify(userInfo)}</div>
<button onClick={() => addAge()}>Add Age</button>
<button onClick={() => pushLikes({ id: 10, name: 'ZiuChen', age: 21 })}>Push Likes</button>
<button onClick={() => modifyName('ZiuChen')}>Modify Name</button>
</div>
)
}
从代码中可以看出差距:对于复杂的更新状态逻辑,使用useReducer
可以将他们聚合在一起,通过统一的出口对状态进行更新,而不像useState
方案中需要频繁调用暴露在外部的setUserInfo
状态更新接口
当然,对于复杂数据的处理,也可以将其放到Redux这类的状态管理中,如果没有Redux,那么useReducer
可以在一定程度上扮演Redux的角色,但是本质还是useState
的替代方案
这两个Hook都是用于性能优化
我们以计数器案例来对useCallback
进行说明:
// Counter.jsx
import React, { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
function increment() {
setCount(count + 1)
}
return (
<div>
<div>Counter</div>
<div>{count}</div>
<button onClick={increment}>+1</button>
</div>
)
}
当我们每次点击+1
的button时,整个函数都会被重新执行一次,但是increment
也会被重复定义,虽然上一次组件渲染快照中的increment
函数由于引用次数为0,会被GC机制回收,但是函数的重复定义对性能也存在消耗
但是useCallback
的使用常常存在一个误区:使用useCallback
可以减少函数重复定义,以此来进行性能优化
// Counter.jsx
import React, { useState, useCallback } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
// var1 = useCallback(fn1)
// var2 = useCallback(fn2)
// var1 === var2 === fn0
const increment = useCallback(function () {
setCount(count + 1)
}, [count])
return (
<div>
<div>Counter</div>
<div>{count}</div>
<button onClick={increment}>+1</button>
</div>
)
}
本质上,传递给useCallback
的参数时一个函数,这个函数在每次重新渲染时也会被重新定义,所以useCallback
并没有解决函数重新定义的问题,它只是保证了组件每次访问increment
时,访问到的都是同一个函数引用
useCallback
会返回一个函数的memoized值在使用useCallback
时,我们同样可以为其传入一个依赖数组,仅当依赖数组中的state值发生变化时,useCallback
才会返回一个新的函数
当我们为其传入一个空的依赖数组时,这里涉及到一个概念:闭包陷阱
// Counter.jsx
...
const increment = useCallback(function () {
setCount(count + 1)
}, [])
...
即使我们多次点击+1的按钮,状态count也不会发生变化,这是因为依赖数组为空时,useCallback
每次返回的都是第一次定义时的函数,而那个函数的count
值始终为0,那么每次setCount
得到的值都为1
闭包陷阱:函数在定义时,从上层作用域捕获变量,并保存在闭包中。
后续即使重复定义新的函数,其取到的值仍然是闭包内部保存的变量的值,而这个值是始终没有发生改变的
这里我们使用普通函数模拟了一下这个场景
function foo() {
let count = 0
function bar() {
console.log(count + 1)
}
return bar
}
const bar1 = foo()
bar1() // 1
bar1() // 1
不论调用了多少次bar1
,其内部取到的值都始终是最初的那个count
,自然值也不会发生变化
所以,我们需要显式地为useCallback
指定依赖state,这样才能让函数准确的使用到最新的状态
经过之前的说明,目前useCallback
看起来并没有实际的用途,它没有减少函数的定义次数,甚至在不合理使用时还会出现闭包陷阱,而带来的唯一好处就是:保证函数指向确定且唯一
下面我们举一个实际场景来说明useCallback
的用途: