目 录CONTENT

文章目录

React Hooks 闭包陷阱

Hello!你好!我是村望~!
2024-12-24 / 0 评论 / 0 点赞 / 31 阅读 / 1,583 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

经典案例

import { useEffect, useState } from "react";

const HookClosure = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1000);
  }, []); // [] 表示组件 effect 只会执行一次
  return <div>{count}</div>;
};

export default HookClosure;

思考一下,页面会如何展示!

页面从 0 变成 1 然后不会有任何变化了!

这个就是很典型的 Hooks 闭包陷阱问题!

闭包解析

HookClosure 组件中,useEffect 回调函数是一个内部函数(被传递给 useEffect 来执行特定的副作用逻辑)

它嵌套在 HookClosure 这个函数组件内部。在 useEffect 的回调函数中,访问了在组件作用域内定义的 count 变量(通过 setCount(count + 1) 这样的语句)。

闭包形成的关键 —— 对外部变量的持久引用

image-20241220113106603

HookClosure 组件首次渲染时,useEffect 的回调函数会被创建并保存下来(因为传递给 useEffect 的第二个参数 [] 表示只在组件挂载时执行一次,所以这个回调函数后续会一直存在)。此时,它就形成了一个闭包,其中包含了 count 变量 第一次保存的时候 count 为 0。

三种解法

1. setData 回调函数的参数

import { useEffect, useState } from "react";

const HookClosure = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setCount((count) => count + 1);
    }, 1000);
  }, []); // [] 表示组件 effect 只会执行一次
  return <div>{count}</div>;
};

export default HookClosure;

setCount 接收一个回调函数时,React 会将回调函数中的 count 参数自动设置为当前状态的最新值

image-20241220113506037

虽然回调函数本身处于定时器这个相对独立的环境中(定时器每次触发时都会执行这个回调函数),但 React 通过传递最新状态值给回调函数中的 count 参数,使得每次更新 count 状态时,都是基于上一次更新后的正确值来进行加 1 操作。

iShot_2024-12-20_11.40.25

2. 更新依赖数组

当然也会有在外面使用的情况,不能每次都依靠setData 的回调参数拿最新的值啊!

比如这里,setDoubleCount 就用到了外面的 count,形成了闭包,但又不能把它挪到 setState 里去写:

import { useEffect, useState } from "react";

const HookClosure = () => {
  const [count, setCount] = useState(0);
  const [doubleCount, setDoubleCount] = useState(0);
  function double(num: number) {
    return num * 2;
  }
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(count + 1);
      setDoubleCount(double(count));
    }, 1000);
    return () => {
      clearInterval(timer);
    };
  }, []); // [] 表示组件 effect 只会执行一次
  return (
    <div>
      <div>{count}</div>
      <div>{doubleCount}</div>
    </div>
  );
};

export default HookClosure;

iShot_2024-12-20_14.05.33

这个时候就需要把 count 当做依赖传入数组,当依赖变动的时候,会重新执行 effect。

import { useEffect, useState } from "react";

const HookClosure = () => {
    const [count, setCount] = useState(0);
    const [doubleCount, setDoubleCount] = useState(0);
    function double(num: number) {
        return num * 2;
    }
    useEffect(() => {
        const timer = setInterval(() => {
            setCount(count + 1);
            setDoubleCount(double(count));
        }, 1000);
        return () => {
				// 这里要记得清除当前 effect 的定时器 否则下一次执行 effect 也会创建定时器
            clearInterval(timer); 
        };
    }, [count]); // [] 表示组件 effect 只会执行一次
    return (
        <div>
            <div>{count}</div>
            <div>{doubleCount}</div>
        </div>
    );
};

export default HookClosure;

iShot_2024-12-24_22.28.16

3. 使用 useRef

某些情况下可能希望定时器不要重置

比如下面的代码,可能页面进入的时候,定时器去按顺序去取渐变色!如果添加了 count 依赖,每次定时器重新执行,定时器内部的顺序 startIdx,endIdx都会重置,背景色就不会按照顺序持续变化,而是一直展示第一种。【强行使用的 demo🤣】

