Web 开发
2022-05-08

闭包及闭包在 React 中问题

次点击
16分钟阅读

🤺 闭包就是可以读取其他函数内部变量的函数 — 阮一峰博客

闭包常见的考题

输出的结果

for (var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 一秒后,输出了 10 个 10

出现这种原因就是用 var 声明的变量,没有块级作用域,所以限定不了 var 声明变量的访问范围

问:如果要输出 0 - 9 那该怎么改呢

1.简单方法:将 var 改成 let

// 简单。常用改法。将 var 改成 let
for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

2.就是使用 IIFE

// 使用 iife 实现块级作用域
// _i 保存着 iife 对 i 的引用
// 其实也就是闭包
for (var i = 0; i < 10; i++) {
  ((_i) => {
    setTimeout(() => {
      console.log(_i);
    }, 1000);
  })(i);
}

3.当然也可以使用 setTimeout 本身解决。接受第三个参数

for (var i = 0; i < 10; i++) {
  setTimeout(
    (_i) => {
      console.log(_i);
    },
    1000,
    i
  );
}

React 与 闭包的问题

useState 的闭包问题

import React, { useState } from 'react';
function FunComponent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log('count', count);
    }, 1000);
  };
  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={handleClick}>点击</button>
    </div>
  );
}
export default FunComponent;

问:快速点击三次后,这段代码会输入什么?

正常看好像应该是输出 三个 3,但是实际上是 0,1,2,这是什么原因导致的呢?

其实这也是和闭包有关系的,setTimeout 回调函数就是一个闭包,里面的 count 的值来自于函数组件 FunComponent statecount ;而且这个 count 在闭包中是不会被销毁的

所以具体的调用过程如下:

  • 第一次点击,第一个定时器回调函数获取的 count 等于 0,一秒之后就会打印 0
  • 组件重新渲染,count 变为 1
  • 第二次点击,第二个定时器回调函数获取的 count 此时等于 1,一秒之后就会打印 1
  • 之后的以此类推

那么为了让打印出 3,3,3,该怎么修改呢?

使用 useRef 来解决

+ import React, { useState, useRef } from 'react';
function FunComponent() {
  const [count, setCount] = useState(0);
+  const countRef = useRef(count);
  const handleClick = () => {
    setCount(count + 1);
+    countRef.current = count + 1;
    setTimeout(() => {
      console.log('count', count);
+      console.log('countRef.current', countRef.current);
    }, 1000);
  };
  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={handleClick}>点击</button>
    </div>
  );
}
export default FunComponent;

image.png

通过添加上面几行代码就可以实现打印 3,3,3 的功能。

但是这样每次都要写 countRef.current = count + 1 很不优雅,可以通过下面的方式来进行优化

使用 useEffect 来优化

import React, { useState, useRef, useEffect } from 'react';
function FunComponent() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  useEffect(() => {
    countRef.current = count;
  });
  const handleClick = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log('count', count);
      console.log('countRef.current', countRef.current);
    }, 1000);
  };
  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={handleClick}>点击</button>
    </div>
  );
}
export default FunComponent;

组件重新渲染后就会触发 useEffect 方法,将最新的值赋给 current 即可,无需再将 count + 1

但是还是可以接着优化,使用自定义 hook 来实现获取最新的 state

使用 自定义hook 获取最新的 state

import { useEffect, useRef } from 'react';
/**
 * 自定义Hook -- 获取最新的 state 的值
 * @param value
 */
export const useGetCurrentValue = (value: any) => {
  const ref = useRef(value);
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref;
};
import React, { useState } from 'react';
import { useGetCurrentValue } from '../../hook/useGetCurrentValue';
function FunComponent() {
  const [count, setCount] = useState(0);
  const countRef = useGetCurrentValue(count);
  const handleClick = () => {
    setCount(count + 1);
    setTimeout(() => {
      console.log('count', count);
      console.log('countRef.current', countRef.current);
    }, 1000);
  };
  return (
    <div>
      <p>你点击了 {count} 次</p>
      <button onClick={handleClick}>点击</button>
    </div>
  );
}
export default FunComponent;

问:如果自定义 hook return 的是 ref.current,为什么会有问题 ?

useEffect 的闭包问题

同样,在 useEffect 中也有这样的问题

import React, { useEffect, useState } from 'react';
function FunComponent() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1000);
  }, []);
  return (
    <div>
      <p>{count}</p>
    </div>
  );
}
export default FunComponent;

会发现,页面上的 count 到 1 之后就不变了,还以为是卡了。出现这种原因还是因为闭包。setInterval 的回调函数还是一个闭包,count 就是初始值 0,并且不会被销毁且一直存在,所以就相当于一直是 0 + 1,所以 count 一直是 1。

解决方法如下:

import React, { useEffect, useState } from 'react';
function FunComponent() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setCount((c) => c + 1);
    }, 1000);
  }, []);
  return (
    <div>
      <p>{count}</p>
    </div>
  );
}
export default FunComponent;

这样就可以获取到最新的 state

闭包的内容就到这里了。后面再讲讲 useRef 的原理 👋🏻