什么是hooks ?
Hooks是React16.8中新增的功能,它们让你无需编写类即可使用状态和其他 React 功能
Hooks是函数,它有多个种类,每个hook都是为function Component提供使用React状态和声明周期特性的通道。Hooks不能在class Component中使用,React中提供了一些预定义好的Hooks供我们使用,下面我们来了解一下:
useState
咱们来看一段代码
import React, { useState } from 'react';
function Example() {
// 定义一个 State 变量,变量值可以通过 setCount 来改变
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
可以看到useState的入参只有一个,就是state的初始值。这个初始值可以是一个数字、字符串、数组或对象,甚至可以是一个函数,当入参是一个函数的时候,这个函数只会在这个组件初始化渲染的时候执行:
const [state,setState] = useState(()=>{
const initalState = someExpensiveComputation(props)
return initalState
})
当需要根据之前的状态值来计算出当前状态值的时候,就需要传入函数,这跟class Component 中的setState有点像,另外跟class Component的setState很像的一点是,当新传入的值跟之前的值一样时(使用object.is比较),不会触发更新。
useEffect
解释useEffect之前先理解一下什么是副作用。网络请求,订阅某个模块或DOM操作都是副作用的例子,useEffect专门用来处理副作用的。正常情况下,在function Component的函数中,是不建议写副作用代码的,容易出bug。
下面的class Component例子中,副作用代码写在了componentDidMount和componentDidUpdate中:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
可以看到componentDidMount和componentDidUpdate中的代码一样,而使用函数组件中的useEffect来写就不会出现这样的问题:
import React, {useState, useEffect} from "react";
function FriendStatus(props) {
const [count,setCount] = useState(0);
useEffect(()=>{
document.title = `You clicked $(count) times`
});
return (
<div>
<p>You clicked {count} times</p>
<button @click={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
useEffect会在每次 DOM 渲染后执行,不会阻塞页面渲染,它同时具备componentDidMount、componentDidUpdate和omponentWillUnmount三个声明周期函数的执行时机。
此外还有一些副作用需要组件卸载的时候做一些额外的清理工作,如订阅某个功能:
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
在componentDidMount订阅后,需要在componentWillUnmount取消订阅,接下来我们使用函数组件中的useEffect:
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 返回一个函数来进行额外的清理工作:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
当useEffect返回一个函数时,React会在下一次执行这个副作用之前执行一次清理工作,整个组件的声明周期流程可以如下理解:
组件挂载 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 执行副作用 --> 组件更新 --> 执行清理函数 --> 组件卸载
上文提到useEffect会在每次渲染后执行,但有的时候我们只希望在state和props改变才执行
如下class Component中的使用:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
使用useEffect时,我们只需要传入第二个参数:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改变的时候才执行 Effect
第二个参数是个数组时,可以传入多个值,一般会将用到的所有props和state都传进去
当副作用只需要在组件挂载的时候和卸载的时候执行,第二个参数都可以传入一个空数组[],实现效果优点类似于componentDidMount和componentWillUNmount的组合
useLayoutEffect
useLayoutEffect的用法和useEffect的用法完全一样,都可以执行副作用和清理操作,它们之间唯一的区别就是执行的时机不同
useEffect不会阻塞浏览器的绘制任务,它在页面更新后才会执行。
而useLayoutEffect跟componentDidMount和componentDidUpdate的执行时机一样,会阻塞页面渲染,如果在里面执行耗时任务的话,会出现页面卡顿。
在大多数情况下,useEffect是非常好的选择。唯一例外的就是需要根据新的 UI 来进行 DOM 操作的场景,useLayoutEffect会保证在页面渲染前执行,也就是页面渲染出来的是最终的效果。如果使用useEffect,页面很有肯能因为渲染了2次而出现的页面抖动。
useContext
useContext可以帮助我们跨组件层级直接传递变量,实现共享
以下代码就相当于把count变量允许跨层级实现传递和使用了(也就是实现了上下文),当父组件的count发生改变时子组件也会发生改变:
import React, { useState , createContext } from 'react';
const CountContext = createContext()
function Example(){
const [ count , setCount ] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={()=>{setCount(count+1)}}>click me</button>
<CountContext.Provider value={count}>
</CountContext.Provider>
</div>
)
}
export default Example;
接下来我们看看一个hook组件是如何接收到这个变量的
import React, {useState , createContext , useContext} from 'react';
function Counter(){
const count = useContext(createContext) //一行代码就可以拿到count
return ( <h1>{count}</h1> )
}
得到后就可以显示出来了,但要记得在<CountContext.Provider>的闭合标签中,如下:
<CountContext.Provider value={count}>
<Counter />
</CountContext.Provider>
useMemo
下面我们先看一个没有使用useMemo的例子:
import React from 'react'
export default function withoutMemo() {
const [count , setCount] = useState(1);
const [val , setValue] = useState('');
function expensive() {
let sum = 0;
for (let i = 0; i < count * 100; i++){
sum += i;
}
return sum;
}
return (
<div>
<h3>{count} - {val} - {expensive()}</h3>
<div>
<button @click={() => setCount(count + 1)} > +c1 </button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</div>
)
}
这里创建了两个state,然后通过 expensive 函数,执行一次昂贵的计算,拿到count对应的某个值,我们可以看到:无论是修改count还是val,由组件的重新渲染,都会触发 expensive 的执行,但是这里的计算只依赖于count的值,在val修改的时候,是没有必要再次进行计算的,这种情况我们可以使用useMemo 如下:
import React from 'react'
export default function withoutMemo() {
const [count , setCount] = useState(1);
const [val , setValue] = useState('');
const expensive = useMemo(() => {
let sum = 0;
for (let i = 0; i < count * 100; i++){
sum += i;
}
return sum;
},[count]);
return (
<div>
<h3>{count} - {expensive()}</h3>
{val}
<div>
<button @click={() => setCount(count + 1)} > +c1 </button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</div>
)
}
上面我们可以看到,使用useMemo来执行昂贵的计算,然后将计算值返回,并且将count作为依赖值传递进去。这样,就只会在count改变的时候触发expensive执行,在修改val的时候,返回上一次缓存的值。
useCallback
讲完了useMemo,接下来是useCallback。useCallback跟useMemo比较类似,但它返回的是缓存的函数。我们看一下最简单的用法:
const fnA = useCallback(fnB, [a])
上面的useCallback会将我们传递给它的函数fnB返回,并且将这个结果缓存;当依赖a变更时,会返回新的函数。既然返回的是函数,我们无法很好的判断返回的函数是否变更,所以我们可以借助ES6新增的数据类型Set来判断,具体如下:
import React, { useState , useCallback } from 'react'
const set = new set();
export default function Callback() {
const [count , setCount] = useState(1);
const [val , setValue] = useState('');
const callback = useCallback(() => {
console.log(count)
},[count]);
set.add(callback)
return (
<div>
<h3>{count}</h3>
<h3>{set.size}</h3>
<div>
<button @click={() => setCount(count + 1)} > +c1 </button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</div>
)
}
我们可以看到,每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。
知道useCallback有什么样的特点,那有什么作用呢?
使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。
import React, { useState , useCallback , useEffect } from 'react'
function Parent() {
const [count , setCount] = useState(1);
const [val , setValue] = useState('');
const callback = useCallback(() => {
return count;
},[count]);
set.add(callback)
return (
<div>
<h3>{count}</h3>
<Child callback={callback} />
<div>
<button @click={() => setCount(count + 1)} > +c1 </button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</div>
)
}
function Child({ callback }) {
const [count , setCount] = useState(() => callback());
useEffect(() => {
setCount(callback());
},[callback]);
return <div> {count} </div>
}
不仅是上面的例子,所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。
useRef
useRef返回一个普通 JS 对象,可以将任意数据存到current属性里面,就像使用实例化对象的this一样。
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
});
const prevCount = prevCountRef.current;
return <h1>Now: {count}, before: {prevCount}</h1>;
}
自定义Hooks
自定义 Hook 其实就是一个普通的函数定义,以use开头来命名也只是为了方便静态代码检测,不以它开头也完全不影响使用。
总结
到此为止,Hooks 相关的内容基本介绍完了,想要彻底理解 Hooks 的设计是需要投入相当精力的,希望本文可以为你学习这一新特性提供一些帮助。