天天看点

你为什么需要 useMount 和 useUnmount?

作者:web前端开发

背景

使用 React 类组件时,React 有明显的生命周期方法,可以在确定的生命周期方法里做确定逻辑,自从 React Hooks 推出后,React 没有推出明确的生命周期方法,更多的是使用 useEffect 去模拟生命周期方法,比如 useMount、useUnmount 等等。

然而经过笔者本人长时间在项目中使用 React Hooks 后,发现使用 useEffect 模拟生命周期方法不当会导致一些新问题

React Hooks 的执行顺序

下面是一份来自社区的 React Hooks 的执行顺序图

你为什么需要 useMount 和 useUnmount?

原理分析

以一段简单的代码解释 React Hooks 的执行顺序

import { useEffect, useMemo, useState, useLayoutEffect } from "react";

const Count = () => {
  const [count, setCount] = useState(() => {
    console.log(1);
    return 0;
  });

  const double = useMemo(() => {
    console.log(2);
    return count * 2;
  }, [count]);

  const handleClick = () => setCount((c) => c + 1);

  useEffect(() => {
    console.log(4);
    return () => {
      console.log(6);
    };
  }, [count]);

  useLayoutEffect(() => {
    console.log(3);

    return () => {
      console.log(5);
    };
  }, [count]);

  return (
    <div>
      <p>
        {count}---{double}
      </p>
      <button onClick={handleClick}>click</button>
    </div>
  );
};

export default Count;

           

在浏览器中第一次运行该段代码,得到下面的结果

你为什么需要 useMount 和 useUnmount?

以 React update DOM and Refs 作为执行顺序的分界线,会先执行 useState、useMemo 、 useLayoutEffect 以及内部的变量或方法声明,后执行 useEffect

点击 click 按钮,得到了一份新结果如下

你为什么需要 useMount 和 useUnmount?

通过结果可以得到,先执行 useMemo,接着执行 useLayoutEffect 的 side effect cleanup,useEffect 的 side effect cleanup,等到 React update DOM and Refs 执行后,再执行 useLayoutEffect、useEffect。通过这个例子,基本上弄清楚 useEffect 的执行顺序,由此分析使用 useEffect 模拟生命周期方法不当会导致什么问题

  • 对于生命周期挂载方法,使用 useEffect 模拟 mount 如下
useEffect(() => {
  // do something
}, []);
           

只有当 useEffect 的 deps 参数是空数组时,该用法才等同于 mount 方法

  • 对于组件销毁的生命周期方法,使用 useEffect 模拟 unmount 如下
useEffect(() => {
  return () => {
    // do something
  }
}, []);
           

useMount 和 useUnmount

前面的问题根本原因是 useEffect 与 Lifecycle Methods 需要解耦。如果 useEffect 的 deps 参数不是空数组,那么当前的 useEffect 不等同 mount 方法,就会造成意外的结果

社区里提供专门的 ahooks 解决这个问题,这个第三方库中有两个重要方法,分别是 useMount 和 useUnmount

useMount 的实现如下

// useMount.ts

import { useEffect } from 'react'

const useMount = (fn: () => void) => {
  if (!isFunction(fn)) {
    console.error(`useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`)
  }

  useEffect(() => {
    fn?.()
  }, [])
}

export default useMount

           

useUnmount 的实现如下

// useUnMount.ts

import { useEffect, useRef } from 'react'

export default function useUnmount(fn: () => void): void {
  if (!isFunction(fn)) {
   console.error(`useUnmount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`)
  }

  const ref = useRef(fn)

  useEffect(
    (): (() => void) => (): void => {
      ref.current?.()
    },
    []
  )
}

function isFunction(fn: unknown): fn is Function {
  return typeof fn === 'function'
}
           

在项目中这样使用 useMount 和 useUnMount

const App = () => {
  useMount(() => {
    // ...
  })

  useUnMount(() => {
    // ...
  })

  return <></>
}
           

使用 useMount 和 useUnmount 就不用考虑传入的依赖,实现 useEffect 与 Lifecycle Methods 解耦