React中的闭包陷阱以及使用useRef姿势

简介: 前言本文是昨天的续篇,昨天发了一篇闭包陷阱的文章,然后有后台的童鞋问我为什么0会无限循环,这里科普一下一个基础知识==!:setTimeout与setInterval有一个重要区别:

前言

本文是昨天的续篇,昨天发了一篇闭包陷阱的文章,然后有后台的童鞋问我为什么0会无限循环,这里科普一下一个基础知识==!:

setTimeout与setInterval有一个重要区别:

setTimeout 只执行一次,setInterval 是每间隔给定的时间周期性执行(也就是说如果不调用函数停止执行,比如clearInterval,它会无限循环下去)。

这里还有一个例子:

const App = () => {
  const [value, setValue] = useState(1)
  const show = () => {
    setTimeout(() => {
      alert(value)
    }, 2000);
  }
  return (
    <div>
      <p>this is App</p>
      <div>value: {value}</div>
      <button onClick={() => setValue(value + 1)}>add</button>
      <br/>
      <button onClick={show}>alert</button>
    </div>
  )
}

在上面的函数式组件中,我们点击 alert 按钮后会在 2s 后弹出 value 的值,我们在这 2s 的时间内可以继续点击 add 按钮增加 value 的值。

40.png

上图是我们操作的结果。我们发现弹出的值和当前页面显示的值不相同。换句话说:show 方法内的 value 和点击动作触发那一刻的 value 相同,value 的后续变化不会对 show 方法内的 value 造成影响。这种现象被称为“闭包陷阱”(又称为stale-closure,由名字就可以知道:传递给setTimeout的回调引用了闭包捕获的value的过期值):函数式组件每次render 都会生产一个新的 show 函数,这个新的 show 函数会产生一个在当前这个阶段 value 值的闭包。

再看一下前面的代码:

exportdefaultfunction App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setInterval(() => {
      setCount(count + 1);
    }, 1500);
  }, []);
  useEffect(() => {
    setInterval(() => {
      console.log(count);
    }, 1500);
  }, []);
  return<div>Hello world</div>;
}


虽然上面第一个useEffect中一直在 加 1,但是第二个useEffect中的count的值其实一直是count的初始值,这和前面一个例子中value 的后续变化不会对 show 方法内的 value 造成影响的逻辑是一样的;

再次小结一下React中存在的闭包陷阱:

一般是指useEffect/useMemo/useCallback等 hook 中用到了某个 state,但是没有把它加到 deps 数组里,导致 state 变了,但是执行的函数依然引用着之前的 state。

useRef的引入

在引入useRef之前,先看看它的基本用法:

import { useRef} from"react";
import React from"react";
exportdefaultfunction App() {
  const ref1 = useRef();
  const ref2 = useRef(2021);
  console.log("if rendered, I am showing");
  console.log(ref1, ref2);
  return (
    <div>
      <h2>{ref1.current}</h2>
      <h2>{ref2.current}</h2>
    </div>
  );
}

41.png

由此可以知道:

useRef返回一个具有current属性的对象;如果将参数initialValue传递给useRef(initialValue),则此值将存储在current中;

useRef的常见用例

实例1

例1-1:

import { useRef } from"react";
import"./styles.css";
const App = () => {
  const countRef = useRef(0);
  console.log("render");
  return (
    <div className="App">
      <h2>count: {countRef.current}</h2>
      <button
        onClick={() => {
          countRef.current = countRef.current + 1;
          console.log(countRef.current);
        }}
      >
        increase count
      </button>
    </div>
  );
};

不断点击按钮:

42.png

我们的目标是定义一个名为countRef的ref,用0初始化该值,并在每次单击按钮时该计数器变量会增加。呈现的计数值应该更新

令人惊讶的是:页面上的计数值没有更新,但是:控制台的输出证明了当前属性是保存了正确的更新的;

同时,自从初始渲染之后,组件就没有重新渲染了;也就是说useRef不会触发重新渲染

那么useRef应该怎么用?

其实它与其他触发重新渲染的 Hook 结合起来很方便,例如useState,useReducer和useContext

看个例子:

43.png


import { useState } from"react";
import React from"react";
exportdefaultfunction App() {
  const [value, setValue] = useState("");
  console.log("I am rendering");
  const handleInputChange = (e) => {
    setValue(e.target.value);
  };
  return (
    <div className="App">
      <input value={value} onChange={handleInputChange} />
    </div>
  );
}

下面使用useRef重写一下:

import { useRef, useState } from"react";
import React from"react";
exportdefaultfunction App() {
  const [value, setValue] = useState("");
  const valueRef = useRef();
  console.log("render");
  const handleClick = () => {
    console.log(valueRef);
    setValue(valueRef.current.value);
  };
  return (
    <div className="App">
      <h4>Value: {value}</h4>
      <input ref={valueRef} />
      <button onClick={handleClick}>click</button>
    </div>
  );
}

点击前:

44.png

点击后:

45.png

通过该ref属性,React 提供了对 React 组件或 HTML 元素的直接访问。控制台输出显示我们确实可以访问该input元素。ref存储在current属性中。

实例2

再看一个和useEffect结合的例子:

import { useRef, useState, useEffect } from"react";
import React from"react";
exportdefaultfunction App() {
  const inputRef = useRef();
  console.log("render");
  useEffect(() => {
    console.log("running in useEffect");
    inputRef.current.focus()//注意有无这句话刷新前后的区别
  }, []);
  return (
    <div className="App">
      <input ref={inputRef} placeholder="this is input" />
    </div>
  );
}

