在了解react-hooks之前必须要先了解 react中什么是类组件 什么是函数组件,为什么react-hooks会推出hooks来加强函数组件
一、类组件
所谓的类组件就是基于ES6 class这种写法 继承了React.Compoent 或者PureComponent得来的组件,如下图所示:
class App extends React.Component {
constructor() {
this.state = { count: 1 }
}
handleSetCount = () => {
this.setState({
...this.state,
count: this.state.count + 1
})
}
render() {
return <div>{count}
<button onClick={() => { this.handleSetCount() }}>+1</button>
</div>
}
}
无hooks的函数组件:
function ChildA(props){
return(
<div>
{props.count}
</div>
)
}
1. 函数组件与类组件的区别对比
我们基于以上的demo,对比一下两者的不同:
1.类组件可以维护state,改变数据可以存储在state,函数组件不能
2.类组件可以访问生命周期函数,函数组件不能
3.类组件需要继承React的基本类,函数组件不需要
4.类组件可以实例化对象拿到实例化以后得this,基于this做一系列任务,函数组件不能
表面上看类组件可以做的事情很多,而函数组件能做的却很少,但是这并不代表函数组件就不行,特别是引入了React-hooks之后 发生了质的变化
2. 说了类组件的很多优势,再来说说类组件的劣势
1.类组件需要理解整个生命周期的流程走向
2.类组件需要深入理解this的定义,避免this的指向问题
3.类组件代码对比函数组件代码量更多,层级更深,不便于维护
4.类组件对比函数组件并不能准确的捕获render内部的状态,如果props变化 延迟触发的函数的props里的参数会发生变化,导致触发函数的参数准确
我们经过对比就会发现并不是说功能越多就越好,通常项目中可能遇到的一些小页面小组件我们可以用函数组件代码写起来更简单跟简洁更便于维护,类组件适合于那种类型复杂,功能很多的页面和组件,各有各的用处。
从趋势上来看,React团队在逐渐完善react-hooks的机制,因为函数组件编程更契合react的UI=f(data)的设计初衷,函数组件对比类组件更易于维护,因为他对比类组件来说紧紧的跟渲染绑定在了一起,不用this没有生命周期,吃进数据,吐出的就是UI,对于开发者而言写起来更爽更舒服
二、 函数组件(无hooks时的函数组件)
普通的函数组件就是以函数形式存在的组件,它内部不能维护状态和state,所以也叫无状态组件,如下图所示:
function App(props) {
const { count } = props
return <div>
来自父组件的参数{count}
</div>
}
(一) React-hooks的本质是一套 ‘钩子
react-hooks的定义就是钩子,需要什么就把什么’勾进来’,灵活而强大 ,类组件能做的事情,hooks都能做,下面我们就详细来说说hooks的哪些功能和运作机制
(二) 谈谈hooks的几个基本API
1. useState
useState与类组件的this.setState相似,都是用来维护state的,不同的是useState返回的是一个数组,解构出来的一个元素是我们需要定义的元素,第二个参数是修改这个元素的方法,这两个参数都可以由我们自己定义名称,方法通常以set开头,如下图所示:
const [state, setState] = useState(initialState);
和this.state不同的是useState定义的是单个变量或者对象,不会全部储存在一个state里面,这样用的时候不需要去state里面取了可以直接使用变量名,改变方法为自定义,区分度比较高,不用像class组件那样再关心哪个地方修改了state可能导致其中某个变量被改变
2. useEffect
useEffect允许函数组件执行副作用操作(发送异步请求等副作用)。与类组件相比函数组件缺少了state和生命周期,useState的引入解决了函数组件不能维护组件内部状态的问题,useEffect的引入则为函数组件解决了生命周期上的缺失。
useEffect能够支持函数组件执行副作用,我们以前在类组件中在ComponentDidmount 和ComponentDidUpate以及ComponentWillUnmount里面做的事情都可以使用useEffect完成,比如请求数据,获取DOM节点,组件挂载以后初始化state数据等等
useEffect对比类组件需要操作3个组件才能完成的事情,现在一个函数就能完成,下面来分析一下它的主要用法,先看以下代码:
function App() {
const [count, setCount] = useState(0)
const [name, setName] = useState('张三')
useEffect(() => {
console.log('组件初始化加载')
}, [])
useEffect(() => {
console.log('count变量更新时调用了')
return () => {
console.log("组件被卸载了")
}
}, [count])
return <div>
{count}
<button onClick={() => { setCount(count + 1) }}>+1</button>
{name}
<button onClick={() => { setName('李四') }}>+1</button>
</div>
}
- useEffect的第一个参数是一个回调函数,我们通常处理的异步请求,操作DOM都可以在这里完成
- useEffect的第二个参数是一个数组,不传参数的时候组件内部的任何state发生改变都会调用这个useEffect
- 传一个空数组就代表只有在组件初始化挂载完成的时候调用一次,以后发生任何变化都不会在调用这个useEffect
- 传一个或者多个变量的时候,这个变量或者其中一个变量发生变化就会调用这个useEffect
- useEffect内部可以return 一个函数,这个函数将在组件被卸载的时候调用
注:setInterval定时器操作在useEffect里面使用的时候一定要清除定时器,否则将会出现问题,示例代码如下:
代码实现业务为定时出现一个可以抢购的满减券
import { useEffect, useState } from 'react';
import './App.css';
function App() {
let [count, setCount] = useState(10);
useEffect(() => {
let id = setInterval(() => {
if(count !== 0){
setCount(count - 1);
}
}, 1000);
return () => clearInterval(id);
}, [count])
return (
<div id="App">
<div className='text'>
<div className='text_top'> 显卡满500减1券</div>
<div className='text_bottom'> 京东某显卡店</div>
</div>
<div className='panicbuying'>
{
count !== 0 ? count + "S" :
<button className='button'>抢购</button>
}
</div>
</div>
);
}
export default App;
3. useMemo
useMemo是用于优化组件性能的,useMemo传入2个参数,一个是你要渲染的函数组件,一个是你要更新数据的变量,代码如下所示:
function Test() {
const [text, setText] = useState("我是子组件的文本")
const [count, setCount] = useState(1)
const handleSetCount = () => {
setCount(count + 1)
}
const handleSetText = () => {
setText("修改之后的子组件文本")
}
return (
<div className="App" >
<button onClick={handleSetCount}>点击+1</button>
<button onClick={handleSetText}>修改A文本</button>
<ChildA text={text} count={count} />
</div>
)
}
function ChildA(props) {
//渲染文字内容
const renderText = (text) => {
console.log("ChildA 文字内容渲染了")
return <div>
子组件的内容:
{text}
</div>
}
//渲染父组件的数字
const renderCount = (count) => {
console.log("父组件的数字 内容渲染了")
return <div>
父组件的数字:
{count}
</div>
}
const MemorenderText = useMemo(() => renderText(props.text), [props.text])
const MemorenderCount = useMemo(() => renderCount(props.count), [props.count])
return (
<div>
{MemorenderText}
{MemorenderCount}
</div>
);
}
export default Test
如果对性能优化有兴趣的同学可以移步React性能优化(React笔记之二)-CSDN博客
4. useCallback
useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。通俗易懂的话就是你在父组件中有一个回调函数需要传给子组件来使用,但是你不想在每次父组件都更新的时候.props更新这个回调函数就会更新新的引用地址,导致子组件重新渲染,那么React.memo就会失效(因为他只是浅比较),代码如下:
import React,{ useCallback, useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
const [Name, setName] = useState("我是谢XX");
const handleADD = useCallback(() => {
setCount(count + 1)
}, [count]);
const handleSetName = ()=>{
setName(Name + 1)
}
return (
<div id="App">
{Name}
<button onClick={handleSetName}>点击修改名字</button>
<ChildA handleChildADD={handleADD} count={count}/>
</div>
);
}
const ChildA = React.memo(function ChildA(props){
console.log("子组件发生了渲染!")
return(
<div>
{props.count}
<button onClick={props.handleChildADD}>+</button>
</div>
)
}
)
export default App;
刷新页面,我们看到子组件渲染了(有两次是因为React开启了严格模式-StrictMode,)
但是我们继续点击修改Name会发现子组件没有更新,依旧是之前打印的2次
如果我们把useCallback去掉的话,就会看到每点击一次修改Name按钮,子组件都会更新
import React,{ useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
const [Name, setName] = useState("我是谢XX");
const handleADD = () => {
setCount(count + 1)
}
const handleSetName = ()=>{
setName(Name + 1)
}
return (
<div id="App">
{Name}
<button onClick={handleSetName}>点击修改名字</button>
<ChildA handleChildADD={handleADD} count={count}/>
</div>
);
}
const ChildA = React.memo(function ChildA(props){
console.log("子组件发生了渲染!")
return(
<div>
{props.count}
<button onClick={props.handleChildADD}>+</button>
</div>
)
}
)
export default App;
5. 自定义hook
自定义hook就是将组件的公共逻辑提取出来封装好的一个函数,但是因为其中会用到hook钩子,所以叫自定义hook,通常是为了提高代码的可读性以及逻辑复用率
显示代码:
import useRequestApi from '@/component/hooks/useRequestApi';
type UserType = {
name: string;
vip: number;
}
function Home() {
const { loading, data, handleGetData } = useRequestApi<UserType[]>("api")
return (
<div>
{loading ? (
<div>加载中……</div>
) : (
<div>
{data.map((item, index) => (
<p key={index}>
姓名:{item.name}, vip等级:{item.vip}
</p>
))}
</div>
)}
<button onClick={handleGetData}>加载数据</button>
</div>
)
}
export default Home
自定义hook:
import { useState, useEffect } from 'react';
const useRequestApi = <T,>(url: string) => {
const [data, setData] = useState<T>();
const [loading, setLoading] = useState<boolean>(false);
const requestApi = (url: string) => {
return new Promise<any>((resolve, reject) => {
if (url === 'api') {
resolve([
{ name: '用户1', vip: 1 },
{ name: '用户2', vip: 2 },
]);
} else {
reject('404 请求参数错误');
}
});
};
const fetchData = async () => {
setLoading(true);
setTimeout(async () => {
const response = await requestApi(url);
setData(response);
setLoading(false);
}, 2000);
};
useEffect(() => {
fetchData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url])
const handleGetData = () => {
fetchData();
};
return {
data: data || [], loading, handleGetData
}
}
export default useRequestApi
小结: 其实hook还有很多,但是这里就不一一列举了.想学习更多看api可以去官网自行查阅
(三) 为什么需要 React-Hooks,它比类组件好在哪里?
为什么需要React-Hooks,这经常是面试里面常常问到的,我个人从一些社区大佬的文章中理解了4个点:
- 告别难以理解的class
- 解决业务逻辑难以拆分的问题
- 使状态逻辑变得简单可行
- 函数组件从设计思想上来看,更契合React的的设计理念(经典:函数组件会捕获 render 内部的状态)
1. 告别难以理解的class : class的两大痛点(生命周期和this)
初学React的时候大家学习class this和生命周期的痛苦我相信大部分人都深有体会,偶现的this.XXX is undefined this的指向问题,生命周期componentWillRecevieProps里面的this.setStatede调用的死循环等等问题总结一下就是以下两点
- this指向问题经常需要解决,比如利用箭头函数,bind方法等等
- 生命周期逻辑划分导致函数逻辑难以拆分等等
- 生命周期学习成本高
2. hooks是如何解决业务逻辑拆封的
在类组件中,我们通常会把逻辑拆分到ComponentDidmount和ComponentDidUpdate里面去,比如在Didmount里面发送请求,初始化数据,在DidUpdate里面获取props的更新改变state,获取Didmount里面异步请求的返回更新state等等,这就很难受,明明是不同的事情但是却放在了一个生命周期函数里面,如下代码所示:
componentDidMount() {
// 1. 这里发起异步调用
// 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM
// 3. 这里设置一个订阅
// 4. 这里随便干点别的什么
// ...
}
componentWillUnMount() {
// 在这里卸载订阅
}
componentDidUpdate() {
// 1. 在这里根据 DidMount 获取到的异步数据更新 DOM
// 2. 这里从 props 里获取某个数据,根据这个数据更新 DOM(和 DidMount 的第2步一样)
}
在react-hooks的帮助下我们借助钩子函数,可以轻松的把相同的逻辑放在一起,可以设置专门管理DOM的函数组件,可以设置专门管理异步请求的组件,可以设置专门初始化的组件等等,Hooks能够帮助我们完成逻辑的聚合可以使我们维护起来更方便,减少冗余的代码和复杂的组件
3. 使状态逻辑变得简单可行
我们平常想要复用状态逻辑,通常是使用高阶组件和Render Props 这些组件设计模式,使用这些默认虽然也完成了我们的任务需求,但是高昂的学习成本来却只换取了那么一点点的灵活度,实在是有点得不偿失啊
Hooks可以视作是React团队为了解决状态复用问题的一个原生途径,可以通过自定义hook来解决我们的状态逻辑复用问题,这样可以既不破坏组件结构,又可以达到复用状态逻辑的目的
总结一下:虽然说了这么hooks使用的好处,但是hooks也并不是万能的,在处理复杂的组件的时候逻辑不好拆分,使用类组件会是一个很好的选择,一味的轻量并不是处理问题的唯一途径。但是随着hooks的不断改进以后类组件的使用机会将大大减少,React对hooks的功能开发的越来越完善了