一、概念
Hooks
是 React v16.8 的新特性,可以在不使用类组件的情况下,使用 state
以及其他的 React
特性;
Hooks
是完全可选的,无需重写任何已有代码就可以在一些组件中尝试 Hook
于React v16.8发布,100%向后兼容,Hooks
不包含任何破坏性改动.
React
也没有计划移除 class
类组件,而且 Hooks
不会影响对 React
的理解,它为已知的 React
概念提供了更直接的 API
二、Hooks解决的问题
- 函数式组件不能使用
state
:函数式组件比类组件更简洁好用,而Hooks
让它更加丰富强大; - 副作用问题:诸如数据获取、订阅、定时执行任务、手动修改
ReactDOM
这些行为都可以称为副作用;而Hooks
的出现可以使用useEffect
来处理这些副作用; - 有状态的逻辑重用组件
- 复杂的状态管理:之前通常使用
redux、dva、mobx
这些第三方状态管理器来管理复杂的状态,而Hooks
可以使用useReducer、useContext
配合实现复杂的状态管理; - 开发效率和质量问题:函数式组件比类组件简洁,效率高,性能也好。
useState:组件状态管理的钩子
import { useState } from 'react'
const [state, setState] = useState(initState)
//state:管理组件的状态;
//setState:更新state的方法,方法名不可更改!
//initState:初始的state,可以是任意的数据类型(只在初次渲染时有效,二次渲染时会被忽略),也可以是回调函数,但函数必须有返回值。
//函数式组件实现计数器
import { useState } from 'react'
export default function App() {
const [count, setCount] = useState(0) // 初始值0
return (
<div>
<div>点击了{ count }次</div>
<button onClick={()=>setCount(count+1)}>点击</button>
</div>
)
}
useState
让函数式组件具备了管理状态的能力,不再是一个无状态组件;与 class
的 setState
类似,当向 useState
更新状态的方法 setCount
传递一个函数时,此函数会接收到上一次的状态:setCount(prevCount => prevCount + 1)
但与 class
的 setState
不同的是,如果状态值是一个对象,useState
更新状态的方法是不会合并更新对象的,可以结合展开运算符来达到合并更新对象的效果:
setState(prevState => {
return { ...prevState, ...updateValues };
});
useState
当然可以多次调用,从而定义多个状态值;但不管调用多少次,相互之间都是独立的,不会相互污染。这就是为什么 React
否掉了 Mixins
,因为 Mixins
机制让多个 Mixins
共享一个对象的数据空间,这样就很难保证不同 Mixins
依赖的状态不发生冲突;
function App() {
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
React
又是如何保证多个useState
的相互独立呢,定义时并没有告诉React
这些值的key
,React
如何保证这三个useState
能准确找到它对应的state
呢 —> 根据useState
出现的顺序!
第一次渲染
useState(42); //将age初始化为42
useState('banana'); //将fruit初始化为banana
useState([{ text: 'Learn Hooks' }]); //...
第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
useState('banana'); //读取状态变量fruit的值(这时候传的参数banana直接被忽略)
useState([{ text: 'Learn Hooks' }]); //...
加入条件控制语句
let showFruit = true;
function App() {
const [age, setAge] = useState(42);
if(showFruit) {
const [fruit, setFruit] = useState('banana');
showFruit = false;
}
const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
}
这样一来,首次渲染的初始化过程是不变的,但第二次渲染就有所不同了
第二次渲染
useState(42); //读取状态变量age的值(这时候传的参数42直接被忽略)
// useState('banana');
useState([{ text: 'Learn Hooks' }]); //读取到的却是状态变量fruit的值,导致报错
鉴于此,React
规定:
Hooks
必须写在函数的最外层,不能在循环语句、条件判断、子函数中调用;
只能在 React
的函数式组件、自定义Hooks
中调用Hooks
,不能在其他JavaScript
函数中调用。
useEffect
useEffect
(callback, array):副作用处理的钩子;它也是componentDidMount()、componentDidUpdate()、componentWillUnmount()
、这几个生命周期方法的统一,一个顶三个!React
会等待浏览器完成画面渲染之后才会延迟调用 useEffect
,而生命周期钩子是同步执行的
callback
回调函数,作用是处理副作用的逻辑,可以返回一个函数,用作清理副作用;
import { useEffect } from 'react'
useEffect(() => {
......//副作用处理
return () => {
......//清理副作用的清除函数
}
}, [])
为防止内存泄漏,清除函数会在组件卸载前执行;如果组件多次渲染,则在执行下一个 effect
之前,上一个 effect
就已被清除;
array
可选数组,用于控制useEffect
的执行; 省略时,每次渲染都会执行; 空数组时,只会在组件挂载/卸载时执行一次,类似componentDidMount
、componentWillUnmount
; 非空数组时,会在数组元素发生改变后执行,且这个变化的比较是浅比较。
提供第二个参数时,相当于告诉React
该组件不依赖于state/props
的变化,只依赖第二个参数的变化。
function App() {
const [count, setCount] = useState(0) // 初始值0
useEffect(()=>{
console.log('1') // 初次渲染时执行一次,之后每次 count 变化都执行
return () => {
console.log('2') // 初次渲染时不执行,之后每次 count 变化都最先执行
}
}, [count])
return (<div>
<div>点击了{ count }次</div>
<button onClick={() => setCount(preCount => preCount+1)}>点击</button>
</div>)
}
// 初次渲染:1
// 点击更新count:2 1
父组件也可以通过 props
控制子组件是否执行 useEffect
useEffect(()=>{
// 注册事件
const subscription = props.source.subscribe();
return ()=>{
// 解绑事件
subscription.unsubscribe();
}
}, [props.source]);
如前文所述,Hooks
可以反复多次使用,相互独立。所以合理的做法是,给每一个副作用加一个单独的useEffect
钩子。这样一来,这些副作用不再全堆在class
组件的生命周期钩子里,代码变得更加清晰;
useEffect
中定义的副作用函数在执行时不会阻碍浏览器更新视图,即这些函数是异步执行的,而class
组件中的生命周期钩子都是同步执行的;异步设计对大多数副作用是合理的,但也有特例,比如有时候需要先根据DOM
计算出某个元素的尺寸,然后再去渲染,此时则希望二次渲染是同步发生的,也就是在浏览器真的去绘制界面前发生;
为此,React
为此提供了一个额外的 useLayoutEffect Hook
来处理这类 effect
。它和 useEffect
的结构相同,区别只是调用时机不同 – useLayoutEffect
是同步的。
useContext
useContext()
:同一个父组件的后台组件之间的全局数据共享;
useContext()
接收React.createContext()
的返回值作为参数,即context
对象,并返回最近的context
。当最近的context
更新时,使用该context
的 Hooks
将会重新渲染;
创建一个Context
文件 InfoContext.js
// 设置了默认值 defaultValue
const InfoContext = React.createContext({ name: 'Jerry', age: 18 })
export default InfoContext
父组件
import InfoContext from './context/InfoContext'
const Person = () => {
const ctx = useContext(InfoContext)
return(<InfoContext.Provider value={{ username: 'superman' }}>
<div>
<AgeCompt></AgeCompt>
</div>
</InfoContext.Provider>)
}
export default Person
某一层的后代组件
import { useContext } from 'react'
import InfoContext from './context/InfoContext'
const AgeCompt = () => {
const { username } = useContext(InfoContext)
return <p>{username}</p>
}
export default AgeCompt
useReducer
useReducer()
:useState
的一个增强体,用于处理复杂的状态管理,灵感来源于Redux
的reducer
useState
内部就是基于 useReducer
实现的,只是对于简单的状态管理,useState()
比较好用;
const [state, setState] = useState(initState)
const [state, dispatch] = useReducer(reducer, initState, initAction)
reducer
– 一个函数,根据action
状态处理并更新state
initState
--初始化state
initAction -- useReducer()
初次执行时被处理的action
,会把第二个参数initState
当作参数执行
state
– 状态值
dispatch
– 更新state
的方法,接收action
作为参数,当它被调用时,reducer
函数也会被调用,同时根据action
去更新state
,action
是一个描述操作的对象,如
dispatch({type: 'add'})
import { useReducer } from 'react';
const initState = { count: 0 };
const initAction = initState => { count: initState.count + 2 };
const reducer = (state, action) => {
switch(action.type) {
case 'ADD':
return { count: state.count+1 }
case 'DEL':
return { count: state.count-1 }
case 'RESET':
return initState
default:
return state
}
}
export default function UserCompt() {
const [state, dispatch] = useReducer(reducer, initState)
return (<div>
<p>{state.count}</p>
<div>
<button onClick={() => dispatch({ type: 'ADD', /**可以加一些其他属性*/ })}>增加</button>
<button onClick={() => dispatch({ type: 'DEL' })}>减少</button>
<button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
</div>
</div>)
}
更多Hooks
useMemo(callback, array):性能优化,利用了闭包的特性,通过记忆值来避免在每个渲染上都执行高开销的计算(计算缓存);适用于复杂的计算场景,如复杂的列表渲染,对象深拷贝…
callback - 用于处理逻辑的函数
array - 依赖项,控制useMemo()重新执行的数组,array元素改变时才会重新执行useMemo()
返回值是一个记忆值,也是callback的返回值
import React, { useMemo } from 'react';
export default function UserCompt() {
const obj1 = { name: 'Tom', age: 15 }
const obj2 = { name: 'Jerry', age: 18, sex: '男' }
//合并obj1、obj2
const memoValue = useMemo(() => Object.assign(obj1, obj2), [obj1, obj2])
return (<div>
<p>{ memoValue.name }</p>
<p>{ memoValue.age }</p>
</div>)
}
注意:不能在 useMemo() 中处理副作用逻辑,而是把副作用处理逻辑放在useEffect()
useCallback(callback, array):也是用于性能优化,与useMemo()不同的是,返回值是callback本身;
//合并obj1、obj2
const backValue = useCallback(() => Object.assign(obj1, obj2), [obj1, obj2])
<div>{ backValue().name } --- { backValue().age }</div>
当依赖项变化时,返回一个新的函数体callback。
useRef():创建ref,方便访问操作DOM
const RefCompt = () => {
//创建ref
const inputRef = useRef();
const getValue = () => {
//访问ref
const inpt = inputRef.current; // input的DOM对象
inpt.focus(); // 让 input 框获取焦点
console.log(inpt.value); // input输入框的值
};
//挂载
return (<div>
<input ref={ inputRef } type="text" />
<button onClick={ getValue }>获取值</button>
</div>);
}
自定义Hooks
Hooks本质上就是封装好的勾子函数,在自定义Hooks时,最需要关心的就是性能、重复渲染这些问题;
自定义一个Effect Hooks,把可以复用的逻辑抽离出来,变成一个个可插拔的插销;
当标题变化时,则修改标题、否则不执行的Hooks
import { useEffect } from 'react'
//封装Hooks,以 use 开头
const useChangeTitle = (title) => {
useEffect(() => {
document.title = title
}, [title])
}
export default (props) => {
useChangeTitle("自定义修改标题Hooks")
return <div>测试</div>
}
一个用来判断某个 id 是否在线的Hooks
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(()=>{
//注册监听事件
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
//取消监听事件
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
在组件中使用此Hooks
function FriendStatus(props) {
//调用自定义Hooks
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
Hooks的使用规则
只在顶层调用Hooks
Hooks的调用尽量只在顶层作用域
不要在循环、条件或嵌套函数中调用Hook,否则可能会无法确保每次组件渲染时都以相同的顺序调用Hook
只在函数组件调用Hooks:目前只支持函数式组件,未来版本Hooks会扩展到class类组件;
React Hooks的应用场景:函数式组件、自定义Hooks