React Hooks
1 初识hooks
react hooks复制了class构建的组件所能实现的功能,常见的有生命周期函数、状态等,并且解决了类组件中的this指向问题,可以说使用Function实现Class
看一下二者对比:
// class组件
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
// function hooks
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
可以看到,Example变成了一个函数,但这个函数却有自己的状态(count),同时它还可以更新自己的状态(setCount)。这是因为它注入了一个hook–useState,就是这个hook让我们的函数变成了一个有状态的函数。
除了useState这个hook外,还有很多别的hook,比如useEffect提供了类似于componentDidMount等生命周期钩子的功能,useContext提供了上下文(context)的功能等等。
Hooks本质上就是一类特殊的函数,它们可以为你的函数型组件(function component)注入一些特殊的功能。
hooks的引入直白的讲:
- 现有的无状态组件和类组件对于
跨组件复用包含状态的逻辑
非常困难,状态往往和ui和逻辑绑定的太深。 逻辑复杂的组件难以开发和维护
,组件越来越大涉及到的逻辑越来越多状态越来越复杂,各种逻辑散落在各处,不相关逻辑被堆放在一起有些无状态组件由于业务需求需要改变成有状态组件,使用类组件重构工作量过于巨大
2 state hooks
使用方式:
const [count, setCount] = useState(0);
含义:初值0赋给count,赋值方式赋给setCount函数
其实拆开来很好理解:
// useState()是一个函数,返回一个长度为2的数组,
// 数组第一个元素的引用为需要管理状态的变量,
// 数组第二个元素为更新状态的函数
const _countState = useState(0);
count = _countState[0]
setCount = _countState[1]
猜想:函数如何保证状态,每次调用函数都调用了hooks函数怎么确保读取到的值为对应的变量呢?
答案:通过顺序决定,第一次赋值决定顺序,再次渲染则按照之前的顺序进行赋值,状态值由react管理,注入到函数中,顺序改变会引起错误
useState
如果传入值为函数,则变为延迟初始化:即如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
const [count, setCount] = useState(0);
const [count, setCount] = useState(()=>0);
// 这两种初始化方式 是相等的,但是在函数为初始值时会被执行一次
const [count, setCount] = useState(()=>{
console.log('这里只会在初始化的时候执行')
// class 中的 constructor 的操作都可以移植到这里
return 0
});
// 当第一次执行完毕后 就和另一句的代码是相同的效果了
在类组件中,setState
是具有回调功能的,而在hooks中,则需要使用effect
3 effect hooks
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 类似于componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 更新文档的标题
document.title = `You clicked ${count} times`;
});
// 也可以传递第二个参数
useEffect(() => {
// 更新文档的标题
document.title = `You clicked ${num} times`;
},[num]);
useEffect
接收一个参数或者两个参数:
一个参数:函数,该函数每次渲染页面前都会执行,包括初次渲染,影响渲染与否的为是否调用了改变状态的hook或者props是否变化
两个参数:第一个是函数,执行同上面一样,第二个参数是表示只有处于数组中的变量改变时才执行前面的函数
注意:当第二个参数传递[]
时,反应只在componentDidMount
和componentWillUnmount
时执行,其中当在 useEffect 的回调函数中返回一个函数时,这个函数会在组件卸载前被调用(我们可以在这里面清除定时器或事件监听器),否则只在加载时调用。useEffect
执行是异步的,不会阻碍浏览器更新视图,如需同步需要使用类组件。所有值类型改变都能触发effect
而引用类型只有引用改变时才会触发,内部属性改变,引用不变不会触发。
特别注意:useEffect第一个参数传入的函数中,在依赖状态发生变化(即第二个参数中数组的值变化)时,会将变化的值通知第一个函数,如果第二个参数不变化,则第一个函数是无法接收到最新状态!!!猜测:某一时刻react的状态值是固定的,放入函数进行处理,形成结果,如果状态变化,react不通知该函数获取最新状态则拿的都是原来的状态。
let x = 0;
export default function App() {
const [count, setCount] = useState(0);
// 此种情况count变化不会改变输出
useEffect(() => {
setInterval(() => console.log(count), 1000);
}, []);
// x变化会改变输出
useEffect(() => {
setInterval(() => console.log(x), 1000);
}, []);
// 此时setCount接受一个函数,则会去取最新的count值进行计算,如果写成setCount(count+1)则每次获取的count的值都是定时器初始化时获取到的初值,每次定时器执行完后count都是1
useEffect(() => {
setInterval(() => {
console.log("run");
setCount((count) => count + 1);
}, 1000);
}, []);
return (
<div className="App">
<h1>{count}</h1>
<button onClick={() => setCount(count + 1)}>addcount</button>
<h1>{x}</h1>
<button onClick={() => (x += 1)}>addx</button>
</div>
);
}
useEffect 参数一的return
作用:在下次渲染前执行,即每次更新渲染都会先执行return的清理函数,然后再执行effect函数
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 一定注意下这个顺序:告诉react在下次重新渲染组件之后,同时是下次调用ChatAPI.subscribeToFriendStatus之前执行cleanup
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
分析以下执行流程:
- 先是执行参数一函数订阅功能
- 如果参数一函数中的变量发生变化,则参数一函数返回,执行取消订阅流程
- 触发下一次渲染,并执行参数一的订阅功能
注意:同类组件一样,对于引用数据类型对象,如果地址没有改变,则默认为该数据未发生改变!!所以,如果是数组类型推荐使用concat
而不是push
!!
3.1 包装effect自己定义特定功能
参考示例:
抽取出useFriendStatus函数为自定义hook
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
将自定义hook插入函数组件中:
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
3.2 使用hooks模拟生命周期
自定义hook
import { useEffect, useRef } from "react";
export function useUpdate(callback) {
const mounting = useRef(true)
useEffect(() => {
if (mounting.current) mounting.current = false
else callback()
})
}
使用:
import { useState } from "react";
import { useUpdate } from "./useUpdate";
export default function (props) {
useUpdate(() => { console.log("abc") }) // 使用自定义hook,传入一个函数,该函数在每次update时会调用,第一次mount不会调用
let [num, setNum] = useState(0)
return <div>{ num }<button onClick={setNum.bind(this,num+1)}>click!</button></div>
}
3.3 自定义hook例子(包含返回值)
function useDocumentTitle(title){
useEffect(
()=>{document.title = title; return ()=>{document.title = "默认"}},
[title]
)
}
只有在title变换时才会触发更新,且当组件卸载时会调用返回的回调函数
3.4 useEffect和useState的触发问题
useEffect和useState的更新不一定在同一个更新周期中,即如果effect依赖state中的值,当值set改变后,触发state变化,而effect的销毁和触发有可能在下一个周期,这对于effect中有定时器的,可能会导致定时器无法及时取消的问题。
如果想要在state改变的这个更新周期中触发effect,可以使用:useLayoutEffect
,该方法的回调函数会在DOM更新完浏览器渲染前执行完毕!阻塞!
4 useImperativeHandle
在调用函数组件时,如果需要调用函数组件内的方法或者获取函数组件内的属性,则应当使用该hook,手动暴露函数组件内部的一些属性
使用时需要配合 forwardRef 一起使用:
const FancyInput = forwardRef((props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
getData
}));
const getData = ()=>{console.log("调用方法")}
return <input ref={inputRef} ... />;
})
// 使用
...
// constructor中
this.FancyInputRef = React.createRef()
...
// 方法中,获取到的是暴露出来的两个属性:focus和getData
this.FancyInputRef.current
// render中
<FancyInput ref={this.FancyInputRef}/>
5 forwardRef
forwardRef常用于ref转发,将ref转发到DOM组件上
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
6 useRef
在hook中使用ref,用于函数组件中创建ref调用子组件
const inputRef = useRef(null)
<input ref={inputRef } type="text" />
7 useMemo
用来缓存变量,保证在函数重新渲染时变量不变,可以提高性能减少重绘次数,接受两个参数:
第一:函数,返回变量值
第二:依赖,依赖变化,返回值被重新计算
8 useCallback
用来缓存回调函数,因为函数组件每次刷新都会创建一遍其中定义的回调函数,所以可以使用该hook来保证回调函数只创建一遍(一般场合提升性能不大),适用于多次刷新的场合,可以提高性能,接受两个参数:
第一:函数,该函数为返回的回调函数
第二:依赖,依赖变化,回调函数重新生成
import {useMemo,useCallback} from 'react'
export default function App() {
const x = useMemo(()=>{
return new Date().toString()
},[])
const y = useCallback(()=>{
console.log(123)
},[])
return (
<div className="App">
<h1>{x}</h1>
<button onClick={y}>y</button>
</div>
);
}
9 useTransition
用于降低回调函数更新的优先级,传入其中的回调函数会在一个低优先级的状态下去执行,把CPU留给高优先级的代码
useTransition(()=>{
setData(111)
...
setValue(222)
})
10 useDeferredValue
用于降低包裹数据的优先级,被包裹的数据在渲染时会在一个低优先级的状态下去渲染,把CPU留给高优先级的代码
const data = useDeferredValue(bigArray)
...
{
data.map(item=><div key={item.id}>{item.name}</div>)
}
注意:useTransition和useDeferredValue不要同时使用,因为其效果类似,且有一定性能损耗,当无法获取会更新界面的操作时,就选择包裹值,否则选择包裹回调函数,因为可以同时处理多个低优先级操作!
11 useId
用于生成一个包含冒号的token,可以在组件多次引用时生成唯一的一个标识,常用于htmlFor
function NameFields() {
const id = useId();
return (
<div>
<label htmlFor={id + '-firstName'}>First Name</label>
<div>
<input id={id + '-firstName'} type="text" />
</div>
<label htmlFor={id + '-lastName'}>Last Name</label>
<div>
<input id={id + '-lastName'} type="text" />
</div>
</div>
);
}
注意:这种id不支持css选择器,如querySelectorAll
12 useLayoutEffect
同useEffect,不过回调函数触发时机为同步,在DOM生成完毕渲染到页面之前调用完成。
13 useReducer
使用reducer方式来管理react状态!
const [state, dispatch] = useReducer(reducer, 初始状态, 返回初始状态的初始函数);
在某些场景下,useReducer 会比 useState 更适用,例如
- state 逻辑较复杂且包含多个子值
- 下一个 state 依赖于之前的 state
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 dispatch。
useReducer接收的第三个参数是一个初始化函数
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)
并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。dispatch 不会变化,所以不会触发组件重新渲染。
// 使用context来传递dispatch,可以避免一层层传递回调函数
const TodosDispatch = React.createContext(null);
function TodosApp() {
// 提示:`dispatch` 不会在重新渲染之间变化
const [todos, dispatch] = useReducer(todosReducer);
return (
<TodosDispatch.Provider value={dispatch}>
<DeepTree todos={todos} />
</TodosDispatch.Provider>
);
}
// 使用
function DeepChild(props) {
// 如果我们想要执行一个 action,我们可以从 context 中获取 dispatch。
const dispatch = useContext(TodosDispatch);
function handleClick() {
dispatch({ type: 'add', text: 'hello' });
}
return (
<button onClick={handleClick}>Add todo</button>
);
}
14 useContext
参考上一个例子
其他hooks
useContext
useMutationEffect
参官方考文档吧!或者参考这篇文章
参考文档:
https://www.jianshu.com/p/76901410645a
https://www.cnblogs.com/Grewer/p/10665460.html
高级
有这么一串代码:
const [a, sa] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(a);
sa((a) => a + 1);
}, 1000);
}, []);
效果是每1秒页面数字增加1,但是输出一直是0,因为存在同步问题(如果写成sa(a+1)也会一直是0)
解决该问题,将依赖[]改为[a]
,此时每次定时器被重新创建,问题很严重!!
解决该问题,将函数返回值设置成清除函数,在每次触发effect,会先清除原来的作用,再附加作用:
const [a, sa] = useState(0);
useEffect(() => {
const t = setInterval(() => {
console.log(a);
sa((a) => a + 1); // sa(a + 1)也可以
}, 1000);
return () => clearInterval(t);
}, [a]);
由于此处Interval功能相当于Timeout
所以也可以改成setTimeout
,但是返回值不能省略(如果在1秒内组件卸载,会导致出错)
分析:如果依赖是[]
,则内部回调函数只创建一次,所以回调函数创建的闭包不变,即内部的变量是初始变量不会变!!改了依赖后,依赖变化回调函数会重新创建,闭包会获取到最新的值!