输入前:

46.png

如果没有inputRef.current.focus():

47.png

48.png

如果有inputRef.current.focus():
49.png50.png

通过ref可以直接访问DOM元素;

实例3

另外一个例子的场景是:

需要上一个渲染周期的状态值时也要运用到useRef

这个例子很重要(终于讲到重点了)

import { useRef, useState, useEffect } from"react";
import React from"react";
exportdefaultfunction App() {
  console.log("render");
  const [count, setCount] = useState(0);
  //获取上一个值(在上次渲染时传递给钩子的)
  const ref = useRef();
  // 存储ref中的current值
  useEffect(() => {
    console.log("useEffect");
    ref.current = count;
  }, [count]); // count变化时才会重新渲染
  return (
    <div className="App">
      <h1>
        Now: {count}, before: {ref.current}
      </h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

51.png

在初始渲染之后,呈现出初始状态,将状态变量count分配给reference.current。因为没有发生额外的事件,故呈现出来的值是未定义的。单击按钮会触发状态更新,由于调用了setCount。

由于state发生变化,重新渲染UI, before标签显示正确的值(此时为0)。渲染之后,再次执行useEffect中的回调。1被赋值给ref,以此类推...

需要注意的是: 所有的ref都需要在useEffect回调或内部处理函数中更新,达到在渲染过程中改变ref的目标,如果从其他地方(比如例1-1),可能会引入bug。useState同理。

闭包陷阱的终极解决方案

好了,回归正题:如何解决昨天的闭包陷阱问题,受实例3的启发,这里给出解决方案:

import { useRef, useState, useEffect, useLayoutEffect } from"react";
import React from"react";
exportdefaultfunction App() {
  const [count, setCount] = useState(0);
//useEffect 是异步执行的,而useLayoutEffect是同步执行的。
//用 useLayoutEffect 能保证在 useEffect 之前被调用
  useLayoutEffect(() => {
    setInterval(() => {
      setCount((count) => count + 1);
    }, 500);
  }, []);
  const log = () => {
    console.log(count);
  };
  const ref = useRef(log);
  useEffect(() => {
    ref.current = log;
  });
  useEffect(() => {
    setInterval(() => ref.current(), 500);
  }, []);
  return<div>hello world</div>;
}


39.png

总结

最好总结一下useState和useRef的区别:

  • 两者都在渲染周期和UI更新期间保存它们的数据,但只有useState Hook及其更新函数会导致重新渲染
  • useRef返回一个具有current保存实际值的属性的对象。相反,useState返回一个包含两个元素的数组:第一项构成状态,第二项表示状态更新函数
  • useRef的current属性是可以被赋值的;但useState的state不是,要改变state,只能通过setState来进行,不能通过赋值的方式
  • useRef可以直接访问React组件或DOM元素,而useState不可以

彩蛋:React中的坑还未完,后续会继续更新,敬请期待...

目录
相关文章
|
6月前
|
存储 前端开发 JavaScript
React闭包陷阱产生的原因是什么,如何解决
react闭包陷阱产生的原因是由于在React组件中使用了异步操作(如定时器、事件监听等)时,闭包会保留对旧状态的引用,导致更新后的状态无法正确地被获取或使用。
134 0
|
2月前
|
前端开发 JavaScript
React中函数式Hooks之useRef的使用
React中函数式Hooks的useRef用于获取DOM元素的引用,示例代码演示了其基本用法。
33 3
|
3月前
|
前端开发
React使用useRef ts 报错
【8月更文挑战第17天】
66 4
|
24天前
|
存储 缓存 JavaScript
深入理解 React-Hooks 之 useRef
【10月更文挑战第20天】总之,`useRef` 是 React-Hooks 中一个非常实用的工具,它为我们提供了一种灵活而强大的方式来处理组件中的各种数据和操作。通过深入理解和掌握 `useRef` 的原理和用法,我们能够更好地构建高效、稳定的 React 应用。
32 6
|
1月前
|
存储 前端开发 JavaScript
React useState 和 useRef 的区别
本文介绍了 React 中 `useState` 和 `useRef` 这两个重要 Hook 的区别和使用场景。`useState` 用于管理状态并在状态变化时重新渲染组件,适用于表单输入、显示/隐藏组件、动态样式等场景。`useRef` 则用于在渲染之间保持可变值而不触发重新渲染,适用于访问 DOM 元素、存储定时器 ID 等场景。文章还提供了具体的代码示例,帮助读者更好地理解和应用这两个 Hook。
40 0
|
2月前
|
存储 前端开发 JavaScript
react的useRef用什么作用
react的useRef用什么作用
49 1
|
3月前
|
前端开发 JavaScript API
|
4月前
|
前端开发 JavaScript 数据格式
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
93 1
|
4月前
|
前端开发
react18【系列实用教程】Hooks 闭包陷阱 (2024最新版)含useState 闭包陷阱,useEffect 闭包陷阱,useCallback 闭包陷阱
react18【系列实用教程】Hooks 闭包陷阱 (2024最新版)含useState 闭包陷阱,useEffect 闭包陷阱,useCallback 闭包陷阱
75 0
|
4月前
|
JavaScript
react18【系列实用教程】useRef —— 创建 ref 对象,获取 DOM (2024最新版)
react18【系列实用教程】useRef —— 创建 ref 对象,获取 DOM (2024最新版)
57 0