React Hooks —— useState异步更新队列、闭包、浅比较深入理解

useState

作用:在函数组件中使用状态,修改状态值可让函数组件更新,类似于类组件中的setState

语法:

const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数

seXXX(value)修改状态值为value,并通知视图更新。注意,不同于类组件setState的部分更新语法,而是直接修改成value

import React, { useState } from "react";
export default function Demo(props) {
    let [num, setNum] = useState(10);
    const handler = () => {
        setNum(num + 1);
    };
    return <div>
        <span>{num}</span>
        <button onClick={handler}>新增</button>
    </div>;
};

函数组件【Hooks组件】不是类组件,所以没有实例的概念,调用组件不再是创建类的实例,而是执行函数并产生一个私有上下文而已,所以在函数组件中不涉及this的处理

设计原理

类组件只在初次渲染时创建一个实例,之后的更新都是按照生命流程走,并不是重新创造实例。

函数组件的每一次渲染或更新是让函数重新执行,也就是useState会被重新执行,产生一个全新的私有上下文,内部的代码也重新执行

// 函数组件每一次渲染/更新,都具备独立的闭包
import React, { useState } from "react";
export default function Demo(props) {
    let [num, setNum] = useState(10);
    const handler = () => {
        setNum(100);
        setTimeout(() => {
            console.log(num); //10
        }, 1000);
    };
    return <div>
        <span>{num}</span>
        <button onClick={handler}>新增</button>
    </div>;
};

实现原理

  1. 执行handle方法时,由于所用到的setNum num都不是当前作用域的私有变量,所以里面会沿着作用域链找到上级上下文里面的num和setNum(闭包)

  2. 每次更新都重新执行一次内部的代码、都创建一个新的私有上下文如EC(DEMO2),涉及的函数需要进行重新构建。这些函数的作用域会沿着函数的作用域链向上查找,找到每一次执行DEMO产生的新的闭包

  3. 每一次执行DEMO函数,也会把useState重新执行。但是:

    • 返回的状态:只有第一次设置的初始值会生效,其余以后再执行,获取的状态都是最新的状态,而不是初始值。

    • 返回的修改状态的方法:每一次都是新的方法函数

    • 每次运行useState返回的结果都用新的num和setNum变量保存,因此状态和修改状态的方法的地址和之前的都不同,是全新的

在这里插入图片描述

那么它是如何确保每一次获取的是最新状态值,而不是传递的初始值呢?

// 核心原理:闭包
var _state; // 创建全局state。
function useState(initialState) {
  _state = _state | initialState;
  function setState(state) {
  	if(Object.is(_state, value)) return;
  	if(typeof value === 'function'){
  		_state = value(_state) // 相当于传入prevalue后,return经过处理得到的新value
  	}else{
  		_state = value
  	}
    // 通知视图更新 
    //...重新渲染组件
  }
  return [_state, setState]; // 数组是新的变量,里面的每项自然也是新的,栈地址也不相同 
}

let [num1, setNum] = useState(0); //初始时num1=0  setNum=setState 0x001
setNum(100); //=>_state=100 通知视图更新
// ---
再次执行整个函数组件,在执行到useState的时候:
let [num2, setNum] = useState(0); //由于初次渲染时,全局state被赋值了,不再为undefined,所以不再赋值为initialState
在内部又产生了一个新的setState,地址和之前不同,使用这次新的闭包作为父级上下文
最后返回新的state和新的setState并被声明为新的变量
num2=100  setNum=setState 0x002

setXXX沿着作用域查找闭包的理解——与同步异步无关

在这里插入图片描述

第一个setTimeout沿着作用域链找到的闭包里的num是初始渲染的num,和setNum后产生的新的闭包(作用域)无关,因此输出0

更新多状态

方案一:类似于类组件中一样,让状态值是一个对象(包含需要的全部状态),每一次只修改其中的一个状态值——setXXX不支持类组件setState的partial state change
import React, { useState } from "react";
export default function Demo(props) {
    let [state, setState] = useState({
        x: 10,
        y: 20
    });
    const handler = () => {
        // setState({ x: 100 }); //state={x:100}
        setState({
            ...state,
            x: 100
        });
    };
    return <div>
        <span>{state.x}</span>
        <span>{state.y}</span>
        <button onClick={handler}>处理</button>
    </div>;
};

问题:不能像类组件的setState函数一样,支持部分状态更新!

方案二:执行多次useState,把不同状态分开进行管理「推荐方案」——解耦
import React, { useState } from "react";
export default function Demo(props) {
    let [x, setX] = useState(10),
        [y, setY] = useState(20);
    const handler = () => {
        setX(100);
    };
    return <div>
        <span>{x}</span>
        <span>{y}</span>
        <button onClick={handler}>处理</button>
    </div>;
};

