# Hooks
Hooks 是一种函数,该函数允许您从函数式组件 “勾住(hook into)” React 状态和生命周期功能。
有状态组件,就可以使用函数式组件来定义了。
# 类组件和函数组件
类组件:
import React, { Component } from 'react'
export default class Example extends Component {
state = {
count: 0
}
addCount() {
this.setState({ count: this.state.count+1 })
}
render() {
return (
<div>
<p>You clicked {this.state.count}</p>
<button onClick={this.addCount.bind(this)}>Click me</button>
</div>
)
}
}
函数组件:
import React, { useState } from 'react'
function Example() {
//解构赋值 声明一个新的状态变量,我们将其称为 "count"
const [count, setCount] = useState(0)
return (
<div>
<p>You Click {count} Times</p>
<button onClick={() => {setCount(count + 1);console.log(useState);}}>Click me</button>
</div>
)
}
export default Example
useState
返回一对:current state(状态)值和一个允许你更新它的函数。- 所以用数组解构赋值进行自定义状态。
- React 假定如果多次调用
useState
,则在每次渲染期间以相同的顺序执行。
React 依赖于调用 Hooks 的顺序:(不同的状态的记录是通过useState的执行顺序)
// ------------
// 第一次渲染
// ------------
useState('Mary') // 1. 用'Mary'初始化名称状态变量
useEffect(persistForm) // 2. 添加一个 effect 用于持久化form
useState('Poppins') // 3. 使用 'Poppins' 初始化 surname 状态变量
useEffect(updateTitle) // 4. 添加一个 effect 用于更新 title
// -------------
// 第二次渲染
// -------------
useState('Mary') // 1. 读取 name 状态变量(忽略参数)
useEffect(persistForm) // 2. 替换 effect 以持久化 form
useState('Poppins') // 3. 读取 surname 状态变量(忽略参数)
useEffect(updateTitle) // 4. 替换 effect 用于更新 title
# 使用State Hook
- hooks在class中不起作用
- 如果我们想在状态中存储多个不同的值,我们将调用
useState()
多次 - hooks需要一个一个的设置状态值,不能将前后的状态进行合并操作。
# 初始化state:
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
//使用useState(x)来初始化一个状态
# 读取state:
<p>You clicked {count} times</p>
//函数组件直接读取state值
# 更新state:
<button onClick={() => setCount(count + 1)}>
Click me
</button>
//使用setXxx()来更新
# 使用Effect Hook
可以将 useEffect
Hook 视为 componentDidMount、componentDidUpdate 和 componentWillUnmount 的组合。
通过使用Effect Hook ,您可以告诉 React 在渲染后要做些什么,React 将记住传递的函数(我们将把它称为 “effect” ),然后在执行DOM更新后调用它。(类似与类组件中this.setState的第二个回调参数)
- 每次渲染后
useEffect
都会运行。(异步的不影响视图更新) - 组件内调用
useEffect
,Hooks支持JavaScript闭包。 - 可以使用多个
useEffect
,进行组件的逻辑分解。
import { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
如果需要设置在卸载组件的时候调用的函数需要在useEffect传入的函数中return一个卸载时调用的函数:(即需要清理effects,如果不需要清理effects就不需要返回值)
- 每次重新渲染,都会执行useEffect
- 如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组(
[]
)作为第二个参数传递。
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
# 通过跳过 Effects 来优化性能:
useEffect
可选的第二个参数是一个数组- 如果要运行 effect 并仅将其清理一次(在装载和卸载时),则可以将空数组(
[]
)作为第二个参数传递。
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // Only re-run the effect if count changes
//如果count渲染前和渲染后都是一样的值,react会跳过这个effect
useEffect(() => {
document.title = `You clicked ${count} times`;
}, []); // Only re-run the effect if count changes
//只会执行一次effect,相当于类组件生命周期componentDidMount和componentWillUnmount
如果你传入了一个空数组([]
),effect 内部的 props 和 state 就会一直持有其初始值:
function Demo(){
const [count,setCount]=useState(0)
useEffect(()=>{
setInterval(()=>{
console.log(count)//每次都是初始值0
setCount(count+1)
},500)
},[])//不依赖于任何值,只会执行一次
return (<div>count:{count}</div>)//始终显示1
}
# 总结:
- 每次渲染后
useEffect
都会运行。(异步的不影响视图更新) - 通过
useEffect
返回一个函数,来实现组件卸载unMount的生命周期(每次重新渲染都会执行useEffect,所以每次重新渲染也就都会执行return 的函数) - 通过
useEffect
传入第二个参数为一个数组,用于监听状态的变化,实现拦截优化执行useEffect
[state1,..,stateN]
,状态变化即执行当前的useEffect
,否则不执行[]
,仅在执行一次首次渲染时useEffect
和最后卸载组件时useEffect return
# Hook规则
# 1】只在顶层调用Hook:
- 不要在循环,条件或嵌套函数中调用 Hook 。 相反,总是在 React 函数的顶层使用 Hooks。
# 2】只在 React Functions 调用 Hooks:
- 不要在常规 JavaScript 函数中调用 Hook
- 在 React 函数式组件中调用 Hooks
- 从自定义 Hooks 调用 Hooks
确保组件中的所有 stateful (有状态)逻辑在其源代码中清晰可见。
# 3】组件逻辑代码分割
单个组件中使用多个 State 或 Effect Hook:
function Form() {
// 1. Use the name state variable
const [name, setName] = useState('Mary');
// 2. Use an effect for persisting the form
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
// 3. Use the surname state variable
const [surname, setSurname] = useState('Poppins');
// 4. Use an effect for updating the title
useEffect(function updateTitle() {
document.title = name + ' ' + surname;
});
// ...
}
React 如何知道哪个 state(状态) 对应于哪个 useState
调用呢?
- React 依赖于调用 Hooks 的顺序
// ------------
// 第一次渲染
// ------------
useState('Mary') // 1. 用'Mary'初始化名称状态变量
useEffect(persistForm) // 2. 添加一个 effect 用于持久化form
useState('Poppins') // 3. 使用 'Poppins' 初始化 surname 状态变量
useEffect(updateTitle) // 4. 添加一个 effect 用于更新 title
// -------------
// 第二次渲染
// -------------
useState('Mary') // 1. 读取 name 状态变量(忽略参数)
useEffect(persistForm) // 2. 替换 effect 以持久化 form
useState('Poppins') // 3. 读取 surname 状态变量(忽略参数)
useEffect(updateTitle) // 4. 替换 effect 用于更新 title
// ...
# 4】打破规则实例:
// 🔴 我们在条件语句中使用Hook,打破了第一条规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
//导致hooks调用顺序发生改变
我们需要保证所有的Hook能正常执行。所以解决上面这个问题就需要把条件放在Hook中:
useEffect(function persistForm() {
// 👍 我们不再违反第一条规则了
if (name !== '') {
localStorage.setItem('formData', name);
}
});
# 使用useContext
组件共享状态传值
createContext(defaultValue)
- 可选传入参数:默认value值
- 返回值是一个context对象,里面有Provider、Consumer
useContext(contextObj)
- 参数接收一个 context 对象(React.createContext 的返回值)
- 返回值是provider提供的共享value
import React, { useState, createContext, useContext } from 'react'
const CountContext = createContext()
function Child() {
//直接使用useContext接收context对象,获取其中的value值
let count = useContext(CountContext)
return (
<div>{count}</div>
)
}
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<p>You Click {count} Times</p>
<button onClick={() => { setCount(count + 1); }}>Click me</button>
<CountContext.Provider value={count}><Child /></CountContext.Provider>
</div>
)
}
export default Parent
也可以使用consumer
import React, { useState, createContext, useContext } from 'react'
//创建context对象
const CountContext = createContext()
function Child() {
return (
<div>{/*使用context对象的消费者,回调函数获取值*/}
<CountContext.Consumer>{value => {return value}}</CountContext.Consumer>
</div>
)
}
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<p>You Click {count} Times</p>
<button onClick={() => { setCount(count + 1); }}>Click me</button>
<CountContext.Provider value={count}><Child /></CountContext.Provider>{/*使用context对象的提供者,传入value属性*/}
</div>
)
}
export default Parent
# 使用useReducer
使用JavaScript原生实现reducer(纯函数)
function countReducer(state,action){
switch(action.type){
case 'add':
return state+1
case 'sub':
return state-1
default:
return state
}
}
useReducer(callback,initialValue)
- 第一个参数是一个回调函数,回调函数中传入形参state和action
- 第二个参数是state的初始值
- 返回值是一个数组,数组第一位是计算后的状态state,第二位是dispatch分发函数:
[state,dispatch]
import React, { useReducer } from 'react'
function ReducerDemo() {
//数组解构,第一个是改变后的状态,第二个是dispatch方法
const [count, dispatch] = useReducer((state, action) => {
switch (action) {
case 'add':
return state + 1;
case 'sub':
return state - 1;
default:
return state;
}
}, 0)
return (<div>
<h2>现在的分数是{count}</h2>
<button onClick={() => dispatch('add')}>Increment</button>
<button onClick={() => dispatch('sub')}>Decrement</button>
</div>)
}
export default ReducerDemo
# 结合使用useReducer和useContext
useReducer和useContext可实现小型的Redux
import React, { createContext, useContext, useReducer } from 'react'
let ColorContext = createContext()//创建context对象
function ShowArea() {
let color = useContext(ColorContext)//使用context对象中的value
return <div style={{ color: color }}>字体颜色是{color}</div>
}
function Buttons() {
//使用useReducer进行计算state
let [state, dispatch] = useReducer((state, action) => action, 'blue')
return (
<ColorContext.Provider value={state}>
{/*使用context对象的Provide动态传入value*/}
<button onClick={() => dispatch('red')}>红色</button>
<button onClick={() => dispatch('yellow')}>黄色</button>
<ShowArea />
</ColorContext.Provider>)
}
export default Buttons
# 使用useMemo解决子组件重复执行的问题
shouldComponentUpdate组件更新之前进行状态对比,判断子组件是否需要更新。
useMemo类似于shouldComponentUpdate阻止子组件渲染。
useMemo(callback,[state])
:用于缓存状态或属性
- 第一个参数是需要每次渲染时执行的函数
- 可选的第二个参数是一个数组,数组中传入需要监听变化的状态state
- 如果该状态发生变化,则执行函数,否则不执行。
- 如果是空数组,不依赖于任何状态,则第一次渲染时才进行缓存,之后不再触发缓存。
import React, { useState, useMemo } from 'react'
function Parent() {
const [state1, setState1] = useState('x')
const [state2, setState2] = useState('y')
return (<>
<button onClick={() => { setState1(new Date().getTime()) }}>按钮1</button>
<button onClick={() => { setState2(new Date().getTime()) }}>按钮2</button>
<Child name={state1}>{state2}</Child>
</>)
}
function Child({ name, children }) {
function ChangeState1(name) {
console.log('xxxxxx');
return name
}
return (<>
<div>{ChangeState1(name)}</div>
//此时我们不管是点击按钮1还是按钮2,都会重复执行打印'xxxxxx',因为子组件都会都会重新进行渲染,执行函数
<div>{children}</div>
</>)
}
export default Parent
使用useMemo来根据状态变化优化子组件重复渲染,提升性能(类似shouldComponentUpdate)
import React, { useState, useMemo } from 'react'
function Parent() {
const [state1, setState1] = useState('x')
const [state2, setState2] = useState('y')
return (<>
<button onClick={() => { setState1(new Date().getTime()) }}>按钮1</button>
<button onClick={() => { setState2(new Date().getTime()) }}>按钮2</button>
<Child name={state1}>{state2}</Child>
</>)
}
function Child({ name, children }) {
function ChangeState1(name) {
console.log('xxxxxx');
return name//useMemo缓存的是name这个属性值
}
return (<>
<div>{useMemo(() => ChangeState1(name), [name])}</div>
//使用useMemo包装子组件渲染时同时会执行的函数,第二个参数是需要监听变化的状态变量。
<div>{children}</div>
</>)
}
export default Parent
此时我们点击'按钮1',依然会打印‘xxxxxx’,因为传入子组件的name发生了改变。此时我们再点击'按钮2'就不会执行打印‘xxxxxx’,虽然传入子组件的状态children发生改变,但name没有发生改变;
# 使用useRef获取DOM和保存变量
useRef(initialValue)
- 返回一个可变的ref对象,其
.current
属性被初始化为传入的参数(initialValue
)。 - 返回的ref对象放在需要获取dom元素的标签上,即可使用
.current
.current
可以进行保存变量
# 获取DOM值:
import React, { useRef,createRef } from 'react'
function Example6() {
let inputRef = useRef(null)
//let inputRef = createRef(null)
//使用createRef效果一模一样
function getInput() {
console.log(inputRef.current.value)//绑定ref的输入框的value值
}
return (
<>
<input ref={inputRef} type="text" name="" id="" />
<button onClick={getInput}>按钮</button>
</>
)
}
export default Example6
# 保存变量:.current保存变量
import React, { useState, useRef, createRef, useEffect } from 'react'
function Example6() {
let inputRef = useRef()
// let inputRef = createRef()
const [text, setText] = useState('xxx')
useEffect(() => {
inputRef.current = text//使用.current来保存变量
console.log('inputRef:' + inputRef.current)
})
return (
<>
<input ref={inputRef} type="text" value={text} onChange={(e) => setText(e.target.value)} />
</>
)
}
export default Example6
# createRef 与 useRef 的区别:
createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用。
例子:
import React, { useState, useRef, createRef } from 'react'
function Diff() {
const [renderIndex, setRenderIndex] = useState(0)
const UseRef = useRef()//useRef默认传入null
if (!UseRef.current) {//所以会执行一次,每次重新渲染,使用之前的变量,所以.current就不为空,保存了之前的变量。
UseRef.current = renderIndex
console.log(UseRef.current)//1
}
const CreateRef = createRef()//createRef不接受参数
if (!CreateRef.current) {//并不会保存之前变量,而是每次重新渲染再次执行createRef创建空的.current
CreateRef.current = renderIndex
}
return (<>
<p>renderIndex:{renderIndex}</p>
<p>UseRef.current:{UseRef.current}</p>
<p>CreateRef.current:{CreateRef.current}</p>
<button onClick={() => setRenderIndex(renderIndex + 1)}>渲染</button>
</>)
}
export default Diff
# 使用useCallback缓存方法
useCallback(fn,[state])
:用于缓存方法
- 传入需要缓存的方法或函数(区别于useMemo)。
- 返回一个 memoized 回调函数。
useCallback(fn, deps) 相当于 useMemo(() => value, deps)
useMemo
和 useCallback
接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据
- 如果第二个参数是空数组,不依赖于任何状态,则只有第一次渲染组件时进行缓存,之后不再缓存。
共同作用:
- 仅仅
依赖数据
发生变化, 才会重新计算结果,也就是起到缓存的作用。
两者区别:
useMemo
计算结果是return
返回的值, 主要用于 缓存计算结果的值 ,应用场景如: 需要 计算的状态useCallback
计算结果是函数
, 主要用于 缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
# 自定义Hooks函数
自定义Hooks函数是由各种hook API进行融合的函数
例如:下面这个实时获取页面大小的Hooks函数
import React, { useState, useEffect, useCallback } from 'react'
function useWinSize() {
//定义一个状态
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
//使用useCallback进行缓存方法,(当数组中的依赖,变化时才再次触发更新)
const onResize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])//空数组,不依赖于任何值,只有第一次渲染时进行缓存一次
useEffect(() => {//组件每次挂载时,进行执行
window.addEventListener('resize', onResize)
return () => {//当组件卸载时,进行清理
window.removeEventListener('resize', onResize)
}
}, [])//空数组,不依赖于任何值,只有第一次渲染时,执行一次生命周期
return size//返回修改后的状态
}
function Example() {
const size = useWinSize()//使用自定义Hooks函数
return (
<div>页面的size是{size.width}X{size.height}</div>
)
}
export default Example