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中的坑还未完,后续会继续更新,敬请期待...

目录
相关文章
|
1月前
|
存储 前端开发 JavaScript
React闭包陷阱产生的原因是什么,如何解决
react闭包陷阱产生的原因是由于在React组件中使用了异步操作(如定时器、事件监听等)时,闭包会保留对旧状态的引用,导致更新后的状态无法正确地被获取或使用。
83 0
|
1月前
|
前端开发 JavaScript 测试技术
React Hooks之useState、useRef
React Hooks之useState、useRef
|
1月前
|
存储 前端开发 JavaScript
React Hooks的useState、useRef使用
React Hooks的useState、useRef使用
34 2
|
1月前
|
存储 前端开发 JavaScript
[React] useRef用法和特性
[React] useRef用法和特性
|
1月前
|
前端开发 JavaScript
React useRef 详细使用
React useRef 详细使用
40 0
|
1月前
|
前端开发
React的闭包陷阱问题和解决方案
React的闭包陷阱问题和解决方案
43 1
|
1月前
|
自然语言处理 前端开发 JavaScript
说说你对 React Hook的闭包陷阱的理解,有哪些解决方案?
说说你对 React Hook的闭包陷阱的理解,有哪些解决方案?
59 0
|
7月前
|
存储 前端开发 JavaScript
react闭包陷阱及解决方案
react闭包陷阱及解决方案
76 0
|
10月前
|
存储 前端开发 开发者
对 React Hook的闭包陷阱的理解,有哪些解决方案?
对 React Hook的闭包陷阱的理解,有哪些解决方案?
97 0
|
10月前
|
JavaScript 前端开发
React useRef 详细使用
React useRef 详细使用
112 0