更新队列机制【updater,异步批处理】——异步和闭包是两码事

和类组件中的setState一样,每次更新状态值,也不是立即更新,而是利用了更新队列updater机制来处理

①遇到setState会立即将其放入到更新队列中,此时状态和视图还都未更新

当所有的代码操作结束,会刷新队列,也就是通知更新队列中的所有任务执行:把所有放入的setState合并在一起执行,只触发一次状态更新和视图更新

  • React 18 全部采用批更新
  • React 16中也和this.setState一样,只在合成事件/生命周期函数中异步,在定时器、手动DOM事件绑定等操作中同步

在这里插入图片描述

  • 可以基于flushSync刷新渲染队列
检验方式一:在handler里面修改state之后直接log ❌

不能在handler里面修改state之后直接log,因为这时log的变量仍然是上一次闭包中的,无论同步还是异步更新,都只能是上一个闭包中的值

因此,每次log的结果都是上一次的state

import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';
import { flushSync } from 'react-dom';

const Demo = function Demo() {
    let [x, setX] = useState(10),
        [y, setY] = useState(20),
        [z, setZ] = useState(30);

    const handle = () => {
        setX(x + 1);
        console.log(x);
        // 1.异步批处理:所有的setXXX操作放到更新队列里面,执行完所有操作之后才会一次清空更新队列,因此console.log先执行
        // 2.闭包:由于handle始终拿到的是父级作用域的闭包,也就是更新前的闭包
        setY(y + 1);
        setZ(z + 1);
    };
    return <div className="demo">
        <span className="num">x:{x}</span>
        <span className="num">y:{y}</span>
        <span className="num">z:{z}</span>
        <Button type="primary"
            size="small"
            onClick={handle}>
            新增
        </Button>
    </div>;
};

export default Demo;
检验方式二:比较输出”验证值的次数“

若同步更新,那么会顺序输出“render”三次

若异步更新,则只在最后批处理更新一次,所以只输出一次

import React, { useState } from "react";
import { Button } from 'antd';
import './Demo.less';
import { flushSync } from 'react-dom';

const Demo = function Demo() {
    console.log('RENDER渲染');
    let [x, setX] = useState(10),
        [y, setY] = useState(20),
        [z, setZ] = useState(30);

    const handle = () => {
        setX(x + 1);
        setY(y + 1);
        setZ(z + 1);
    };
    return <div className="demo">
        <span className="num">x:{x}</span>
        <span className="num">y:{y}</span>
        <span className="num">z:{z}</span>
        <Button type="primary"
            size="small"
            onClick={handle}>
            新增
        </Button>
    </div>;
};

export default Demo;

只更新了一次,说明是异步执行的,与位置无关

在这里插入图片描述

更新队列flushSync设置同步操作

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      l

异步操作与闭包的深入理解

在这里插入图片描述

异步:handle里面的10次setX都会放在更新队列里面,然后在其他事情都做完之后,批处理一次更新完毕所有队列中的数据和视图,因此只’RENDER渲染’一次

闭包:x最后的状态值是11,因为handle里面的所有x都是在上一级闭包中拿到的,都是10,因此批处理中10个setX都是将x更新为11

setXXX的两种传参方式

1.直接传入新对象,不支持this.setState的部分更新
2.函数式更新——配合for循环、updater机制可以实现结果累计、只更新状态和视图一次 setXXX(prev => { return curV; })

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState;该函数将接收先前的 state,并返回一个更新后的值!

惰性初始state——复杂的初始化逻辑只执行一次

如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用,之后更新视图以后,状态值不再是undefined,所以不会再执行初始的惰性回调

import React, { useState } from "react";
export default function Demo(props) {
    let [num, setNum] = useState(() => {
        let { x, y } = props;
        return x + y;
    });
    return <div>
        <span>{num}</span>
    </div>;
};
import React, { useState } from "react";
export default function Demo() {
    let [num, setNum] = useState(10);
    const handler = () => {
        for (let i = 0; i < 10; i++) {
            // 函数式更新
            setNum(num => {
                return num + 1;
            });
        }
    };
    return <div>
        <span>{num}</span>
        <button onClick={handler}>处理</button>
    </div>;
};
// 核心原理:闭包
var _state; // 创建全局state。
function useState(initialState) {
  _state = _state | initialState;
  if(typeof _state === 'undefined'){
      if(typeof initialState === 'function'){
          _state = initialState(); // 惰性初始化
      }else{
          _state = initialState;
      }
  }
  function setState(value) {
  	if(Object.is(_state, value)) return;
  	if(typeof value === 'function'){
  		_state = value(_state) // 相当于传入prevalue后,return经过处理得到的新value
  	}else{
  		_state = value
  	}
    // 通知视图更新 
    //...重新渲染组件
  }
  return [_state, setState]; // 数组是新的变量,里面的每项自然也是新的,栈地址也不相同 
}

