【解读 ahooks 源码系列】Dev篇——useTrackedEffect 和 useWhyDidYouUpdate

前言

本文是 ahooks 源码(v3.7.4)系列的第六篇——Dev 篇,该篇主要是协助开发调优的 Hook,只有两个

往期文章:

本文主要解读 useTrackedEffectuseWhyDidYouUpdate 的源码实现

useTrackedEffect

追踪是哪个依赖变化触发了 useEffect 的执行。

官方文档

基本用法

查看每次 effect 执行时发生变化的依赖项

官方在线 Demo

import React, { useState } from 'react';
import { useTrackedEffect } from 'ahooks';

export default () => {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useTrackedEffect(
    (changes) => {
      console.log('Index of changed dependencies: ', changes);
    },
    [count, count2],
  );

  return (
    <div>
      <p>Please open the browser console to view the output!</p>
      <div>
        <p>Count: {count}</p>
        <button onClick={() => setCount((c) => c + 1)}>count + 1</button>
      </div>
      <div style={{ marginTop: 16 }}>
        <p>Count2: {count2}</p>
        <button onClick={() => setCount2((c) => c + 1)}>count + 1</button>
      </div>
    </div>
  );
};

核心实现

实现原理:通过 uesRef 记录上一次依赖的值,在当前执行的时候,判断当前依赖值和上次依赖值之间有无变化

  • changes:变化的依赖 index 数组
  • previousDeps:上一个依赖
  • currentDeps:当前依赖
useTrackedEffect(
  effect: (changes: [], previousDeps: [], currentDeps: []) => (void | (() => void | undefined)),
  deps?: deps,
)

源码实现

const useTrackedEffect = (effect: Effect, deps?: DependencyList) => {
  const previousDepsRef = useRef<DependencyList>(); // 记录上次依赖

  useEffect(() => {
    // 判断依赖前后的 changes
    const changes = diffTwoDeps(previousDepsRef.current, deps);
    const previousDeps = previousDepsRef.current; // 赋值上次依赖
    previousDepsRef.current = deps;
    return effect(changes, previousDeps, deps);
  }, deps);
};

diffTwoDeps 方法实现:

  1. 对前后两个 deps 依赖项列表使用 Object.is 进行严格相等性检查
  2. 如果定义了 deps1,则遍历 deps1 并将每个元素与来自 deps2 的对应索引元素进行比较(因为这个函数只在这个钩子中使用,所以假设两个 deps 列表的长度总是相同的)

    • 相等返回 -1
    • 不相等返回索引值
    • 过滤小于 0 的值(即校验结果相等的).filter((ele) => ele >= 0),最终只返回变化的数组索引值
const diffTwoDeps = (deps1?: DependencyList, deps2?: DependencyList) => {
  // 对前后两个 deps 依赖项列表使用 Object.is 进行严格相等性检查
  return deps1
    ? deps1
        .map((_ele, idx) => (!Object.is(deps1[idx], deps2?.[idx]) ? idx : -1))
        .filter((ele) => ele >= 0) // 过滤相等值
    : deps2
    ? deps2.map((_ele, idx) => idx)
    : [];
};

完整源码

useWhyDidYouUpdate

帮助开发者排查是那个属性改变导致了组件的 rerender。

官方文档

基本用法

官方在线 Demo

打开控制台,可以看到改变的属性。

import { useWhyDidYouUpdate } from 'ahooks';
import React, { useState } from 'react';

const Demo: React.FC<{ count: number }> = (props) => {
  const [randomNum, setRandomNum] = useState(Math.random());

  useWhyDidYouUpdate('useWhyDidYouUpdateComponent', { ...props, randomNum });

  return (
    <div>
      <div>
        <span>number: {props.count}</span>
      </div>
      <div>
        randomNum: {randomNum}
        <button onClick={() => setRandomNum(Math.random)} style={{ marginLeft: 8 }}>
          🎲
        </button>
      </div>
    </div>
  );
};

export default () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Demo count={count} />
      <div>
        <button onClick={() => setCount((prevCount) => prevCount - 1)}>count -</button>
        <button onClick={() => setCount((prevCount) => prevCount + 1)} style={{ marginLeft: 8 }}>
          count +
        </button>
      </div>
      <p style={{ marginTop: 8 }}>Please open the browser console to view the output!</p>
    </div>
  );
};

使用场景

  • 检查哪些 props 发生改变
  • 协助找出无效渲染:useWhyDidYouUpdate 会告诉我们监听数据中所有变化的数据,不管它是不是无效的更新,但还需要我们自己来区分识别哪些是无效更新的属性,从而进行优化。

实现思路

  1. 使用 useRef 声明 prevProps 变量(确保拿到最新值),用来保存上一次的 props
  2. 每次 useEffect 更新都置空 changedProps 对象,并将新旧 props 对象的属性提取出来,生成属性数组 allKeys
  3. 遍历 allKeys 数组,去对比新旧属性值。如果不同,则记录到 changedProps 对象中
  4. 如果 changedProps 有长度,则输出改变的内容,并更新 prevProps

核心实现

实现原理:通过 useEffect 拿到上一次 props 值 和当前 props 值 进行遍历比较,如果值发送改变则输出

// componentName:观测组件的名称
// props:需要观测的数据(当前组件 state 或者传入的 props 等可能导致 rerender 的数据)
export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
  const prevProps = useRef<IProps>({});

  useEffect(() => {
    if (prevProps.current) {
      // 获取所有的需要观测的数据
      const allKeys = Object.keys({ ...prevProps.current, ...props });
      const changedProps: IProps = {}; // 发生改变的属性值

      allKeys.forEach((key) => {
        // 通过 Object.is 判断是否进行更新
        if (!Object.is(prevProps.current[key], props[key])) {
          changedProps[key] = {
            from: prevProps.current[key],
            to: props[key],
          };
        }
      });

      // 遍历改变的属性,有值则输出日志
      if (Object.keys(changedProps).length) {
        console.log('[why-did-you-update]', componentName, changedProps);
      }
    }

    prevProps.current = props;
  });
}

完整源码

520 声望
239 粉丝
0 条评论
推荐阅读
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs40阅读 4.8k评论 5

封面图
ESlint + Stylelint + VSCode自动格式化代码(2023)
安装插件 ESLint,然后 File -&gt; Preference-&gt; Settings(如果装了中文插件包应该是 文件 -&gt; 选项 -&gt; 设置),搜索 eslint,点击 Edit in setting.json

谭光志34阅读 20.7k评论 9

涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco22阅读 2.2k评论 3

你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan23阅读 1.6k评论 1

封面图
在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 2k

封面图
【代码鉴赏】简单优雅的JavaScript代码片段(一):异步控制
Promise.race不满足需求,因为如果有一个Promise率先reject,结果Promise也会立即reject;Promise.all也不满足需求,因为它会等待所有Promise,并且要求所有Promise都成功resolve。

csRyan26阅读 3.3k评论 1

「彻底弄懂」this全面解析
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在 哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在 函数执行的过程中用到...

wuwhs17阅读 2.4k

封面图
520 声望
239 粉丝
宣传栏