Skip to content

ahooks生命周期相关hook 源码解读

上期我们主要介绍了useMount源码和一些前置的知识点,在ahooks中生命周期相关hook还剩下useUnmountuseUnmountedRef

上期留下的问题

上期传送门,useMount的实现为什么不用useLayoutEffect,我个人总结还是同步和异步的区别,如果使用useLayoutEffect实现的话,如果useMount的fn中,如果存在重度计算等代码就会存在阻塞首屏渲染的风险。其次的话,如果在SSR的场景,React也是不支持useLayoutEffect的

为什么上期useUnmount的实现是有问题的

首先我们看下

tsx
const useUnmount = (fn: () => void) => {
  useEffect(
    () => () => {
      fn?.();// 就这?
    },
    [],
  );
};

可能暂时一眼还看不出问题所在,不如我们自己在demo中跑一下看看

jsx
import { useEffect, useState } from "react";

const useUnmount = (fn) => {
  useEffect(
    () => () => {
      fn?.(); // 就这?
    },
    [],
  );
};
const Child = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  useUnmount(() => {
    console.log("count:", count);
  });
  return <div>{count}</div>;
};

export default function Test() {
  const [flag, setFlag] = useState(false);

  return (
    <div>
      <button
        onClick={() => {
          setFlag(!flag);
        }}
      >
        点我
      </button>
      {!!flag ? <Child /> : null}
    </div>
  );
}

大家脑海里面可以想下组件卸载的时候log的count值是多少,界面上的count值是多少?

如果觉得log中count值是0的同学,恭喜你们get到了,这里就存在一个闭包的问题,如何解决呢?我们看ahooks的源码是如何解决的

useUnmount

tsx
import { useEffect } from 'react';
import useLatest from '../useLatest';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';

const useUnmount = (fn: () => void) => {
  if (isDev) {
    if (!isFunction(fn)) {
      console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);
    }
  }

  const fnRef = useLatest(fn);

  useEffect(
    () => () => {
      fnRef.current();
    },
    [],
  );
};

export default useUnmount;

这里使用了useLatest来保证得到最新值,至于useLatest又是怎么实现的,我们到对应的章节来讲,现在大家只要知道这样能解决刚才说的闭包问题就好了,我们改下代码,再试一下就能发现

image-20231206130223438

Wow!?变成1了。但是,你是不是发现界面上的值也一直是1,为什么没有累加上去呢?

思考一下。嗯。

对咯,这个Effect里面也是有闭包问题的,不妨你在定时器内部也log下count,是不是一直是0

具体原因就牵扯到useEffct的源码实现了,我们后面在讲,这里不展开了。

解决办法呢也是可以用下刚才提到的useLatest

jsx
 const ref = useLatest(count);
 
 useEffect(() => {
   const interval = setInterval(() => {
          console.log("count:", count);
          console.log("ref:", ref);
          setCount(ref.current + 1);
   }, 1000);
   return () => clearInterval(interval);
 }, []);

useUnmountRef

本篇最后我们再聊最后一个ahooks中跟生命周期有关的一个hook,useUnmountRef

作用就是给你一个标记,让你在使用的时候知道当前组件是否已经被卸载

useUnmountRef源码

tsx
import { useEffect, useRef } from 'react';

const useUnmountedRef = () => {
  const unmountedRef = useRef(false);
  useEffect(() => {
    unmountedRef.current = false;
    return () => {
      unmountedRef.current = true;
    };
  }, []);
  return unmountedRef;
};

export default useUnmountedRef;

这里牵扯到原生hook,useRef,不在赘述,简单理解就是通过useRef,分别在挂载和卸载阶段修改对应的值

demo如下

tsx
/**
 * title: Default usage
 * desc: unmountedRef.current means whether the component is unmounted
 *
 * title.zh-CN: 基础用法
 * desc.zh-CN: unmountedRef.current 代表组件是否已经卸载
 */

import { useBoolean, useUnmountedRef } from 'ahooks';
import { message } from 'antd';
import React, { useEffect } from 'react';

const MyComponent = () => {
  const unmountedRef = useUnmountedRef();
  useEffect(() => {
    setTimeout(() => {
      if (!unmountedRef.current) {
        message.info('component is alive');
      }
    }, 3000);
  }, []);

  return <p>Hello World!</p>;
};

export default () => {
  const [state, { toggle }] = useBoolean(true);

  return (
    <>
      <button type="button" onClick={toggle}>
        {state ? 'unmount' : 'mount'}
      </button>
      {state && <MyComponent />}
    </>
  );
};

好了,本期内容就到这里。

个人感觉一开始这3个hook的实现是相对比较容易的,但也有些细微的点值得大家去注意的。后面我们会讲下ahooks中State相关的hook及源码实现。

下期预告

我们将在下期,clone下ahooks的源码项目,进行源码目录,工程化,测试等分析和使用,敬请期待,更多的交流关注喵爸的小作坊