我们组的前端妹子在组内分享时谈到了 react 的钩子,趁此机会我也对我所理解的内容进行下总结,方便更多的同学了解。在 React 的 v16.8.0 版本里添加了 hooks 的这种新的 API,我们非常有必要了解下他的使用方法,并能够结合我们的业务编写几个自定义的 hooks。

1. 常用的一个 hooks

官方中提供了几个内置的钩子,我们简单了解下他们的用法。

1.1 useState: 状态钩子

需要更新页面状态的数据,我们可以把他放到 useState 的钩子里。例如点击按钮一下,数据加 1 的操作:

const [count, setCount] = useState(0);

return (<>
<p>{ count}</p>
<button onClick = {
() => setCount(count + 1)
}> add 1 </button>
</>
);

在 typescript 的体系中,count 的类型,默认就是当前初始值的类型,例如上面例子中的变量就是 number 类型。如果我们想自定义这个变量的类型,可以在 useState 后面进行定义:

const [count, setCount] = useState<number | null>(null); // 变量count为number类型或者null类型

同时,使用 useState 改变状态时,是整个把 state 替换掉的,因此,若状态变量是个 object 类型的数据,我只想修改其中的某个字段,在之前 class 组件内调用 setState 时,他内部会自动合并数据。

class Home extends React.Component {
state = {
name: 'wenzi',
age: 20,
score: 89
}; update() {
this.setState({
score: 98
}); // 内部自动合并
}
}

但在 function 组件内使用 useState 时,需要自己先合并数据,然后再调用方法,否则会造成字段的丢失。

const [person, setPerson] = useState({
name: 'wenzi',
age: 20,
score: 89
}); setPerson({
...person,
{
score: 98
}
}); // 先合并数据 { name: 'wenzi', age: 20, score: 98 }
setPerson({
score: 98
}); // 仅传入要修改的字段,后name和age字段丢失

1.2 useEffect: 副作用钩子

useEffect 可以看做是 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。

useEffect 钩子在组件初始化完毕时,一定会执行一次,在组件重新渲染的过程中,是否还要 update,还要看传入的第 2 个参数。

  1. 当只有回调函数这一个参数时,组件的每次更新,回调都会执行;
  2. 当有 2 个参数时,只有第 2 参数里的数据发生变化时,回调才执行;
  3. 只想在组件初始化完毕时只执行一次,第 2 个参数可以传入一个空的数组;

我们可以看下这个例子,无论点击 add按钮 还是 settime按钮 ,useEffect 的回调都会执行:

const Home = () => {
const [count, setCount] = useState(0);
const [nowtime, setNowtime] = useState(0); useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
}); return ( <>
<p>count: {count} </p>
<p>nowtime: {nowtime} </p>
<button onClick = {() => setCount(count + 1)}> add 1 </button>
<button onClick = {() => setNowtime(Date.now())} > set now time </button>
</>);
};

若改成下面的这样,回调仅会在 count 发生变化时才会在控制台输出,仅修改 nowtime 的值时没有输出:

useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime);
}, [count]);

useEffect 的回调函数还可以返回一个函数,这个函数在 effect 生命周期结束之前调用。为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除

基于上面的代码,我们稍微修改一下:

useEffect(() => {
console.log('count', count);
console.log('nowtime', nowtime); return () => console.log('effect callback will be cleared');
}, [count]);

基于这个机制,在一些存在添加绑定和取消绑定的案例上特别合适,例如监听页面的窗口大小变化、设置定时器、与后端的 websocket 接口建立连接和断开连接等,都可以预计 useEffect 进行二次的封装,形成自定义的 hook。关于自定义 hook,下面我们会讲到。

1.3 useMemo 和 useCallback

function 组件中定义的变量和方法,在组件重新渲染时,都会重新重新进行计算,例如下面的这个例子:

const Home = () => {
const [count, setCount] = useState(0);
const [nowtime, setNowtime] = useState(0); const getSum = () => {
const sum = ((1 + count) * count) / 2;
return sum + ' , ' + Math.random(); // 这个random是为了看到区别
}; return ( <>
<p> count: {count}< /p>
<p> sum: {getSum()}</p>
<p> nowtime: {nowtime}</p>
<button onClick = {() => setCount(count + 1)} > add 1 </button>
<button onClick = {() => setNowtime(Date.now())}> set now time </button>
</>);
};

这里有 2 个按钮,一个是 count+1,一个设置当前的时间戳, getSun() 方法是计算从 1 到 count 的和,我们每次点击 add 按钮后,sum 方法都会重新计算和。可是当我们点击 settime 按钮时,getSum 方法也会重新计算,这是没有必要的。

这里我们可以使用 useMemo 来修改下:

const sum = useMemo(() => ((1 + count) * count) / 2 + ' , ' + Math.random(), [count]);

<p> {sum} </p>;

修改后就可以看到,sum 的值只有在 count 发生变化的时候才重新计算,当点击 settime 按钮的时候,sum 并没有重新计算。这要得益于 useMemo 钩子的特性:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

useMemo 返回回调里 return 的值,而且 memoizedValue 它仅会在某个依赖项改变时才重新计算。这种优化有助于避免在每次渲染时都进行高开销的计算。如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

在上面的例子里,只有 count 变量发生变化时,才重新计算 sum,否则 sum 的值保持不变。

useCallback 与 useMemo 类型,只不过 useCallback 返回的是一个函数,例如:

const fn = useCallback(() => {
return ((1 + count) * count) / 2 + ' , ' + nowtime;
}, [count]);

2. 实现几个自定义的 hook

在官方文档里,实现了好友的在线与离线功能。这里我们自己也学着实现几个 hook。

2.1 获取窗口变化的宽高

我们通过监听resize事件来获取实时获取window窗口的宽高,对这个方法进行封装后可以在生命周期结束前能自动解绑resize事件:

const useWinResize = () => {
const [size, setSize] = useState({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
});
const resize = useCallback(() => {
setSize({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
})
}, [])
useEffect(() => {
window.addEventListener('resize', resize);
return () => window.removeEventListener('resize', resize);
}, []);
return size;
}

使用起来也非常方便:

const Home = () => {
const {width, height} = useWinResize(); return <div>
<p>width: {width}</p>
<p>height: {height}</p>
</div>;
};

点击链接useWinResize的使用可以查看demo演示。

2.2 定时器 useInterval

在前端中使用定时器时,通常要在组件生命周期结束前清除定时器,如果定时器的周期发生变化了,还要先清除定时器再重新按照新的周期来启动。这种最常用的场景就是九宫格抽奖,用户点击开始抽奖后,先缓慢启动,然后逐渐变快,接口返回中奖结果后,再开始减速,最后停止。

我们很容易想到用 useEffect 来实现这样的一个 hook:

const useInterval = (callback, delay) => {
useEffect(() => {
if (delay !== null) {
let id = setInterval(callback, delay);
return () => clearInterval(id);
}
}, [delay]);
};

我们把这段代码用到项目中试试:

const Home = () => {
const [count, setCount] = useState(0); useInterval(() => {
console.log(count);
setCount(count + 1);
}, 500); return <div > {
count
} < /div>;
};

可是这段运行后很奇怪,页面从 0 到 1 后,就再也不变了, console.log(count) 的输出表明代码并没有卡死,那么问题出在哪儿了?

React 组件中的 props 和 state 是可以改变的, React 会重渲染它们且「丢弃」任何关于上一次渲染的结果,它们之间不再有相关性。

useEffect() Hook 也「丢弃」上一次渲染结果,它会清除上一次 effect 再建立下一个 effect,下一个 effect 锁住新的 props 和 state,这也是我们第一次尝试简单示例可以正确工作的原因。

但 setInterval 不会「丢弃」。 它会一直引用老的 props 和 state 直到你把它换掉 —— 不重置时间你是无法做到的。

这里就要用到useRef这个 hook 了,我们把 callback 存储到 ref 中,当 callback 更新时去更新 ref.current 的值:

const useInterval = (callback, delay) => {
const saveCallback = useRef(); useEffect(() => {
// 每次渲染后,保存新的回调到我们的 ref 里
saveCallback.current = callback;
}); useEffect(() => {
function tick() {
saveCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};

当我们使用新的 useInterval 时,发现就可以自增了,点击查看样例useInterval 的简单使用

这里我们使用一个变量来控制增加的速度:

const [count, setCount] = useState(0);
const [diff, setDiff] = useState(500); useInterval(() => {
setCount(count + 1);
}, diff); return ( <div>
<p> count: {count} </p>
<p> diff: {diff}ms </p>
<p>
<button onClick = {() => setDiff(diff - 50)}> 加快50ms </button>
<button onClick = {() => setDiff(diff + 50)} > 减慢50ms </button>
</p>
</div>);

分别点击两个按钮,可以调整count增加的速度。

3. 总结

使用react hook可以做很多有意思的事情,这里我们也仅仅是举几个简单的例子,后续我们也会更加深入了解hook的原理。

▼我是来腾讯的小小前端开发工程师,

长按识别二维码关注,与大家共同学习、讨论▼

如何构建自己的 react hooks的更多相关文章

  1. React Hooks 深入系列 —— 设计模式

    本文是 React Hooks 深入系列的后续.此篇详细介绍了 Hooks 相对 class 的优势所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子 ...

  2. React Hooks 深入系列

    本文基于近段时间对 hooks 碎片化的理解作一次简单梳理, 个人博客.同时欢迎关注基于 hooks 构建的 UI 组件库 -- snake-design. 在 class 已经融入 React 生态 ...

  3. 使用 React hooks 转化 class 的一些思考

    Hooks 是 React 16.8 的新增特性.它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性. 使用 React hooks 转化 class 的一些思考 ...

  4. 蒲公英 · JELLY技术周刊 Vol.17: 90 行代码实现 React Hooks

    蒲公英 · JELLY技术周刊 Vol.17 React Hooks 相信大家都不陌生,自被设计出以来就备受好评,在很多场景中都有极高的使用率,其中原理更是很多大厂面试中的必考题,很多朋友都能够如数家 ...

  5. 蒲公英 &#183; JELLY技术周刊 Vol.21 -- 技术周刊 &#183; React Hooks vs Vue 3 + Composition API

    蒲公英 · JELLY技术周刊 Vol.21 选 React 还是 Vue,每个人心中都会有自己的答案,有很多理由去 pick 心水的框架,但是当我们扪心自问,我们真的可以公正的来评价这两者之间的差异 ...

  6. 通过 React Hooks 声明式地使用 setInterval

    本文由云+社区发表 作者:Dan Abramov 接触 React Hooks 一定时间的你,也许会碰到一个神奇的问题: setInterval 用起来没你想的简单. Ryan Florence 在他 ...

  7. 初探React Hooks & SSR改造

    Hooks React v16.8 发布了 Hooks,其主要是解决跨组件.组件复用的状态管理问题. 在 class 中组件的状态封装在对象中,然后通过单向数据流来组织组件间的状态交互.这种模式下,跨 ...

  8. React hooks实践

    前言 最近要对旧的项目进行重构,统一使用全新的react技术栈.同时,我们也决定尝试使用React hooks来进行开发,但是,由于React hooks崇尚的是使用(也只能使用)function c ...

  9. 如何使用TDD和React Testing Library构建健壮的React应用程序

    如何使用TDD和React Testing Library构建健壮的React应用程序 当我开始学习React时,我努力的一件事就是以一种既有用又直观的方式来测试我的web应用程序. 每次我想测试它时 ...

随机推荐

  1. sql 外键 on update cascade 和 on delete cascade 作用区别?

    这是数据库外键定义的一个可选项,用来设置当主键表中的被参考列的数据发生变化时,外键表中响应字段的变换规则的.update 则是主键表中被参考字段的值更新,delete是指在主键表中删除一条记录:on ...

  2. vc++源码免杀特殊技巧

    一.Debug 和 Release 编译方式的区别: Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序.Release 称为发布版本,它往往是进行了各种优化,使得程序 ...

  3. 网站是HTTP?10分钟变成HTTPS!域名免费添加配置SSL证书,变成https//环境

    对于小程序request请求需要https域名.navigator.geolocation定位也需要在https环境下才可以生效等问题: 前端开发越来越需要https环境来来测试一下API接口和各类问 ...

  4. 在idea中添加try/catch的快捷键

    在idea中添加try/catch的快捷键ctrl+alt+t选中想被try/catch包围的语句,同时按下ctrl+alt+t,出现下图

  5. 百万年薪python之路 -- 面向对象之:类空间问题以及类之间的关系

    面向对象之:类空间问题以及类之间的关系 1.从空间角度研究类 1.何处添加对象属性 class A: def __init__(self,name): self.name = name def fun ...

  6. 深copy

    更好的对一个对象进行复制 using System; using System.Collections.Generic; using System.Linq; using System.Text; u ...

  7. C++沉思录笔记 —— 序幕

    #include <stdio.h>   class Trace{public: void print(const char* s) { printf("%s\n", ...

  8. Kafka 权威指南阅读笔记(第三章,第四章)

    Kafka 第三章,第四章阅读笔记 Kafka 发送消息有三种方式:不关心结果的,同步方式,异步方式. Kafka 的异常主要有两类:一种是可重试异常,一种是无需重试异常. 生产者的配置: acks ...

  9. OptimalSolution(5)--数组和矩阵问题(2)2

    一.找到无序数组中最小的k个数 二.在数组中找到出现次数大于N/K的数 三.最长的可整合子数组的长度 四.不重复打印排序数组中相加和为给定值的所有二元组和三元组 五.未排序正数数组中累加和为给定值的最 ...

  10. Alpha阶段--第七周Scrum Meeting

    任务内容 本次会议为第六周的Scrum Meeting会议 召开时间为周日下午5点,在潮音餐厅召开,召开时间约为30分钟,对已经完成项目的总结和对今后项目设计的展望 队员 任务 张孟宇 “我的”界面代 ...