let [num1, setNum] = useState(0); //初始时num1=0  setNum=setState 0x001
setNum(100); //=>_state=100 通知视图更新
// ---
再次执行整个函数组件,在执行到useState的时候:
let [num2, setNum] = useState(0); //由于初次渲染时,全局state被赋值了,不再为undefined,所以不再赋值为initialState
在内部又产生了一个新的setState,地址和之前不同,使用这次新的闭包作为父级上下文
最后返回新的state和新的setState并被声明为新的变量
num2=100  setNum=setState 0x002

优点:如果将回调里的逻辑写到外面,则一旦视图更新,不管是第一次还是后续更新的时候,这段逻辑都会执行。即使在更新阶段,num不再是undefined,初始值不再生效,这段逻辑依然会执行,浪费资源效率低下

在这里插入图片描述

useState性能优化机制——Object.is 类似PureComponent的浅比较

useState自带了性能优化的机制:

  • 每一次修改状态值的时候,会拿最新要修改的值和之前的状态值做浅比较「基于Object.is作浅比较,而不是更严格的===。如果前后状态都是NaN,Object.is返回true不更新状态和视图,===返回false更新状态和视图」
  • 如果发现两次的值是一样的,则不会修改状态,也不会让视图更新「可以理解为:类似于PureComponent,在shouldComponentUpdate中做了浅比较和优化,注意函数组件中不可能有PureComponent」

调用 State Hook 的更新函数,并传入当前的 state 时,React 将跳过组件的渲染(原因:React 使用 Object.is 比较算法,来比较新老 state;注意不是因为DOM-DIFF;)!

import React, { useState } from "react";
export default function Demo() {
    console.log('render');
    let [num, setNum] = useState(10);
    return <div>
        <span>{num}</span>
        <button onClick={() => {
            setNum(num);
        }}>处理</button>
    </div>;
};
// 核心原理:闭包
var _state; // 创建全局state。
function useState(initialState) {
  _state = _state | initialState;
  function setState(state) {
  	if(Object.is(_state, value)) return;
  	if(typeof value === 'function'){
  		_state = value(_state) // 相当于传入prevalue后,return经过处理得到的新value
  	}else{
  		_state = value
  	}
    // 通知视图更新 
    //...重新渲染组件
  }
  return [_state, setState]; // 数组是新的变量,里面的每项自然也是新的,栈地址也不相同 
}
let [num1, setNum] = useState(0); //初始时num1=0  setNum=setState 0x001
setNum(100); //=>_state=100 通知视图更新
// ---
再次执行整个函数组件,在执行到useState的时候:
let [num2, setNum] = useState(0); //由于初次渲染时,全局state被赋值了,不再为undefined,所以不再赋值为initialState
在内部又产生了一个新的setState,地址和之前不同,使用这次新的闭包作为父级上下文
最后返回新的state和新的setState并被声明为新的变量
num2=100  setNum=setState 0x002
例1 前后state浅比较true,不更新状态和视图
import React, { useState } from "react";
import { Button } from 'antd';
const UseStateDemo = function UseStateDemo() {
  console.log('RENDER');
  let [x, setX] = useState(10);
    const handle = () => {
      setX(10);
    };
    return <div className="UseStateDemo">
        <span className="num">x:{x}</span>
        <Button type="primary"
            size="small"
            onClick={handle}>
            新增
        </Button>
    </div>;
};

export default UseStateDemo;

不更新视图和状态

例2 更新多次,最终值11
import React, { useState } from "react";
import { Button } from 'antd';
import { flushSync } from 'react-dom'; 

const UseStateDemo = function UseStateDemo() {
  console.log('RENDER');
  let [x, setX] = useState(10);
    const handle = () => {
      for (let i = 0; i < 10; i++) {
        flushSync(() => {
          setX(x + 1);
        })          
      }
    };
    return <div className="UseStateDemo">
        <span className="num">x:{x}</span>
        <Button type="primary"
            size="small"
            onClick={handle}>
            新增
        </Button>
    </div>;
};

export default UseStateDemo;

render多次(理论上是一次,这里是因为这些操作作用的都是一个闭包中的同一个状态值x,在第一次改状态还没改成功之前,其他次操作访问的状态值仍旧是还没改的10,直到第一次改状态成功,剩余次操作不会再通过Object.is的测试。这里render次数和浏览器的效率有关,可能是3次也可能是5次,不过绝对不是10次 ),最后状态值是11

