引言
本文将介绍一些在日常开发中使用react hooks的心得体会,将着重介绍两个hooks的使用:useState 和 useEffect这两个比较常用的使用方式。
1、在react class组件如此成熟的时代,我们为什么还要使用react hooks呢,
首先我们要明确几点:
- react hooks完全是可选择的,你可以重写老的冗余组件,可以在新的组件使用,也可以啥都不学啥都不做,当一条咸鱼🐟。
- 100% 向后兼容 Hooks不包含任何破坏式<颠覆式>更新。
- class不会被移除,依然与hooks并存,但官方更建议使用hooks,
因为在大部分情况下凡是class能做到的hooks都能做到。
在使用react class组件时,
2、跨组件复用含状态(state)的逻辑十分困难,因为我们不得不通过实例对象this上挂载的各种方法来实现业务逻辑(如setState),而过多的this很容易让初级开发者产生一些疑惑(诸如这个this指向谁的问题,同样的问题也发生在vue2中)
3、复杂的组件变得冗余,在项目的后期,class组件很可能变得难以维护,试想一下,一个上千行的组件,内部会有上百个函数,由于每个函数都有自己的作用域,所以如果我们想在这些方法中使用state props的属性,我们就得不停的重定义,而hooks的functionComponent特性,让我们避免了这种事情的发生,从而减少了代码重复也提高了效率。(在不断地研究后发现其实这样做会存在一些性能问题也不好说,但是对于大部分项目而言并不太影响性能)
4、学习成本
这也是hooks的优势之一,对于掌握react的同学来说这简直小菜一碟,对于还没有使用react的同学来说也不会太难,因为vue3与react hooks从思想设计程度上是一致的。当然,react多年来对比vue,业界普遍认为react上手较难,所以从招聘广告来看除了知名大厂使用react,其他公司用vue会更多,而hooks的出现一定程度上降低了难度。
5、hooks将逻辑更细粒度划分,就是使用hooks不仅仅可以封装带dom的组件,也可以封装一些公用的响应式逻辑方法(如实时监听返回一个用户登录状态,这个状态我们可以在任何hooks组件中复用),很显然class组件在处理这类事情的时候,写法稍微复杂一些。
useState与useEffect
我们先简单介绍一下 useState与useEffect 是如何使用的
const [demo1Name, setDemo1Name] = useState('');
返回一个两项数组,第一项为useState返回的最新值,第二项可以理解为 具名化的setState,我们通过传 空字符串 作为 useState 唯一的参数来将其初始化为 空字符串
// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
提示 如果你熟悉
React class的生命周期函数,你可以把useEffect Hook看做componentDidMount,componentDidUpdate和componentWillUnmount这三个函数的组合。
// 相当于每次都会触发 componentDidUpdate
useEffect(() => {
console.log('我一直在触发useEffect1')
})
// 只有组件初始化的时候触发 componentDidMount
useEffect(() => {
console.log('我初始化的时候触发了useEffect2')
}, [])
// 只要nameZyy变化了,以及初始化时就触发 类似于 vue中的 watch但比watch强大
useEffect(() => {
console.log('因为nameZyy变化了,我触发了useEffect3')
}, [nameZyy])
// 只要nameXyb变化了,以及初始化时就触发
useEffect(() => {
console.log('因为nameXyb变化了,我触发了useEffect4')
}, [nameXyb])
// 组件销毁时触发return 中的函数 componentWillUnmount
useEffect(()=>{
return () => {
// 清除定时器、清除事件监听函数
}
}, [])
每次组件更新相当于重新执行组件function
分享会跳转vscode
-
在
class组件中,触发更新视图操作往往只需执行render函数以及特定update生命周期,它内部的状态通过this实例指向唯一内存地址,所以保证了组件状态在更新后的统一性。 -
在
hooks中,没有this,触发视图渲染后,需要重新执行这个组件函数,所导致的后果便是内部声明的变量会全部重新定义一遍,所以hooks组件借助了react内部的方法让该组件从无状态变为有状态(useState),在useState内部保存着一个你初始化时定义的 变量。
为了更好的论证上面的理论,我这边借鉴了 网易云团队给出的概念 —— 类似动画帧的概念(我们将每次触发渲染称为一帧),更容易理解hooks的执行机制
每一帧定义的变量方法都是独立的,前后互不影响
为了解释帧的概念,这里有个定时器的例子
分享会跳转vscode
function Demo2() {
const [num, setNum] = useState(0);
// componentDidMount
useEffect(() => {
setInterval(() => {
setNum(num + 1) // num一直是0,我拿的一直是第一帧即初始化时候的值
console.log(num)
}, 1000);
}, [])
useEffect(()=>{
console.log(num,'我变成了1,之后一直是1,切不会触发渲染了')
},[num])
return (
<Card title="demo2">
<p>{num}</p>
</Card>
)
}
上方demo2是如何执行的呢,首先初始化第一帧
可以模拟为下方第一帧代码
function Demo2_1() {
const num_1 = 0;
// componentDidMount
useEffect(() => {
setInterval(() => {
setNum(num_1 + 1)
console.log(num_1)
}, 1000);
}, [])
useEffect(()=>{
console.log(num_1,'我初始化的时候执行以下0,第二帧我变成了1,之后一直是1,切不会触发渲染了')
},[num_1])
return (
<Card title="demo2">
<p>{num_1}</p>
</Card>
)
}
在demo2组件内部定义了 num_1 常量,两个 useEffect 函数,其中一个useEffect 只会在初始化的第一帧执行。
所导致的后果是:这个第一帧内部的代码就始终保持不变,即内部setInterval的回调函数中的变量num_1一直是0,从而导致setNum一直重复(0+1) 也导致了第二个useEffect console.log num_1 的值 只会增加到1,就不再触发渲染了。
分享会跳转vscode
如果上面的例子 你觉得没有说服力,ok!我给你一个网易大v的例子
const Demo3 = (props) => {
const { count } = props;
const handleClick = () => {
setTimeout(() => {
alert(count);
}, 3000);
};
return (
<Card title="demo3">
<p>{count}</p>
<button onClick={handleClick}>点我弹我</button>
</Card>
);
}
export default Demo3;
我们通过props将count传入子组件,初始值为0,每隔一秒加1,我们点击 点我弹我按钮,alert的count值,发现并不是页面上还在自增的值,而是等于点击的那一刻的值。 从而论证了每一帧的值都是 独立的,就是一个普普通通的数据而已。
- 当然这种情况只会在 hooks中出现,若使用class组件,由于this指向同一实例,this内部属性值是持续变化的。
class Demo4 extends Component {
handleClick = () => {
setTimeout(() => {
alert(this.props.count);
}, 3000);
};
render() {
const {
count
} = this.props;
return (
<Card title="demo4">
<p>{count}</p>
<Button onClick={this.handleClick}>点我弹我4</Button>
</Card>
);
}
}
小结
上面两个例子,通过动画帧的概念辅助理解每次触发render时 hooks函数重新定义内部变量,并且通过useState等方式,react帮我们维护了一组状态,这些状态独立于函数存放。
需要明确的是,我们通过useState解构出来的变量 作为函数中的一个常量,就是普通的数据,并不存在诸如数据绑定这样的操作来驱使 DOM 发生更新。在调用 hooks中的setState 后,React 将重新执行 render 函数,仅此而已。
永远不要欺骗useEffect
对于 useEffect 来说,执行的时机是完成所有的 DOM 变更并让浏览器渲染页面后
const Demo5 = (props) => {
const {
id
} = props;
useEffect(() => {
setTimeout(() => {
console.log(id, '我是demo5中的id值')
}, 100)
}, [])
return (
<Card title="demo5">
我是demo5 {id}
</Card>
)
}
通常我们会有上面的这种需求,id是由接口动态返回的,demo5作为子组件他也要使用id请求一次其他的接口,这时,实际上我们依赖了动态的id,但是在子组件中这么写是无法获得最新的id值,因为子组件在id值发生变化的时候,就已经渲染完毕了。
针对这种情况,我们有两种解决方案
1、useEffect第二个参数的数组中加上id依赖,并做空保护。
useEffect(() => {
if (id) {
setTimeout(() => {
console.log(id, '我是demo5中的id值')
}, 100)
}
}, [id])
2、当id值发生变化后,再渲染子组件。
但是针对 诸如 wiki 这种 文章组件不会销毁的情况,则使用hooks的优势就体现出来了。
useMemo的实践
| 
|_
import React, { useState } from 'react'
import { Button,Card } from 'antd';
export default () => {
const [count, setCount] = useState(1);
const [val, setValue] = useState('朱育仪');
function add() {
console.log('我又执行了');
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}
return (
<Card title="demo6">
<h4>{count}---{add()}</h4>
{val}
<div>
<Button onClick={() => setCount(count + 1)}>+1</Button>
<input value={val} onChange={event => setValue(event.target.value)} />
</div>
</Card>
);
}
运行上面的demo6,我们会发现,无论我们点击Button或者在input添加值,add函数都会执行,然而add函数与val并无关系,这就造成了性能的损耗。所以针对这种情况我们就需要使用 useMemo这个 hook。
export default () => {
const [count, setCount] = useState(1);
const [val, setValue] = useState('朱育仪');
const add = useMemo(() => {
console.log('我只有count变了,我才执行');
let sum = 0;
for (let i = 0; i < count * 10; i++) {
sum += i;
}
return sum;
}, [count])
return (
<Card>
<h4>{count}-{add}</h4>
{val}
<div>
<Button onClick={() => setCount(count + 1)}>+c1</Button>
<input value={val} onChange={e => setValue(e.target.value)} />
</div>
</Card>
);
}
此时只有当我们点击 Button 时再触发 add 函数,节省了性能的消耗。
这里有一张题库项目中的实践图
这里将列表筛选项进行了缓存,减少了不必要的渲染,从而提高性能。(原先的class组件其实没有考虑到这种问题)
在过去的class组件中,我们通过 shouldComponentUpdate 判断当前属性和状态是否和上一次的相同,来避免组件不必要的更新。其中的比较是对于本组件的所有属性和状态而言的,无法根据 shouldComponentUpdate 的返回值来使该组件一部分 elements 更新,另一部分不更新。(公司所有的进件系统筛选项都有这个问题)
而函数组件中的 useMemo 其实就可以代替这一部分工作。
强大的自定义hooks
import { useEffect, useState, } from 'react'
export function useOnline() {
const [online, setOnline] = useState(false);
useEffect(() => {
let timer = setInterval(() => {
setOnline(c => {
return !c
})
}, 3000)
return () => {
clearInterval(timer)
}
}, [])
return online
}
上面的hooks 比较特殊,它定义了一个 online状态,每三秒变化一次true false 来模拟用户在线或者离线。我们可以直接返回 该状态。 如何使用呢?
export default () => {
const online = useOnline();
console.log(online)
return (
<Card title="demo8">
<div>
{online ? '我在线' : '我离线'}
</div>
</Card>
);
}
该示例中直接调用 const online = useOnline(); ,即可获取到online的响应数据。
这就是自定义hooks的强大之处,聪明的开发者们也正是因为自定义hooks的存在,开发出了各种各样好用的hooks
关于父组件如何调取子组件方法
使用useRef useImperativeHandle forwardRef配合使用达到class组件的效果
interface useRefTypes {
getTopicList: () => void
}
const topicRef = useRef<useRefTypes>(null);
...
const topicDom = (
<div className={styles.describeContainer}>
<Topic
dispatch={dispatch}
subjectId={id}
loading={loading}
userId={_id}
ref={topicRef}
/>
</div>
)
...
// 提交并发布评论区
const submitAndTopic = () => {
if (!submitValue) {
notification.warning({
message: '提示',
description: '请输入你的答案'
})
return
}
confirm({
title: '提示',
content: '是否将答案同步到评论区?',
okText: '确定',
cancelText: '取消',
onOk: () => {
submit();
dispatch({
type: 'subjectDetails/addTopic',
payload: {
subjectId: id,
content: submitValue,
parentTopicId: 'topTopic'
},
callback: () => {
if (topicRef && topicRef.current) {
if (topicRef.current.getTopicList) {
topicRef.current.getTopicList();
}
}
}
})
}
})
}
在父组件中使用usRef(上方代码)
在子组件中使用useImperativeHandle forwardRef(下方代码)
// 将方法包裹在forwardRef中
const Topic: React.FC<propsType> = forwardRef((props, ref) => {
...
useImperativeHandle(ref, () => ({
getTopicList
}));
}
将方法包裹在 forwardRef 中,内部调用 useImperativeHandle (useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用。)
问题
1、Hooks 会因为在渲染时创建函数而变慢吗?
不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。 除此之外,可以认为 Hooks 的设计在某些方面更加高效:
Hooks避免了class需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。- 符合语言习惯的代码在使用
Hooks时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。
我们将 只会在useEffect中执行的代码放置于useEffect回调函数内部,这样只有依赖值发生变化的时候才会执行useEffect内部函数声明。
2、只在最顶层使用 Hook?
是的没错!!!
不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。
3、useState在接口的回调函数中表现出同步特性,即每次setState触发了多次render
如何解决这个问题呢,使用 react 提供的 unstable_batchedUpdates 方法,这样两个不相关的setState就不会触发两次render,而变为一次render。
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
dispatch({
type:'xxxx',
callback:()=>{
batchedUpdates(()=>{
// 两个不一样的state
setIsMeThumbsUpState(flag => !flag);
setThumbsUpCount(1);
})
}
})
参考文档
react官方中文网 juejin.cn/post/684490… blog.csdn.net/qq_44753552… useCallback useEffect和useLayoutEffect的区别 addeventlistener事件参数_函数式编程看React Hooks(二)事件绑定副作用深度剖析 为什么顺序调用对 React Hooks 很重要?
其他
格外强调在
useEffect中编写事件监听器时,请时刻注意事件监听器回调函数中的state值。若 useEffect 不添加依赖项则state始终为初始值