ahooks源码系列(三):LifeCycle、控制时机的 hook

函数式组件中的“生命周期”

我们知道,Function Component 不同于 Class Component,它并没有生命周期的概念,而是以状态的更改来驱动代码逻辑、UI渲染的机制

对于 Function Component 来说,状态的更改到页面渲染只需要三步:

  • propsstate 的输入更改
  • 执行与 props、state 相关的逻辑,在 useEffect、useLayoutEffect 中记录副作用
  • 输出到 UI

其中,特别是我们经常会在 useEffect 里面执行像数据请求、定时器记录等逻辑,将他们放在了类似于 Vue 中的 computed 生命周期函数的位置上。

事实上,虽然我们可以通过 useEffectuseLayoutEffect 去实现 Class Component 的生命周期函数。比如 useEffect 接受的 callback 将会在 DOM 更新完毕之后调用 callback,这就完成了 Class Component 的 Mounting 阶段;当我们在 useEffect 里面返回一个 clear Function 时,会在组件卸载时执行该函数清理副作用,这样就实现了 Class Component 的 UnMounting 阶段。

然而,我们回归到本质上来:Function Component 是以状态的更改来驱动逻辑并输出到 UI 的,我个人认为不能把 useEffect 这样的 hook 当作生命周期来使用。对此,ahooks 基于 useEffect、useLayoutEffect 做了一些封装,让我们更清除代码的执行时机,更符合语义化

LifeCycle 相关的 hook

useMount

useMount 只在组件初始化时执行,我们来看看它的源码

const useMount = (fn: () => void) => {
  useEffect(() => {
    fn?.();
  }, []);
};


export default useMount;

很简单,对 useEffect 做了个封装而已,useEffect 的依赖项为空数组

useUnMount

useUnMount组件卸载时执行,我们可以传入一个函数 fn,当组件卸载时会去执行这个 fn 函数清除副作用。我们来看看它的源码

