在介绍React Hooks之前,不得不先说说类组件
和函数式组件
关于React的两套API(类(class)API 和基于函数的钩子(hooks) API)。对于任何一个组件来说,可以用class来写,也可以使用function来写,但是既然区分开来了,那么它们之间就一定有点区别。
下面是两个方法的比较
import React from 'react'
class App extends React.Component {
state = {
count : 1}
render (){
return (
<div>
<p>hello world</p>
<p>{
this.state.count}</p>
<button onClick = {
() => {
console.log(this.state.count)}}>点击</button>
</div>
)
}
}
function App ({
count = 1}) {
return (
<div>
<p>hello world</p>
<p>{
count}</p>
<button onClick = {
() => {
console.log(count)}}>点击</button>
</div>
)
}
可以看到,相同的功能下函数式组件要相对简洁,而对于更复杂的功能,差别也就更加明显了。相比来说,官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较"轻",而类比较"重"。而且,钩子是函数,更符合 React 函数式的本质。
类组件的缺点 :
纯函数
:
函数的返回结果只依赖于它的参数。
不改变函数体外部数据、函数执行过程里面没有副作用。
函数式编程将那些跟数据计算无关的操作,都称为 副效应
(side effect) 。如果函数内部直接包含产生副效应的操作,就不再是纯函数了,我们称之为不纯的函数。纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含副效应,即进行数据的各种操作。这里就要引出React Hooks了。
借用阮一峰大佬的话 : 钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。
React Hooks 的设计目的,就是加强版函数组件,完全不使用"类",就能写出一个全功能的组件。
1.useState() //状态钩子
2.useContext() //共享状态钩子
3.useReducer() //action 钩子
4.useEffect() //副作用钩子
上面的几个钩子在阮一峰大神的文章有详细解释
阮一峰React Hooks入门教程
下面重点介绍几种少见的React Hooks钩子(链接中没有)
在类组件中,我们经常犯下面这样的错误:
class App {
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>;
}
老规矩,第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback 就会重新返回一个新的记忆函数提供给后面进行渲染。
这样只要子组件继承了 PureComponent 或者使用 React.memo 就可以有效避免不必要的 VDOM 渲染。
useCallback 的功能完全可以由 useMemo 所取代,如果你想通过使用 useMemo 返回一个记忆函数也是完全可以的。
useCallback(fn, inputs) === useMemo(() => fn, inputs).
所以前面使用 useCallback 的例子可以使用 useMemo 进行改写:
function App() {
const memoizedHandleClick = useMemo(() => () => {
console.log('Click happened')
}, []); // 空数组代表无论什么情况下该函数都不会发生改变
return <SomeComponent onClick={
memoizedHandleClick}>Click Me</SomeComponent>;
}
唯一的区别是:useCallback 不会执行第一个参数函数,而是将它返回给你,而 useMemo 会执行第一个函数并且将函数执行结果返回给你所以在前面的例子中,可以返回 handleClick 来达到存储函数的目的。
所以 useCallback 常用记忆事件函数,生成记忆后的事件函数并传递给子组件使用。而 useMemo 更适合经过函数计算得到一个确定的值,比如记忆组件。
function Parent({
a, b }) {
const child1 = useMemo(() => <Child1 a = {
a }/>, [a])
const child2 = useMemo(() => <Child2 b = {
b }/>, [b])
return (
<>
{
child1}
{
child2}
</>
)
}
当 a/b 改变时,child1/child2 才会重新渲染。从例子可以看出来,只有在第二个参数数组的值发生变化时,才会触发子组件的更新。
useRef 跟 createRef 类似,都可以用来生成对 DOM 对象的引用,看个简单的例子:
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 秒后得到最新的值。
通过 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 节点。
大部分情况下,使用 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 异步触发函数。
简单来说就是调用时机不同,useLayoutEffect
和原来componentDidMount
&componentDidUpdate
一致,在react完成DOM更新后马上同步调用的代码,会阻塞页面渲染。而useEffect
是会在整个页面渲染完才会调用的代码。
官方建议优先使用useEffect
在实际使用时如果想避免页面抖动(在useEffect
里修改DOM很有可能出现)的话,可以把需要操作DOM的代码放在useLayoutEffect
里。关于使用useEffect
导致页面抖动。
不过useLayoutEffect
在服务端渲染时会出现一个warning,要消除的话得用useEffect
代替或者推迟渲染时机。
下面附上阮一峰老师关于React Hooks的理解
React Hooks介绍