什么是hooks
在React中,hooks是16.8版本新加入的新特性,我们可以在16.8版本以上的react中解构出这些新增的hooks。那么,我们为什么要使用hooks呢?
hooks可以让工厂函数组件拥有与类组件类似的各种状态、生命周期等,这让我们可以减少使用笨重的类组件。
hooks的本质就是函数。
如何使用hooks
从react中解构出react自带的hooks
例如:useState
import React, {useState} from "react";
function App() {
// 我们需要在函数组件顶层使用hooks
const [count,setCount] = useState();
return (
<>
<div>如何使用hooks</div>
</>
)
}
1、useState 保存组件状态
在类组件中,我们使用 this.state
来保存组件状态,并对其修改触发组件重新渲染。比如下面这个简单的计数器组件,很好诠释了类组件如何运行:
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
name: "alife"
};
}
render() {
const { count } = this.state;
return (
<div>
Count: {count}
<button onClick={() => this.setState({ count: count + 1 })}>+</button>
<button onClick={() => this.setState({ count: count - 1 })}>-</button>
</div>
);
}
}
一个简单的计数器组件就完成了,而在函数组件中,由于没有 this 这个黑魔法,React 通过 useState 来帮我们保存组件的状态。
import React, { useState } from "react";
function App() {
const [obj, setObject] = useState({
count: 0,
name: "alife"
});
return (
<div className="App">
Count: {obj.count}
<button onClick={() => setObject({ ...obj, count: obj.count + 1 })}>+</button>
<button onClick={() => setObject({ ...obj, count: obj.count - 1 })}>-</button>
</div>
);
}
通过传入 useState 参数后返回一个带有默认状态和改变状态函数的数组。通过传入新状态给函数来改变原本的状态值。值得注意的是 useState 不帮助你处理状态,相较于 setState 非覆盖式更新状态,useState 覆盖式更新状态,需要开发者自己处理逻辑。(代码如上)
似乎有个 useState 后,函数组件也可以拥有自己的状态了,但仅仅是这样完全不够。
2、useEffect 处理副作用
函数组件能保存状态,但是对于异步请求,副作用(可以理解为异步操作)的操作还是无能为力,所以 React 提供了 useEffect 来帮助开发者处理函数组件的副作用,在介绍新 API 之前,我们先来看看类组件是怎么做的:
import React, { Component } from "react";
class App extends Component {
state = {
count: 1
};
componentDidMount() {
const { count } = this.state;
document.title = "componentDidMount" + count;
this.timer = setInterval(() => {
this.setState(({ count }) => ({
count: count + 1
}));
}, 1000);
}
componentDidUpdate() {
const { count } = this.state;
document.title = "componentDidMount" + count;
}
componentWillUnmount() {
document.title = "componentWillUnmount";
clearInterval(this.timer);
}
render() {
const { count } = this.state;
return (
<div>
Count:{count}
<button onClick={() => clearInterval(this.timer)}>clear</button>
</div>
);
}
}
在例子中,组件每隔一秒更新组件状态,并且每次触发更新都会触发 document.title 的更新(副作用),而在组件卸载时修改 document.title(类似于清除)
从例子中可以看到,一些重复的功能开发者需要在 componentDidMount 和 componentDidUpdate 重复编写,而如果使用 useEffect 则完全不一样。
import React, { useState, useEffect } from "react";
let timer = null;
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = "componentDidMount" + count;
},[count]);
useEffect(() => {
timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 一定注意下这个顺序:
// 告诉react在下次重新渲染组件之后,同时是下次执行上面setInterval之前调用
return () => {
document.title = "componentWillUnmount";
clearInterval(timer);
};
}, []);
return (
<div>
Count: {count}
<button onClick={() => clearInterval(timer)}>clear</button>
</div>
);
}
我们使用 useEffect 重写了上面的例子,useEffect 第一个参数接收一个函数,可以用来做一些副作用比如异步请求,修改外部参数等行为,而第二个参数称之为dependencies,是一个数组,如果数组中的值变化才会触发 执行useEffect 第一个参数中的函数。返回值(如果有)则在组件销毁或者调用函数前调用。
- 1.比如第一个 useEffect 中,理解起来就是一旦 count 值发生改变,则修改 documen.title 值;
- 2.而第二个 useEffect 中传递了一个空数组[],这种情况下只有在组件初始化或销毁的时候才会触发,用来代替 componentDidMount 和 componentWillUnmount,慎用;
- 还有另外一个情况,就是不传递第二个参数,也就是useEffect只接收了第一个函数参数,代表不监听任何参数变化。每次渲染DOM之后,都会执行useEffect中的函数。
useEffect的执行时机,因为函数组件类似于类组件的render阶段的代码,所以每一次状态更新,都会将函数组件顶层的所有代码都执行一遍,useEffect的return在首次执行时都不会执行
第一种,第二个参数为空时
return前的代码在首次挂载和每次状态更新组件挂载之后执行(componentDidMount),
return返回的箭头函数中的代码在首次渲染不会执行,在组件状态更新后,重新渲染挂载之前执行(componentDidUpdate),组件销毁前也会执行(componentWillUnmount)
第二种,第二个参数为空数组时
return前的代码在组件只在首次挂载后执行一次(componentDidMount)
return返回的箭头函数中的代码只在卸载前执行一次(componentWillUnmount)
第三种,第二个参数为一个有依赖的数组
return前的代码在首次挂载以及依赖每次改变并挂载后执行一次(componentDidMount)
return返回的代码在依赖改变之后挂载前执行(componentDidUpdate),销毁阶段也会执行(componentWillUnmount)
基于这个强大 Hooks,我们可以模拟封装出其他生命周期函数,比如 componentDidUpdate 代码十分简单
function useUpdate(fn) {
// useRef 创建一个引用
const mounting = useRef(true);
useEffect(() => {
if (mounting.current) {
mounting.current = false;
} else {
fn();
}
});
}
现在我们有了 useState 管理状态,useEffect 处理副作用,异步逻辑,学会这两招足以应对大部分类组件的使用场景。
3、useContext 减少组件层级
上面介绍了 useState、useEffect 这两个最基本的 API,接下来介绍的 useContext 是 React 帮你封装好的,用来处理多层级传递数据的方式,在以前组件树种,跨层级祖先组件想要给孙子组件传递数据的时候,除了一层层 props 往下透传之外,我们还可以使用 React Context API 来帮我们做这件事,举个简单的例子:
const { Provider, Consumer } = React.createContext(null);
function Bar() {
return <Consumer>{color => <div>{color}</div>}</Consumer>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<Provider value={"grey"}>
<Foo />
</Provider>
);
}
通过 React createContext 的语法,在 APP 组件中可以跨过 Foo 组件给 Bar 传递数据。而在 React Hooks 中,我们可以使用 useContext 进行改造。
const colorContext = React.createContext("gray");
function Bar() {
const color = useContext(colorContext);
return <div>{color}</div>;
}
function Foo() {
return <Bar />;
}
function App() {
return (
<colorContext.Provider value={"red"}>
<Foo />
</colorContext.Provider>
);
}
传递给 useContext 的是 context 而不是 consumer,返回值即是想要透传的数据了。用法很简单,使用 useContext 可以解决 Consumer 多状态嵌套的问题。
function HeaderBar() {
return (
<CurrentUser.Consumer>
{user =>
<Notifications.Consumer>
{notifications =>
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
}
}
</CurrentUser.Consumer>
);
}
而使用 useContext 则变得十分简洁,可读性更强且不会增加组件树深度。
function HeaderBar() {
const user = useContext(CurrentUser);
const notifications = useContext(Notifications);
return (
<header>
Welcome back, {user.name}!
You have {notifications.length} notifications.
</header>
);
}
4、useReducer
useReducer中可以解构出state和dispatch,而他的用法与redux非常类似,我们在useReducer中放入两个参数,一个为reduce模块,一个为初始化仓库状态。
reduce模块:reduce模块接收两个参数,一个为仓库状态(state),一个为方法(action),action为一个type/payload对象,type为方法名,payload为传参。
useReducer 这个 Hooks 在使用上几乎跟 Redux/React-Redux 一模一样,唯一缺少的就是无法使用 redux 提供的中间件。
写一个useReducer的加减运算方法
import React, { useReducer } from "react";
const initialState = {
count: 0
};
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + action.payload };
case "decrement":
return { count: state.count - action.payload };
default:
throw new Error();
}
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: "increment", payload: 5 })}>
+
</button>
<button onClick={() => dispatch({ type: "decrement", payload: 5 })}>
-
</button>
</>
);
}
用法跟 Redux 基本上是一致的,用法也很简单,算是提供一个 mini 的 Redux 版本。
5、useCallback 记忆函数
在类组件中,我们经常犯下面这样的错误:
class App extends Component {
render() {
return <div>
<SomeComponent style={{ fontSize: 14 }} doSomething={ () => { console.log('do something'); }} />
</div>;
}
}
这样写有什么坏处呢?一旦 App 组件的 props 或者状态改变了就会触发重渲染,即使跟 SomeComponent 组件不相关,由于每次 render 都会产生新的 style 和 doSomething(因为重新render前后, style 和 doSomething分别指向了不同的引用),所以会导致 SomeComponent 重新渲染,倘若 SomeComponent 是一个大型的组件树,这样的 Virtual Dom 的比较显然是很浪费的,解决的办法也很简单,将参数抽离成变量。
const fontSizeStyle = { fontSize: 14 };
class App {
doSomething = () => {
console.log('do something');
}
render() {
return <div>
<SomeComponent style={fontSizeStyle} doSomething={ this.doSomething } />
</div>;
}
}
在类组件中,我们还可以通过 this 这个对象来存储函数,而在函数组件中没办法进行挂载了。所以函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。
function App() {
const handleClick = () => {
console.log('Click happened');
}
return <SomeComponent onClick={handleClick}>Click Me</SomeComponent>;
}
这里多说一句,一版把函数式组件理解为class组件render函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。所以上述代码中每次render,handleClick都会是一个新的引用,所以也就是说传递给SomeComponent组件的props.onClick一直在变(因为每次都是一个新的引用),所以才会说这种情况下,函数组件在每次渲染的时候如果有传递函数的话都会重渲染子组件。
而有了 useCallback 就不一样了,你可以通过 useCallback 获得一个记忆后的函数。
function App() {
const memoizedHandleClick = useCallback(() => {
console.log('Click happened')
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}
老规矩,第一个参数称为内联回调函数,第二个参数传入一个依赖项数组,返回值称为memoized回调函数,数组中的依赖项发生改变,useCallback 就会更新这个memoized回调函数,如果传入一个空数组,则表示无论如何都不会更新这个memoized回调函数。useCallback和useMemo都会在组件首次加载时更新一次,后面的情况就会随着依赖数组的情况而改变。
useCallback不是用来解决组件中有过多函数导致的性能问题,1、因为对现在的计算机来说,创建一个函数对象的开销是很小的,2、我们每次使用useCallback都会产生额外的性能开销,就是对deps的监听,3、其实每次组件重新渲染,即使deps没有改变,他也会重新创建内部函数作为useCallback的实参
useCallback的实际作用其实是避免子组件不必要的reRender的,主要是为了保存传给子组件的函数,避免因为重新创建函数实例导致props发生变化而引起的子组件重新渲染,需要搭配React.memo缓存组件才能做到避免子组件无意义的reRender。
举例:首先,假如我们不使用useCallback,在父组件中创建了一个名为handleClick的事件处理函数,根据需求我们需要把这个handleClick传给子组件,当父组件中的一些state变化后,父组件会重新reRender,然后重新创建名为handleClick的函数实例,并传给子组件,这时即使使用React.memo把字组件包裹起来,子组件也会重新渲染,因为props已经变化了,但是这个props改变导致的渲染是无意义的。这时我们就可以用useCallback将handleClick函数包裹起来,这样父组件状态发生的变化就不会导致handleClick的重新渲染了,只有当deps发生改变时才会创建新的handleClick实例,导致子组件重新渲染
搭配:当然我们的子组件如果是函数组件,需要用React.memo包起来,如果是类组件,则要通过继承React.PureComponent对传入的数据进行简单的浅层对比,不然父组件重新渲染还是会导致子组件的重新渲染。
// index.tsx
import React, {
useState,
useEffect,
useRef,
useCallback,
useMemo,
} from 'react';
import App from './app';
const Index = () => {
const [state1, setState1] = useState(false);
const [state2, setState2] = useState(false);
// 创建一个Ref,使用Ref的current保存state1的状态
const sta: React.MutableRefObject<null | boolean> = useRef(null);
// 创建我们要传给子组件的函数,保存在useCallback中
const handleClick = useCallback(() => {
console.log(sta.current);
}, []);
// 当state1的状态发生改变时,更新Ref的current里面保存的状态
useEffect(() => {
console.log('update');
sta.current = state1;
console.log(state1);
}, [state1]);
return (
<>
<button onClick={() => setState1((d) => !d)}>button</button>
<button onClick={() => setState2((d) => !d)}>button--other</button>
<hr />
{state1 ? <App xixi={handleClick} /> : null}
</>
);
};
export default Index;
// App.tsx
import React, { useState, useEffect, memo } from 'react';
// 使用React.memo缓存组件,如果组件的props没有发生改变就不会重新渲染
const App = memo((props: any) => {
// 如果没有memo,我们会发现每次与props无关的父组件中的state2的改变,都会导致子组件的reRender,下面这条代码会重新执行,但我们加了memo后就会发现,props没有改变,子组件也不会重新渲染
console.log('重新执行');
return (
<>
<button onClick={() => props.xixi()}>xixi</button>
</>
);
});
6、useMemo 记忆组件
useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。
useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs).
所以前面使用 useCallback 的例子可以使用 useMemo 进行改写:
function App() {
const memoizedHandleClick = useMemo(() => () => {
console.log('Click happened')
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onClick={memoizedHandleClick}>Click Me</SomeComponent>;
}
将useCallback想要保存的回调函数放在useMemo的第一个类型为回调函数的参数的return结果中就完成了useCallback到useMemo的改写。
唯一的区别是:useCallback 将他保存的第一个参数这个函数返回给你,,而 useMemo 会将第一个参数的return的结果返回给你。所以在前面的例子中,可以返回 handleClick 来达到存储函数的目的。
所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。
记忆组件
function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
)
}
当 a/b 改变时,child1/child2 才会重新渲染。从例子可以看出来,只有在第二个参数数组的值发生变化时,才会触发子组件的更新。
7、useRef 保存引用值
useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用,看个简单的例子:
作用:
1、如果将ref放在DOM节点上,那么我们可以使用uesRef获得的变量来读取真实DOM的信息
2、我们可以在ref变量创建的时候,就在括号中传入一个值(可以为任何类型,除了undefined)作为ref.current的值,我们可以理解为我们在内存中开辟了一个不会被外界状态改变而改变的一块区域,他的引用地址就是我们的ref.current,我们可以在这里面保存我们不想被改变的数据并将这个引用地址传给别的函数、方法、钩子甚至组件使用。
import React, { useState, useRef } from "react";
function App() {
let [name, setName] = useState("Nate");
let nameRef = useRef();
const submitButton = () => {
setName(nameRef.current.value);
};
return (
<div className="App">
<p>{name}</p>
<div>
<input ref={nameRef} type="text" />
<button type="button" onClick={submitButton}>
Submit
</button>
</div>
</div>
);
}
useRef 返回的值传递给组件或者 DOM 的 ref 属性,就可以通过 ref.current 值访问真实的 DOM 节点,从而可以对 DOM 进行一些操作,比如监听事件等等。
当然 useRef 远比你想象中的功能更加强大,useRef 的功能有点像类属性,或者说您想要在组件中记录一些值,并且这些值在稍后可以更改。
利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。
React Hooks 中存在 Capture Value 的特性:
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
alert("count: " + count);
}, 3000);
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>增加 count</button>
<button onClick={() => setCount(count - 1)}>减少 count</button>
</div>
);
}
先点击增加button,后点击减少button,3秒后先alert 1,后alert 0,而不是alert两次0。这就是所谓的 capture value 的特性。而在类组件中 3 秒后输出的就是修改后的值,因为这时候 message 是挂载在 this 变量上,它保留的是一个引用值,对 this 属性的访问都会获取到最新的值。讲到这里你应该就明白了,useRef 创建一个引用,就可以有效规避 React Hooks 中 Capture Value 特性。
function App() {
const count = useRef(0);
const showCount = () => {
alert("count: " + count.current);
};
const handleClick = number => {
count.current = count.current + number;
setTimeout(showCount, 3000);
};
return (
<div>
<p>You clicked {count.current} times</p>
<button onClick={() => handleClick(1)}>增加 count</button>
<button onClick={() => handleClick(-1)}>减少 count</button>
</div>
);
}
只要将赋值与取值的对象变成 useRef,而不是 useState,就可以躲过 capture value 特性,在 3 秒后得到最新的值。
8、useImperativeHandle 透传 Ref
通过 useImperativeHandle 用于让父组件获取子组件内的索引
import React, { useRef, useEffect, useImperativeHandle, forwardRef } from "react";
function ChildInputComponent(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => inputRef.current);
return (
<input
type="text"
name="child input"
ref={inputRef} />
);
}
const ChildInput = forwardRef(ChildInputComponent);
function App() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<ChildInput ref={inputRef} />
</div>
);
}
通过这种方式,App 组件可以获得子组件的 input 的 DOM 节点。
也可以直接将父组件传下来的ref放在子组件的DOM上使用
9. useLayoutEffect 同步执行副作用
大部分情况下,使用 useEffect 就可以帮我们处理组件的副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行。
function App() {
const [width, setWidth] = useState(0);
useLayoutEffect(() => {
const title = document.querySelector("#title");
const titleWidth = title.getBoundingClientRect().width;
console.log("useLayoutEffect");
if (width !== titleWidth) {
setWidth(titleWidth);
}
});
useEffect(() => {
console.log("useEffect");
});
return (
<div>
<h1 id="title">hello</h1>
<h2>{width}</h2>
</div>
);
}
在上面的例子中,useLayoutEffect 会在 render,DOM 更新之后同步触发函数,会优于 useEffect 异步触发函数。
10、useEffect和useLayoutEffect有什么区别?
简单来说就是调用时机不同,**useLayoutEffect**
和原来**componentDidMount**
&**componentDidUpdate**
一致,在react完成DOM更新后马上同步调用的代码,会阻塞页面渲染。而**useEffect**
是会在整个页面渲染完才会调用的代码。
官方建议优先使用useEffect
However, we recommend starting with useEffect first and only trying useLayoutEffect if that causes a problem.
在实际使用时如果想避免页面抖动(在useEffect
里修改DOM很有可能出现)的话,可以把需要操作DOM的代码放在useLayoutEffect
里。关于使用useEffect
导致页面抖动。
不过useLayoutEffect
在服务端渲染时会出现一个warning,要消除的话得用useEffect
代替或者推迟渲染时机。
11、自定义hook
自定义防抖
import React, { useRef, useEffect, useCallback } from 'react';
const useDebounce = (fn: Function, time: number) => {
const { current } = useRef({ fn, timer: 0 });
useEffect(() => {
current.fn = fn;
}, [fn])
return useCallback(function f(...arg) {
if (current.timer) {
clearTimeout(current.timer);
}
current.timer = window.setTimeout(() => {
current.fn(...arg)
}, time)
}, [])
}
export default useDebounce;
自定义节流
import React, { useRef, useEffect, useCallback } from 'react';
const useThrottle = (fn: Function, time: number) => {
const { current } = useRef({ fn, timer: 0, currentTime: 0 })
useEffect(() => {
current.fn = fn;
}, [fn]);
return useCallback((...arg) => {
if (current.currentTime === 0) {
clearTimeout(current.timer);
current.currentTime = 1;
current.timer = window.setTimeout(() => {
current.fn(...arg);
current.currentTime = 0;
}, time)
}
}, [])
}
export default useThrottle;
自定义倒计时