基本介绍
1. 概念及作用
- Hooks,意为钩子,React Hooks就是一堆钩子函数,react通过这些钩子函数对函数式组件的增强
- 让函数型组件可以存储状态,可以拥有处理副作用的能力,
不同的钩子函数提供了不同的功能,让开发者在不使用类组件的情况,实现相同的功能
2. 副作用
在一个组件中,只要不将数据转换成视图的代码就属于【副作用】,
- 如:获取 DOM 元素并为其添加事件、设置定时器、发送 ajax 请求等。
- 在类组件中通常使用生命周期函数处理副作用,而函数组件使用 Hooks 来处理副作用
3. 类组件的不足(hooks要解决的问题)
- 缺少逻辑复用机制(要说一下HOC和render Props的缺点,从而引出hooks)
- 为了复用逻辑增加无实际渲染效果的组件,增加了组件层级 显示十分臃肿,增加了调试的难度以及运行效率的降低
- 类组件经常会变的很复杂难以维护
- 将一组相干的义务逻辑拆分到了多个声明周期函数中
- 在一个生命周期函数内存在多个不相干的业务逻辑
- 类成员方法不能保证this指向的正确性
React Hooks使用
【钩子函数】React 通过这些钩子函数,对【函数型组件】进行增强,不同的钩子函数提供了不同的功能
2.1 useState()
- 让函数型组件在重新渲染后还能保留 state 的状态
- 用于为函数组件引入状态,一个函数调用完之后,里面的变量就被释放掉了,useState是使用闭包来保存状态的
function App(){
const [ count, setCount ] = useState(0)
return (
<div>
<span>{count}</span>
<button onClick={()=>setCount(count+1))}> + 1</button>
</div>
)
}
特性:
- 接受唯一的参数(也就是状态初始值),初始值可以是任意数据类型。
- 参数可以是一个函数,函数返回什么,初始状态就是什么,函数只会被调用一次,用在初始值是动态值的情况。
- 返回值为数组,数组中存储状态值和更改状态值的方法。方法名称约定为以set开头,后面加上状态名称
- 方法可以被调用多次,用以保存不同的状态
- 设置状态方法本身是异步的,可以在参数是一个函数中拿到修改后的count
注意1: useState 传入函数设置初始值 ```function App(){
const [ person, setPerson ] = useState({name:'Fanny',age:'26'})
return (
<div>
<span>{person.name}{person.age}</span>
<button onClick={()=>setPerson({...person,name:'Vicky'})}> + 1</button>
// 如果不对person进行浅拷贝,那么age会为空
</div>
)
}
function App(){ const propsCount = props.count || 0; const [ count, setCount ] = useState(propsCount) // 每次旋绕都会获取,没有意义。只需给useState设置一个初始函数即可。 return (
修改后
function App(){ const [ count, setCount ] = useState(()=>props.count||0) // 只有第一次渲染组件被执行,重新渲染不会执行。 return (
useState(普通值||返回普通值的初始值函数),初始值函数只会执行一次。<br />如果这个初始值是外部接受来的,最好使用初始值函数设置初始值。
注意2: setCount是异步函数<br />setCount(这里可以传入函数,直接在这个函数中处理返回值的依赖值)<br />也就是说,如果 `count` 现在是 `0` ,在 `setCount(count+1)` 后紧接着输出 `count` ,它的值仍然为 `0` ,要获取最新的 `count` 值可以在 `setCount` 中对 `count` 进行处理后得到最新的值
setCount(count=>{ const newCount = count + 1; document.title = newCount; return newCount; })
<a name="jCTik"></a>
#### 2.2 useReducer()
useReducer是另一种让函数组件保存状态的方式<br />它的使用方式与 Redux 中的 reducer 非常相似
```javascript
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'})}>
<span>{count}</span>
<button onClick={()=>dispatch({type: 'decrement'})}>
</div>
)
}
useReducer VS useState
- useReducer 可以合并多个 setState 方法,也就是可以对变量做出多种改变,
- useState 只能对变量作一种改变,每次改变需要定义相应的方法,但结构相对简单
- useReducer 可以把一堆改变变量的方法(dispatch)直接传递给子组件,子组件瞬间 buff(比如子组件需要修改富组件状态,可以把dispatch传递下去)
2.3 useContext()
在【跨组件层级】获取数据时,简化获取数据的代码
import React ,{createContext} from 'react';
const countContext = createContext();
function App() {
return (
<countContext.provider value={100}>
<Foo />
</countContext.provider>
)
}
function Foo() {
// 繁琐的写法,需要优化
return <countContext.Consumer>
{
value =>{
return <div>{value}</div>
}
}
</countContext.Consumer>
}
export default App;
import React ,{createContext} from 'react';
const countContext = createContext();
function App() {
return (
<countContext.provider value={100}>
<Foo />
</countContext.provider>
)
}
function Foo() {
const value = useContext(countContext)
return <div>{value}</div>
}
export default App;
优点
- 跨多个层级的组件也能获取到上层组件传递的值,不用一级一级传递
缺点
-
2.4 useEffect()
class 组件使用【生命周期函数】来处理【副作用】
useEffect 就是用来在函数组件中处理副作用的,根据不同的参数,等价于类组件的componentDidMount, componentDidUpdate,componentWillUnMount 生命周期
useEffect(()=>{}) => componentDidMount, componentDidUpdate
useEffect(()=>{}, []) => componentDidMount
useEffect(()=>()=>{}) => componentWillUnMount
useEffect 的执行时机:
组件挂在完成后
- 组件数据更新后
- 组件被卸载前
2.4.1. 监听所有 observable 变量,一旦发生变化就会执行
// 组件挂载完成之后执行 组件数据更新完成之后执行
useEffect(()=>{
console.log('123');
})
- useEffect可以写多个,可以把不同的用途分开来写
- 将同一组业务逻辑写在一个生命周期函数中,比如挂载和卸载卸载一起
2.4.2.挂载后执行一次,再也不会执行
useEffect(()=>{
console.log('挂载后执行一次,再也不会执行');
}, [])
2.4.3.组件被卸载之前执行一次
useEffect(()=>{
return ()=>{
console.log('组件被卸载了')
}
})
怎样卸载 App 这个组件?
从 ‘react’ 中引入 ReactDOM 调用方法,传入 root 节点进行卸载
2.4.4. 指定数据发生变化时才会触发 effect
useEffect(()=>{
document.title = count
}, [count])
2.4.5. 在 useEffect 中执行异步函数:IIFE
- useEffect 中第一个参数必须为一个函数,而且不能是异步函数,
- 因为 useEffect 要返回清理资源的函数,异步函数则会默认返回 Promise ```javascript // 不可以,因为useEffect的return是一个卸载的回调函数,而下面这种写法是返回一个promise useEffect(async ()=> { const res = await axios.get() })
// 正确用法 // 在useEffect中写一个自执行函数 useEffect(()=> { (async ()=> { const res = await axios.get() })() })
<a name="B1MIJ"></a>
#### useEffect VS 生命周期函数
- useEffect 可以被多次调用,根据不用的调用方式产生不同的【生命周期】效果,代码更加简洁、灵活
- useEffect 可以将多个【生命周期】逻辑整合,如【初次更新】和【数据更新】后代码逻辑类似,整合之后去除重复代码使代码逻辑更清晰
- 类中的【生命周期函数】往往需要将多个业务逻辑写在同一个生命周期函数中,或需要将一个逻辑拆分在不同的生命周期函数中
<a name="fsCXJ"></a>
#### 2.5 useMemo()
- useMemo 的行为类似 Vue 中的【计算属性】,可以监测某个值的变化,根据变化值计算新值
- useMemo 会缓存计算结果,如果监测值没有发生变化,即使组件重新渲染也不会重新计算;此行为可以有助于避免在某个渲染上进行昂贵的计算
```javascript
const result = useMemo(()=> {
// 如果count变化,重新执行这个函数
return result
}, [count])
优点
- 缓存昂贵计算值
- 它的返回值可以参与视图渲染
-
2.6 memo()
【性能优化】如果组件中的数据没有发生变化,
memo(组件)
可以阻止组件更新;- 类似 class 组件中的
PureComponent
和shouldComponentUpdate
```javascript const Foo = memo(function Foo() { returnxxx})
`memo(Component)` 的返回值是一个新的组件,这个新的组件会判断传入的组件数据有没有发生变化,例<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/1359495/1630064301418-64650caf-2e31-4b07-985b-cb32a3115625.png#clientId=u3dc73b03-5cd2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=551&id=ubb2cf98e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1102&originWidth=1500&originalType=binary&ratio=1&rotation=0&showTitle=false&size=672160&status=done&style=none&taskId=ub4155c6b-453c-457f-baa6-f91c2ff84fd&title=&width=750)
<a name="zmCbK"></a>
####
<a name="eMvgJ"></a>
#### 2.7 useCallback()
性能优化,缓存函数,使组件重新渲染使得到相同函数实例<br />函数传递给子组件时,每次重新渲染都得到了一个新的函数实例,这样子组件就又会重新渲染
```javascript
function App(){
const [count, setCount] = useState(0)
//function resetCount(){
// setCount(0)
// }
const resetCount = useCallback(()=> setCount(0), [setCount])
return (
<div>
<Foo resetCount={resetCount} />
</div>
)
}
const Foo = memo(function Foo(props) {
return <div><button onClick={props.resetCount}></button></div>
})
// 这里foo组件重新渲染了,因为count值发生变化,app组件重新渲染,(重新渲染每次都生成了不同的resetCount实例)重新渲染之后resetCount就不是之前的
// 函数实例了,值发生了改变,所以导致foo组件也跟着更新
优化后的 resetCount 函数↓,只有当 setCount 发生变化才会重新取得新的 ()=>setCount(0) 函数
2.8 useRef()
2.8.1 用于【获取 DOM 元素对象】
import React, { useRef } from 'react';
function App(){
const username = useRef();
const handler = ()=>console.log(username) // {current:input}
return (
<input ref={username} onChange={handler}/>
)
}
在 input
元素上添加 ref
属性后, username
这个对象上的 current
键会指向 input
这个 DOM 元素,达到取值效果
2.8.1 用于【跨组件周期保存数据】
- 即使组件重新渲染,使用 useRef 保存的数据仍然存在
- 保存的数据被更改不会触发组件重新渲染
例:
下面这段代码无法在点击事件之后清除 interval 定时器
import React, { useState,useEffect} from 'react';
function App(){
const [count, setCount ] = useState(0);
let timerId = null; // 重新渲染后timerId为null
useEffect(()=>{
timerId = setInterval(()=>{
setCount(count=>count+1)
},1000) // 每次count更新都会促发组件的重新渲染
},[]);
const stopCount = ()=>{
clearInterval(timerId)
// 这里的timerId 为 null
}
return <div>
{count}
<button onClick={stopCount}> stop </button>
</div>
}
解决方法:
import React, { useState,useEffect,useRef } from 'react';
function App(){
const [count, setCount ] = useState(0);
let timerId = useRef(); // {current:null}
useEffect(()=>{
timerId.current = setInterval(()=>{
setCount(count=>count+1)
},1000)
},[]);
const stopCount = ()=>{
console.log(timerId) // {current: 定时器ID}
clearInterval(timerId.current)
}
return <div>
{count}
<button onClick={stopCount}> stop </button>
</div>
}
自定义Hook
目的
在组件之间实现资源共享,共享的通常是一套逻辑,如发送请求获取博客文章,有很多组件需要做这件事,就可以把这段逻辑封装成一个 Hook。自定义hook是标准的封装和共享逻辑的方式。
固定格式
自定义hook是一个函数,其名称以use开头
自定义hook其实就是逻辑和内置hook的组合
它可以返回任何需要的数据和方法
// 例1
function useGetPost() {
const [post, setPost] = userState({})
useEffect(()=> {
axios.get(xxx).then(res=> setPost(res.data))
}, [])
return [post, setPost]
}
function App() {
const [post, setPost] = useGetPost()
return (
<div>{post.title}</div>
)
}
// 例2
function useUpdateInput (initialValue) {
const [value, setValue] = useState(initialValue)
return {
value,
onChange: event => setValue(event.target.value)
}
}
function App() {
const username = useUpdateInput('')
const password = useUpdateInput('')
const submitForm = event => {
event.preventDefault();
console.log(username.value)
console.log(password.value)
}
return (
<form onSubmit={submitForm}>
<input type='text' {...username} />
<input type='password' {...password} />
<input type='submit'>
</form>
)
}
React路由Hooks
react-router-dom
提供的路由钩子函数
import { useHistory, useLocation, useRouteMatch, useParams } from 'react-router-dom'
export default function Home(props) {
console.log(props)
console.log(useHistory())
console.log(useLocation())
console.log(useRouteMatch())
console.log(useParams())
}
- useHistory:获取 history 对象
- useLocation:获取 location 对象
- useRouteMatch:获取 match 对象
useParams:获取 match 对象下的 params 对象(params 对象存储路由参数)
useState 原理
初始值(任意类型)
- 初始值就是只初始化一次—-脱离useState函数存在,判断是否有值,
- 返回值为数组[变量,设置变量的方法]
- useState可以调用多次—-使用数组这个数据结构保存起来,闭包缓存index值
import React from 'react'
import ReactDOM from 'react-dom'
// 这些必须脱离函数存在,否则组件重新渲染后,函数内的变量会被重新初始化
let state = []
let setters = []
// 声明下标
let stateIndex = 0
function render() {
// 这里需要清零是因为:重新渲染后 stateIndex 不会自动归零,而是继续累加
// 如果不清零,在重新渲染后 useState 中的 value 和 setter 便无法取得渲染前对应的 state 数组和 setter 数组中的值
stateIndex = 0
// 重新渲染视图
ReactDOM.render(<App />, document.getElementById('root'))
}
function createSetter(index) {
// 通过闭包【保留索引】不被释放
// 下面这个函数中的 index 永远都是上面这个函数被调用时传进去的那个 index 值
return function (newState) {
// 所以这里修改的也永远是这个 index 对应的 state
state[index] = newState
// 重新渲染视图,所以每次调用 setState 都会重新渲染组件
render()
}
}
function useState(initialState) {
// 1. 能取就取存下来的值,没有就用初始值
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
// 2. 重新渲染后都要【按顺序】重新创建【所有的】 setter,所以 useState 声明的顺序不能随意变动
setters.push(createSetter(stateIndex))
// 3. 在数组中取值:确保【对应顺序】
let value = state[stateIndex]
let setter = setters[stateIndex]
// 4. 为下一个使用 useState 的情况做准备
stateIndex++
// 5. 以数组形式返回
return [value, setter]
}
function App() {
const [count, setCount] = useState(0)
const [name, setName] = useState('Mary')
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>setCount</button>
{name}
<button onClick={() => setName('James')}>setName</button>
</div>
)
}
export default App
useEffect 原理
import React from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function render() {
stateIndex = 0
// 每次渲染都会归零
effectIndex = 0
ReactDOM.render(<App />, document.getElementById('root'))
}
function createSetter(index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState(initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setter = setters[stateIndex]
stateIndex++
return [value, setter]
}
// 记录上一次的依赖值(useEffect 第二个参数中的变量值)
let prevDepsAry = []
let effectIndex = 0
function useEffect(callback, depsAry) {
if (Object.prototype.toString.call(callback) !== '[object Function]') {
// 验证 callback 是不是【函数】
throw new Error('useEffect函数的第一个参数必须是函数')
}
// 判断 depsAry 有没有被传递
if (typeof depsAry === 'undefined') {
// 没有传递
callback()
} else {
if (Object.prototype.toString.call(depsAry) !== '[object Array]') {
// 验证 depsAry 是不是【数组】
throw new Error('useEffect函数的第二个参数必须是数组')
}
// 获取上一次的依赖值
let prevDeps = prevDepsAry[effectIndex]
// 将当前的依赖值和上一次的依赖值做对比 如果有变化(即:depsAry.every === false) 则调用callback
let hasChanged = prevDeps ? depsAry.every((dep, index) => dep === prevDeps[index]) === false : true
if (hasChanged) {
// 依赖值有变化
callback()
}
// 同步依赖值
prevDepsAry[effectIndex] = depsAry
// 为下一次调用 useEffect 做准备
effectIndex++
}
}
function App() {
const [count, setCount] = useState(0)
const [name, setName] = useState('Mary')
useEffect(() => {
console.log('count')
}, [count])
useEffect(() => {
console.log('name')
}, [name])
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>setCount</button>
{name}
<button onClick={() => setName('James')}>setName</button>
</div>
)
}
export default App
useReducer 原理
import React from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function render() {...
}
function createSetter(index) {...
}
function useState(initialState) {...
}
let prevDepsAry = []
let effectIndex = 0
function useEffect(callback, depsAry) {...
}
// useState 的增强版本
function useReducer(reducer, initialState) {
// 初始值其实是为 useState 传入的
const [state, setState] = useState(initialState)
// 精妙的设计 dispatch
function dispatch(action) {
// 调用传入的 reducer 方法
const newState = reducer(state, action)
// 使用 reducer 返回的值来为 state 重新赋值
setState(newState)
}
// 以数组形式返回
return [state, dispatch]
}
function App() {
// 定义传入 useReducer 的第一个参数,reducer 会被 useReducer 中定义的 dispatch 调用
function reducer(state, action) {
// 为同一个 state 以不同的 action 方式进行变换
switch (action.type) {
case 'increment':
// 返回什么,新的 state 就是什么
return state + 1
case 'decrement':
// 返回什么,新的 state 就是什么
return state - 1
default:
return state
}
}
const [count, dispatch] = useReducer(reducer, 0)
return (
<div>
{count}
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
</div>
)
}
export default App