const useUnmount = (fn: () => void) => {
  const fnRef = useLatest(fn);


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

export default useUnmount;

它通过 useLatest 记录最新的 fn,内部通过 useEffect 返回 clear function,当组件卸载时,就会执行 clear function 内部的 fnRef.current()来清除副作用

代码也挺简单的

useUnmountRef

useUnmountRef 用于判断当前的组件是否已经卸载

const useUnmountedRef = () => {
  const unmountedRef = useRef(false);
  useEffect(() => {
    unmountedRef.current = false;
    return () => {
      //组件卸载时,会记录 unmountRef.current = false;
      unmountedRef.current = true;
    };
  }, []);
  return unmountedRef;
};


export default useUnmountedRef;

它就是通过 useEffect 包装了一层,然后在返回的 clear function 中修改 unmountRef.current = true 表明当前组件已经卸载,然后返回 unmountRef

结合例子来看的话

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







const MyComponent = () => {
  const unmountedRef = useUnmountedRef();
  useEffect(() => {
    if (!unmountRef.current) {
      message.info('MyComponent is alive')
    } else {
      message.error('MyComponent is unMount')
    }
  }, []);



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




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

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

当我点击按钮时,state 为 false,不展示 MyComponent 组件,也就是卸载了组件,此时会弹出消息 MyComponent is Unmount

控制执行时机的 hook

useUpdateEffect、useUpdateLayoutEffect

useUpdateEffectuseUpdateLayoutEffect 和 useEffect、useLayoutEffect 用法一样,只不过他们会忽略首次执行,只在依赖改变时执行

useUpdateEffect、useUpdateLayoutEffect 源码会调用 createUpdateEffect 函数,我们来看看它的源码

// useUpdateEffect.js
import { useEffect } from 'react';
import { createUpdateEffect } from '../createUpdateEffect';




export default createUpdateEffect(useEffect);


// useUpdateLayoutEffect.js
import { useLayoutEffect } from 'react';
import { createUpdateEffect } from '../createUpdateEffect';

export default createUpdateEffect(useLayoutEffect);




// createUpdateEffect.js
type EffectHookType = typeof useEffect | typeof useLayoutEffect;


export const createUpdateEffect: (hook: EffectHookType) => EffectHookType =
  (hook) => (callback, deps) => {
    const isMounted = useRef(false);



    // for react-refresh
    hook(() => {
      return () => {
        isMounted.current = false;
      };
    }, []);

    hook(() => {
      if (!isMounted.current) {
        //忽略首次执行
        isMounted.current = true;
      } else {
        return callback();
      }
    }, deps);
  };

export default createUpdateEffect;

它的逻辑是,首先给 isMount.current 设置为 false,然后执行对应的 hook 函数(也就是 useEffect、useLayoutEffect),然后在首次执行时,判断 !isMount.current 就不执行 callback 回调函数,把 isMount.current 设置为 true,然后当依赖 deps 发生改变时,就可以去执行 callback 回调了。这样就实现了忽略首次执行,依赖改变时才执行的逻辑

useDeepCompareEffect、useDeepCompareLayoutEffect

同样,useDeepCompareEffectuseDeepCompareLayoutEffect 用法与 useEffect、useLayoutEffect 一致,但会通过 lodash isEqual 方法对 deps 进行比较看依赖有没有发生变化。

const depsEqual = (aDeps: DependencyList, bDeps: DependencyList = []) => {
  return isEqual(aDeps, bDeps);
};







type EffectHookType = typeof useEffect | typeof useLayoutEffect;
type CreateUpdateEffect = (hook: EffectHookType) => EffectHookType;


const useDeepCompareEffect =  CreateUpdateEffect = (hook) => (callback, deps) => {
  // 通过 useRef 保存上一次的依赖的值
  const ref = useRef<DependencyList>();
  const signalRef = useRef<number>(0);


  // 判断最新的依赖和旧的区别
  // 如果不相等或者 deps 为 undefined,则变更 signalRef.current,从而触发 useEffect 中的回调
  if (deps === undefined || !depsEqual(deps, ref.current)) {
    ref.current = deps;
    signalRef.current += 1;
  }



  hook(callback, [signalRef.current]);
};

它的思路也比较简单,首先用 ref.current 会记录旧的依赖,然后会通过 lodash 的 isEqual 方法深比较新旧依赖,如果依赖发生了改变,也就是不相等时,会将新的依赖记录在 ref.current 上,然后 signalRef.current++,这样 hook(也就是 useEffect)的依赖就发生了改变,重新执行 callback

同理 useDeepCompareLayoutEffect

useUpdate

useUpdate 会返回一个函数,调用该函数会强制组件重新渲染。我们来看看它的源码

import { useCallback, useState } from 'react';

const useUpdate = () => {
  const [, setState] = useState({});



  return useCallback(() => setState({}), []);
};


export default useUpdate;

很简单,就是当我们调用返回的函数时,useUpdate 内部会通过 setState 更改状态,然后就促使组件重新渲染

拿 ahooks 官网的举的例子来看:

import React from 'react';
import { useUpdate } from 'ahooks';






export default () => {
  const update = useUpdate();

  return (
    <>
      <div>Time: {Date.now()}</div>
      <button type="button" onClick={update} style={{ marginTop: 8 }}>
        update
      </button>
    </>
  );
};

每当点击按钮后,执行 update 回调,组件重新刷新,Date.now() 展示最新的当前时间

总结

函数式组件是通过 状态更改驱动逻辑输出到 UI 的特性,因此我们在使用 useEffect 这样的 hook 时,不要把他放在生命周期的位置上,而是始终将他看作是以依赖状态为准则的抽象逻辑。

ahooks 通过封装了以上的 hook,使得我们在编写代码的时候,更加清楚的知道代码的执行顺序执行时机,也更加加深我们对 React 函数组件的了解

使用这些 hook,可以让我们的代码更加具有可读性以及逻辑更加清晰

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYsKbTiu' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片