import { useEffect, useState } from "react";
const colors = ['red', 'green', 'blue'];
const HookClosure = () => {
    const [count, setCount] = useState(0);
    const [doubleCount, setDoubleCount] = useState(0);

    function double(num: number) {
        return num * 2;
    }
    useEffect(() => {
        let startIdx = 0
        let endIdx = 0
        const timer = setInterval(() => {
            startIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
            endIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
            setCount(count + 1);
            setDoubleCount(double(count));
            const body = document.body;
            body.style.background = `linear-gradient(to right, ${colors[startIdx]}, ${colors[endIdx]})`;
        }, 1000);
        return () => {
            clearInterval(timer);
        };
    }, [count]); // [] 表示组件 effect 只会执行一次
    return (
        <div>
            <div>{count}</div>
            <div>{doubleCount}</div>
        </div>
    );
};

export default HookClosure;

iShot_2024-12-24_22.52.19

如果把count 依赖去掉,背景色就正常了,但是 count 的逻辑又回到了之前的问题

import { useEffect, useState } from "react";
const colors = ['red', 'green', 'blue'];
const HookClosure = () => {
    const [count, setCount] = useState(0);
    const [doubleCount, setDoubleCount] = useState(0);

    function double(num: number) {
        return num * 2;
    }
    useEffect(() => {
        let startIdx = 0
        let endIdx = 0
        const timer = setInterval(() => {
            startIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
            endIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
            setCount(count + 1);
            setDoubleCount(double(count));
            const body = document.body;
            body.style.background = `linear-gradient(to right, ${colors[startIdx]}, ${colors[endIdx]})`;
        }, 1000);
        return () => {
            clearInterval(timer);
        };
    }, []); // [] 表示组件 effect 只会执行一次
    return (
        <div>
            <div>{count}</div>
            <div>{doubleCount}</div>
        </div>
    );
};

export default HookClosure;

iShot_2024-12-24_22.53.26

下面的代码可以完美解决!

通过 useRef 创建 ref 对象,保存执行的函数

每次执行setCount都要重新渲染 setIntervalCallBackRef 都会更新最新的 setIntervalCallBack

而 setIntervalCallBack 内部的 state 也都是最新的。

import { useEffect, useRef, useState } from "react";
const colors = ['red', 'green', 'blue'];
const HookClosure = () => {
    const [count, setCount] = useState(0);
    const [doubleCount, setDoubleCount] = useState(0);

    function double(num: number) {
        return num * 2;
    }
    const setIntervalCallBackRef = useRef(setIntervalCallBack)
    
    function setIntervalCallBack() {
        setCount(count + 1);
        setDoubleCount(double(count));
    }
    setIntervalCallBackRef.current = setIntervalCallBack;
    useEffect(() => {
        let startIdx = 0
        let endIdx = 0
        const timer = setInterval(() => {
            startIdx = startIdx + 2 > colors.length ? 0 : startIdx + 1
            endIdx = startIdx + 1
            const body = document.body;
            body.style.background = `linear-gradient(to right, ${colors[startIdx]}, ${colors[endIdx]})`;
            setIntervalCallBackRef.current()
        }, 1000);
        return () => {
            clearInterval(timer);
        };
    }, []); // [] 表示组件 effect 只会执行一次
    return (
        <div>
            <div>{count}</div>
            <div>{doubleCount}</div>
        </div>
    );
};

export default HookClosure;

iShot_2024-12-24_23.01.10

这样,定时器执行的函数里就始终引用的是最新的 count。

useEffect 只跑一次,保证 setIntervel 不会重置,是每秒执行一次。

执行的函数是从 ref.current 取的,这个函数每次渲染都会更新,引用着最新的 count。

补充对比

Js 经典闭包案例

function Count() {
  let counter = 0;
  return () => {
    counter += 1;
    return counter;
  };
}

let counter = Count()

console.log(counter())
console.log(counter())
console.log(counter())
console.log(counter())

为什么这个案例的 counter 能拿到最新的!

image-20241225094533479

但是 React 的却不行

image-20241225094612299

其实很简单,因为 Effect 只执行了一次!所以每次回调获取到的 count 都是 0 !

0

评论区