zoukankan      html  css  js  c++  java
  • React Hook的注意事项--useState

    useState

    1.异步操作导致更新旧的数据状态的问题

    import React, { useState } from 'react'
    export default ()=>{
      const [num, setNum] = useState(0);
      const handleClick = ()=>{
        setNum(num+1);
        setTimeout(() => {
          num
        }, 1000);
      }
      return <div>
        <h1 onClick={handleClick}>{num}</h1>
      </div>
    }
    

    setTimeOut实现了异步的效果,原有代码其实是一个异步请求,但是原理是一样的。

    定义了一个handClick 的回调函数,在函数内部,更新了num,然后设置了一个定时器,一秒之后访问num.

    当第一次点击 h1 元素的时候,将num 的值加一,并为 1 , 并将状态更新。一秒后,执行setTimeOut 回调时,num的值仍然是 0。

    let a = 1; 
    const handleclick = ()=>{
    a = 123;
    setTimeout(() =>{
    a;//123
    }, 2000);
    document.onclicktshpdlsghiethieove
    

    在JS中,当我们声明一个变量的时候,会为其自动分配一个内存空间,当我们的程序中不再需要这个变量的时候,内存空间会被回收,即我们常说的垃圾回收。

    从这个角度来看,我们分析一下以上两端代码最大的区别在哪里:

    首先看一下第一段,我们借助React Hooks 声明了两个变量,num 以及 setNum,而后我们在回调中,调用了setNum,将num+1 并更新到状态中。这一方法调用会触发React组件的更新,组件内部代码就会被重新执行一次,包括第一行代码对 num 以及 setNum的变量声明,也就是说,现在 num 和组件首次渲染时候的 num 分别对应了不通的内存空间,是两个完全不同的变量。

    上面我们提到说,在JS的内存管理中,当变量不再需要的时候,会被销毁,内存空间被回收。回过头来看下我们的代码,在异步的回调中,我们访问了num, 所以这个num,也就是第一次声明的,值为 0 的num会常驻内从中,直到在一秒后执行回调的之后,这个变量才会被销毁,所以我们访问的这个变量的值,也就是 0 了。

    至于第二段代码,其实就很明显了,变量a始终是变量a,并没有被再次声明。(闭包)

    import React, { useState, useRef } from 'react'
    export default ()=>{
      const [num, setNum] = useState(0);
      const numRef = useRef(num);
      numRef.current  = num;
      const handleClick = ()=>{
        setNum(num+1);
        setTimeout(() => {
          console.log(numRef.current);
        }, 1000);
      }
      return <div>
        <h1 onClick={handleClick}>{num}</h1>
      </div>
    }
    

    至于为什么用 useRef 就可以解决问题呢? 在官方文档中,已经讲的很清楚了,简单总结一下就是:

    uesRef 返回的对象将在组件的整个生命周期内保持。
    ref 不仅仅用于访问DOM节点,他其实是一个通用容器,其 current 属性是可变的,可以保存任何值,类似于类上的实例属性。

    function App () {
    const [ count, setCount ] = useState(0)
    return (
    点击次数: { count }
    { setCount(count + 1)}}>点我
    )
    }
    

    2函数式更新对比普通更新

    如果需要使用前一时刻的 state(状态) 计算新 state(状态) ,则可以将 函数 传递给 setState 。该函数将接收先前 state 的值,并返回更新的 state

    那么setCount(newCount)setCount(preCount => newCount)有什么区别呢,我们写个例子来看下:

    function Counter() {
      const [count, setCount] = useState(0);
      function add() {
        setTimeout(() => {
          setCount(count + 1);
        }, 3000);
      }
      function preAdd(){
        setTimeout(() => {
          // 根据前一时刻的 count 设置新的 count
          setCount(count => count + 1);
        }, 3000);
      }
      // 监听 count 变化
      useEffect(() => {
        console.log(count)
      }, [count])
      return (
        <>
          Count: {count}
          <button onClick={add}>add</button>
          <button onClick={preAdd}>preAdd</button>
        </>
      );
    }
    

    我们首先快速点击 add 按钮三次,三秒后 count 变为 1;然后快速点击 preAdd 三下,三秒后依次出现了 2、3、4。

    为什么setCount(count + 1)好像只执行了一次呢,因为每次更新都是独立的闭包,当点击更新状态的时候,函数组件都会重新被调用。 快速点击时,当前 count 为 0,即每次点击传入的值都是相同的,那么得到的结果也是相同的,最后 count 变为 1 后不再变化。

    为什么setCount(count => count + 1)好像能执行三次呢,因为当传入一个函数时,回调函数将接收当前的 state,并返回一个更新后的值。 三秒后,第一次setCount获取到最新的 count 为 1,然后执行函数将 count 变为 2,接着第二次获取到当前 count 为 2,执行函数将 count 变为了 3。每次获取到的最新 count 不一样,最后结果自然也不同。

    注意点

    当我们在使用 useState 时,修改值时传入同样的值,组件不会重新渲染,这点和类组件setState保持一致。

    那么进行第二次实验,我先快速点击 preAdd 三下,然后接着快速点击 add 按钮三次,三秒后结果会怎么样呢。根据以上结论猜测,preAdd 是根据最新值,所以 count 依次变为 1、2、3,然后 add 是传入的当前 count 为 0,最后变为 1。最后结果应该是 1、2、3、1,测试结果正确:

    3过时的闭包(同上)

    组件<DelayedCount>有 2 个按钮:

    • 点击按键 “Increase async” 在异步模式下以1秒的延迟递增计数器

    • 在同步模式下,点击按键 “Increase sync” 会立即增加计数器。

      function DelayedCount() {
      const [count, setCount] = useState(0);

      function handleClickAsync() {

      setTimeout(function delay() {
        setCount(count + 1);
      }, 1000);
      

      }

      function handleClickSync() {

      setCount(count + 1);
      

      }

      return (

      <div>
        {count}
        <button onClick={handleClickAsync}>Increase async</button>
        <button onClick={handleClickSync}>Increase sync</button>
      </div>
      

      );
      }

      点击 “Increase async” 按键然后立即点击 “Increase sync” 按钮,count 只更新到 1

      这是因为 delay() 是一个过时的闭包。

      来看看这个过程发生了什么:

      1. 初始渲染:count 值为 0
      2. 点击 'Increase async' 按钮。delay() 闭包捕获 count 的值 0setTimeout() 1 秒后调用 delay()
      3. 点击 “Increase async” 按键。handleClickSync() 调用 setCount(0 + 1)count 的值设置为 1,组件重新渲染。
      4. 1 秒之后,setTimeout() 执行 delay() 函数。但是 delay() 中闭包保存 count 的值是初始渲染的值 0,所以调用 setState(0 + 1),结果count保持为 1

      delay() 是一个过时的闭包,它使用在初始渲染期间捕获的过时的 count 变量。

      为了解决这个问题,可以使用函数方法来更新 count 状态:

      function DelayedCount() {
        const [count, setCount] = useState(0);
      
        function handleClickAsync() {
          setTimeout(function delay() {
            setCount(count => count + 1); // 这行是重点
          }, 1000);
        }
      
        function handleClickSync() {
          setCount(count + 1);
        }
      
        return (
          <div>
            {count}
            <button onClick={handleClickAsync}>Increase async</button>
            <button onClick={handleClickSync}>Increase sync</button>
          </div>
        );
      }
      
      

      现在 setCount(count => count + 1) 更新了 delay() 中的 count 状态。React 确保将最新状态值作为参数提供给更新状态函数,过时的闭包的问题就解决了。

    4复杂变量

    import React, { useState } from 'react'
    
    export default function ComplexHookState() {
    
      const [friends, setFrineds] = useState(["zhangsan", "lisi"]);
      
      function addFriend() {
        friends.push("wangwu");
        setFrineds(friends);
      }
    
      return (
        <div>
          <h2>好友列表:</h2>
          <ul>
            {
              friends.map((item, index) => {
                return <li key={index}>{item}</li>
              })
            }
          </ul>
          // 正确的做法
          <button onClick={e => setFrineds([...friends, "wangwu"])}>添加朋友</button>
          // 错误的做法
          <button onClick={addFriend}>添加朋友</button>
        </div>
      )
    }
    
    

    这里定义的状态是一个数组,如果想修改这个数组,需要重新定义一个数组来进行修改,在原数组上的修改不会引起组件的重新渲染。因为,React组件的更新机制对state只进行浅对比,也就是更新某个复杂类型数据时只要它的引用地址没变,就不会重新渲染组件。因此,当直接向原数组增加数据时,就不会引起组件的重新渲染。

    对于这种情况,常见的做法就是使用扩展运算符(...)来将数组元素重新赋值给一个新数组,或者对原数据进行深拷贝得到一个新的数据。

    参考文章

    万字总结,React Hooks 初探

    React useState() 使用指南

    使用 JS 及 React Hook 时需要注意过时闭包的坑

  • 相关阅读:
    hdu 5101 Select
    hdu 5100 Chessboard
    cf B. I.O.U.
    cf C. Inna and Dima
    cf B. Inna and Nine
    cf C. Counting Kangaroos is Fun
    Radar Installation 贪心
    spfa模板
    Sequence
    棋盘问题
  • 原文地址:https://www.cnblogs.com/leise/p/15150374.html
Copyright © 2011-2022 走看看