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

1. 常用的一个 hooks

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

1.1 useState: 状态钩子

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

  1. const [count, setCount] = useState(0);
  2. return (<>
  3. <p>{ count}</p>
  4. <button onClick = {
  5. () => setCount(count + 1)
  6. }> add 1 </button>
  7. </>
  8. );

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

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

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

  1. class Home extends React.Component {
  2. state = {
  3. name: 'wenzi',
  4. age: 20,
  5. score: 89
  6. };
  7. update() {
  8. this.setState({
  9. score: 98
  10. }); // 内部自动合并
  11. }
  12. }

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

  1. const [person, setPerson] = useState({
  2. name: 'wenzi',
  3. age: 20,
  4. score: 89
  5. });
  6. setPerson({
  7. ...person,
  8. {
  9. score: 98
  10. }
  11. }); // 先合并数据 { name: 'wenzi', age: 20, score: 98 }
  12. setPerson({
  13. score: 98
  14. }); // 仅传入要修改的字段,后name和age字段丢失

1.2 useEffect: 副作用钩子

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

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

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

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

  1. const Home = () => {
  2. const [count, setCount] = useState(0);
  3. const [nowtime, setNowtime] = useState(0);
  4. useEffect(() => {
  5. console.log('count', count);
  6. console.log('nowtime', nowtime);
  7. });
  8. return ( <>
  9. <p>count: {count} </p>
  10. <p>nowtime: {nowtime} </p>
  11. <button onClick = {() => setCount(count + 1)}> add 1 </button>
  12. <button onClick = {() => setNowtime(Date.now())} > set now time </button>
  13. </>);
  14. };

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

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

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

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

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

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

1.3 useMemo 和 useCallback

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

  1. const Home = () => {
  2. const [count, setCount] = useState(0);
  3. const [nowtime, setNowtime] = useState(0);
  4. const getSum = () => {
  5. const sum = ((1 + count) * count) / 2;
  6. return sum + ' , ' + Math.random(); // 这个random是为了看到区别
  7. };
  8. return ( <>
  9. <p> count: {count}< /p>
  10. <p> sum: {getSum()}</p>
  11. <p> nowtime: {nowtime}</p>
  12. <button onClick = {() => setCount(count + 1)} > add 1 </button>
  13. <button onClick = {() => setNowtime(Date.now())}> set now time </button>
  14. </>);
  15. };

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

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

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

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

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

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

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

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

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

2. 实现几个自定义的 hook

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

2.1 获取窗口变化的宽高

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

  1. const useWinResize = () => {
  2. const [size, setSize] = useState({
  3. width: document.documentElement.clientWidth,
  4. height: document.documentElement.clientHeight
  5. });
  6. const resize = useCallback(() => {
  7. setSize({
  8. width: document.documentElement.clientWidth,
  9. height: document.documentElement.clientHeight
  10. })
  11. }, [])
  12. useEffect(() => {
  13. window.addEventListener('resize', resize);
  14. return () => window.removeEventListener('resize', resize);
  15. }, []);
  16. return size;
  17. }

使用起来也非常方便:

  1. const Home = () => {
  2. const {width, height} = useWinResize();
  3. return <div>
  4. <p>width: {width}</p>
  5. <p>height: {height}</p>
  6. </div>;
  7. };

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

2.2 定时器 useInterval

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

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

  1. const useInterval = (callback, delay) => {
  2. useEffect(() => {
  3. if (delay !== null) {
  4. let id = setInterval(callback, delay);
  5. return () => clearInterval(id);
  6. }
  7. }, [delay]);
  8. };

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

  1. const Home = () => {
  2. const [count, setCount] = useState(0);
  3. useInterval(() => {
  4. console.log(count);
  5. setCount(count + 1);
  6. }, 500);
  7. return <div > {
  8. count
  9. } < /div>;
  10. };

可是这段运行后很奇怪,页面从 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 的值:

  1. const useInterval = (callback, delay) => {
  2. const saveCallback = useRef();
  3. useEffect(() => {
  4. // 每次渲染后,保存新的回调到我们的 ref 里
  5. saveCallback.current = callback;
  6. });
  7. useEffect(() => {
  8. function tick() {
  9. saveCallback.current();
  10. }
  11. if (delay !== null) {
  12. let id = setInterval(tick, delay);
  13. return () => clearInterval(id);
  14. }
  15. }, [delay]);
  16. };

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

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

  1. const [count, setCount] = useState(0);
  2. const [diff, setDiff] = useState(500);
  3. useInterval(() => {
  4. setCount(count + 1);
  5. }, diff);
  6. return ( <div>
  7. <p> count: {count} </p>
  8. <p> diff: {diff}ms </p>
  9. <p>
  10. <button onClick = {() => setDiff(diff - 50)}> 加快50ms </button>
  11. <button onClick = {() => setDiff(diff + 50)} > 减慢50ms </button>
  12. </p>
  13. </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. MySQL生僻函数

    0X01 字符串函数 STRCMP STRCMP(expr1,expr2) 若所有的字符串均相同,则返回STRCMP(),若根据当前分类次序,第一个参数小于第二个,则返回  -1,其它情况返回 1 . ...

  2. ssh隧道代理连接

    0x00 什么是SSH隧道 场景: 假设有两台主机: A主机为外网,B主机为内网通常来说外网主机A是无法直接连接到内网主机B的,这时如果要实现A主机通过ssh控制B主机,通常来说有两种方法: 1.端口 ...

  3. PHP krsort

    1.什么都不想说了,干么没事放那么悲伤的歌呢?回忆里我还是对代码懵懵懂懂的无知青年!也许不是青年,只是少年... <?php $arr = [ 1 => 'Zhangbiyu', 2 =& ...

  4. Javascript实现10种排序算法

    1.冒泡排序: 比较相邻的两个数,如果前一个数大于后一个数,就将这两个数换位置.每一次遍历都会将本次遍历最大的数冒泡到最后.为了将n个数排好序,需要n-1次遍历.如果某次遍历中,没有调整任何两个相邻的 ...

  5. java集合之linkedList链表基础

    LinkedList链表: List接口的链接列表实现.允许存储所有元素(包含null).使用频繁增删元素. linkedList方法: void addFirst(E e) 指定元素插入列表的开头 ...

  6. 小白学 Python(13):基础数据结构(字典)(下)

    人生苦短,我选Python 前文传送门 小白学 Python(1):开篇 小白学 Python(2):基础数据类型(上) 小白学 Python(3):基础数据类型(下) 小白学 Python(4):变 ...

  7. 设计模式(二十)Flyweight模式

    当使用new关键字生成类的实例时,需要给其分配足够的内存空间.当程序中需要大量对象时,如果都是用new关键字来分配内存,将会消耗大量内存空间.Flyweight模式就是尽量避免new出实例,而是通过尽 ...

  8. 设计模式C++描述----10.装饰(Decorator)模式

    一. 举例 我之前做过一个文件系统就叫 MyFileSys 吧,后来的话,客户想加入一些附加功能,比如压缩.加密.杀毒之类的操作,这些附加操作没有先后顺序,比如你可以先压缩再加密,也可以先杀毒再压缩, ...

  9. 七牛云图片存储---Java

    一.新建存储空间 到七牛云官网注册一个账号 新建一个存储空间 到个人中心获取秘钥 二.新建Java项目 1.pom.xml配置 <dependency> <groupId>co ...

  10. ubuntu 制作本地yum仓库

    ubuntu 制作本地yum仓库 笔者: 张首富 W X: y18163201(请备注) qq群:895291458 时间:2019-01-31晚 今天到某银行进行软件部署的时候,碰到所有电脑都不允许 ...