在这里插入图片描述

在第一次渲染时创造出来顶级的函数作用域,_state私有属性就是在这个顶级作用域里面的

点击handle之后会执行10次同步清空更新队列的操作

在第一次flushSync,更新队列里只有一个setX,立即同步执行,使用的是第一次Demo创建出来的EC的闭包中的state,也就是10。进入setX,通过了Object.is的比较,更新状态和视图,此时最外部的_state也被更新为11

第二次flushSync,更新队列里只有一个setX,立即同步执行,使用的也是第一次Demo创建出来的EC的闭包中的state(因为这些flushSync方法都存在于第一个上下文中),也就是10。进入setX,第一次flushSync还没来得及彻底修改完成,所以也通过了Object.is的比较,更新状态和视图

直到某一次flushSync的时候,闭包中的X被彻底修改完了,此时不通过Object.is的比较,不再更新状态和视图,之后的若干轮轮同样不更新

例3 更新1次,最终值20 ——函数式更新

在这里插入图片描述

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
<h3>回答1:</h3><br/>React Hooks 的 useState 可以用来更新状态。useState 返回一个数组,第一个元素是当前状态,第二个元素是一个函数,用于更新状态。可以通过调用这个函数并传入新的状态更新状态。例如: ``` import React, { useState } from 'react'; function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } ``` 在这个例子中,我们使用 useState 创建了一个名为 count 的状态变量,并将其初始设置为 0。我们还使用了 setCount 函数更新 count 的。当用户点击按钮时,我们会调用 setCount 并传入一个新的,这个新的会被用来更新 count 的。 <h3>回答2:</h3><br/>React Hooks 中的 useState 是许多 React 开发者在创建组件时会经常用到的一个 Hook。它旨在帮助开发者在组件中存储和更新本地状态。 通过 useState,我们可以将组件中的状态数据添加到函数中,而不需要类组件的构造函数或 setState 方法。useState 的第一个返回是当前状态的,第二个返回更新状态的方法。该方法可以用于更新状态并重新渲染组件。 当调用 useState 时,我们必须传入一个参数,它用于初始化状态的。例如: ``` import React, { useState } from 'react'; function Example() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } ``` 在上述代码中,count 是一个状态,并且初始为 0。setCount 是一个函数,它可以用于更新 count 的。当点击按钮时,调用 setCount 来更新 count 的,这样组件会重新渲染并显示新的 count useState 的一个重要的使用场景是在处理表单数据时。通常我们需要在表单中存储用户输入的数据并在表单提交时将其发送到服务器。我们可以使用 useState 来存储表单数据并更新它们。 总结起来,useStateReact Hooks 中用于在组件中存储和更新状态的重要Hook之一,可以帮助开发者在构建React组件时更加便捷,实现清晰易读的代码。 <h3>回答3:</h3><br/>React Hooks 已经成为 React 中一个非常重要的功能。在使用 Hooks 时,最常用的是 useState 函数。这个函数提供了一种方便且简单地方式来在 Functional Components 中定义状态,并且可以使用 setState 函数更新状态。 useState 的语法非常简单,它接受一个参数,表示状态的初。然后,它返回一个数组,该数组包含两个。第一个是当前的状态,第二个是一个函数,用于更新状态。我们可以把这个函数叫做 setState 函数。 在使用 setState 函数时,我们首先需要理解的是它是一个异步函数。这意味着,当我们调用 setState 函数时,React 并不会立即更新状态。相反,它会先对比新和旧,然后将新的状态合并到原来的状态对象中,最后在下一次 render 时,将新的状态更新组件中。 如果我们需要在更新状态后做一些操作,例如向服务器发送请求或者更新页面元素,我们需要使用 useEffect Hooks。 具体来说,useState 函数接收的参数是状态的初始,可以是任意类型的,而更新状态时可以调用 setState 函数,它接收一个新的状态作为参数。setState 函数可以是一个函数,用于更新状态,该函数接收一个参数 prevState,表示当前状态。 例如,我们可以这样写一个计数器组件: ``` function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } ``` 在上面的例子中,我们使用了 useState 来定义了一个 count 的状态,初为 0。然后,我们在组件的 render 方法中使用了这个状态,并且实现了一个增加按钮的逻辑。点击按钮时,我们通过调用 setCount 函数更新状态。 总之,useState 是建立和管理状态的函数,它会返回一个数组,其中两项分别代表当前状态和更新状态的函数。使用它来更新状态是非常方便的,同时也充分体现了 React Hooks 在状态管理方面更加简单、便捷的特点。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值