跳到主要内容

认识

2023年02月22日
柏拉文
越努力,越幸运

一、认识


useState 允许在函数组件中定义一个状态变量。useState 返回一个数组, 该数组中含有两个元素: 当前状态值和一个更新该状态的函数。useState 可以传入一个初始值或者一个回调函数, 回调函数会得到一个先前状态的参数。当调用更新状态的函数时, React 会将传入的值与当前状态进行 Object.is 比较, 如果状态发生变化, React 将会重新渲染组件。useState 与类组件中 this.setState 不同的是, useState 会直接替换之前值, 而不是合并。React 会对 useState 调用进行批量处理已优化性能。

二、语法


const [state, setState] = useState<T>(initialState)
  • state: 当前的 state。在首次渲染时,它将与你传递的 initialState 相匹配

  • setState: 返回 dispatch 方法,可以让你将 state 更新为不同的值并触发重新渲染。它可以是任何类型的值,但对于函数有特殊的行为。

    • 对象:

    • 函数: 如果你将函数作为 nextState 传递,它将被视为更新函数。它必须是纯函数,只接受待定的 state 作为其唯一参数,并应返回下一个状态。React 将把你的更新函数放入队列中并重新渲染组件。在下一次渲染期间,React 将通过把队列中所有更新函数应用于先前的状态来计算下一个状态。

  • initialState: state 初始化的值, 它可以是任何类型的值,但对于函数有特殊的行为。在初始渲染后,此参数将被忽略。如果传递函数作为 initialState,则它将被视为 初始化函数。它应该是纯函数,不应该接受任何参数,并且应该返回一个任何类型的值。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态。

    const [state, setState] = useState<T>(xx)

    const [state, setState] = useState<T>(()=> { return xx })

    注意, 直接传递初始化函数与传递调用初始化函数的结果的区别如下所示:

    const calculate = ()=>{
    return xxx;
    }

    const [state, setState] = useState<T>(calculate); // 这个例子传递了初始化函数,因此 calculate 函数仅在初始化期间运行。当组件重新渲染,它不会再次运行。

    const [state, setState] = useState<T>(calculate()); // 这个例子 没有 传递初始化函数,因此 calculate 函数会在每次渲染时运行,这种行为没有什么明显的差异,但这种代码是不那么高效的。

    因此, 我们如果使用函数时, 必须直接将函数作为初始值传入(函数本身), 而不是函数的调用结果。 这样 React 只在初次渲染时调用函数,后续渲染时将其忽略, 可以避免重复创建初始状态。

2.1 对象

const [value,setValue] = useState(0);

setValue(xx);

2.2 函数

const [value,setValue] = useState(0);

setValue((prev)=> xx);

三、更新


React.js 18.x 之前, React合成事件生命周期钩子(除 componentDidUpdate 除外)将会批量处理更新。但是在 Promise.thensetTimeout 或者原生事件处理程序中, 更新将会以同步的方式处理。

React.js 18.x 之后, 几乎所有更新, 包括 Promise.thensetTimeout 或者原生事件处理程序等, 都将自动批处理。因此, 在 React.js 18.x 之后, 如果需要强制同步更新 useState dispatch, 可以将更新包装在 flushSync 中, 但是这样会损害性能。

useState dispatch 批量更新策略: 多次进行 useState dispatch: 如果传入值, 会对其进行覆盖, 取最后一次执行。如果传入函数, 更新函数放入队列, 依次执行, 并计算。

3.1 React.js 18 之前值更新

3.2 React.js 18 之后值更新

import { useEffect, useState } from 'react';

function App() {
const [value, setValue] = useState(0);

useEffect(() => {
setValue(value + 1);
console.log(value); // 0
setValue(value + 1);
console.log(value); // 0

setTimeout(() => {
setValue(value + 2);
console.log(value); // 0
setValue(value + 1);
console.log(value); // 0
}, 0);
}, []);

return <div>{value}</div>; // 1
}

export default App;

3.3 React.js 18 之前函数更新

3.4 React.js 18 之后函数更新

import { useEffect, useState } from "react";

function App() {
const [value, setValue] = useState(0);

useEffect(() => {
setValue(prevValue => prevValue + 1);
console.log(value); // 0
setValue(prevValue => prevValue + 1);
console.log(value); // 0

setTimeout(() => {
setValue(prevValue => prevValue + 2);
console.log(value); // 0
setValue(prevValue => prevValue + 1);
console.log(value); // 0
}, 0);
}, []);

return <div>{value}</div>; // 5
}

export default App;

3.5 React.js 18 之前同步更新

3.6 React.js 18 之后同步更新

可以通过 flushSync 进行同步更新, 多次渲染。flushSync 函数内部的多个 setState 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。因此, 想要同步更新, 多次渲染, 需要将多个 useState dispatch 分开包裹。

function App() {
const [value, setValue] = useState(0);

useEffect(() => {
flushSync(() => setValue(value + 1));
console.log(value); // 0
flushSync(() => setValue(value + 1));
console.log(value); // 0
}, []);

console.log('更新'); // 执行两次
return <div>{value}</div>; // 1
}

export default App;

四、总结沉淀


4.1 使用useState需要注意什么?

使用useState中的dispatchAction函数更新状态时,一定要注意必须在一定的条件下执行,否则容易造成循环渲染

function App(){
const [ num,setNum ] = useState(0);

if(true){
setNum(num++);
}

return <div></div>
}

4.2 为什么 useState 返回的是 [] 而不是 {}?

答: const [a,b] = [1,2] 可以更简洁的随意起名,但是 const {a: a1,b:b2} 增加了代码量。

4.3 为什么setState()传入两次相同的状态,函数组件不更新?

4.4 类组件中的 setState 和函数组件中的 useState 有什么异同?

相同点: 从原理角度出发, setStateuseState 更新视图,底层都调用了 scheduleUpdateOnFiber 方法,而且事件驱动情况下都有批量更新规则

不同点: setState 自动具备浅合并功能, useState dispatch 更新引用需要手动浅合并

4.5 useState 的传参方式,有什么区别?

useState() 的传参有两种方式:纯数据和回调函数。这两者在初始化时,除了传入方式不同,没啥区别。但在调用时,不同的调用方式和所在环境,输出的结果也是不一样的。

const App = () => {
const [count, setCount] = useState(0);

const handleParamClick = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};

const handleCbClick = () => {
setCount(count => count + 1);
setCount(count => count + 1);
setCount(count => count + 1);
};
};

上面的两种传入方式,最后得到的 count 结果是不一样的。为什么呢?因为在以数据的格式传参时,这 3 个使用的是同一个 count 变量,数值是一样的。相当于setCount(0 + 1),调用了 3 次;但以回调函数的传参方式,React 则一般地会直接该回调函数,然后得到最新结果并存储到 React 内部,下次使用时就是最新的了。注意:这个最新值是保存在 React 内部的,外部的 count 并不会马上更新,只有在下次渲染后才会更新。

4.6 多次执行 useState(),会触发多次更新吗?

React18 中,无论是多个 useState()hook,还是操作(dispatch)多次的数据。只要他们在同一优先级,React 就会将他们合并到一起操作,最后再更新数据。

这是基于 React18 的批处理机制。React 将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setState 事件合并);在 v18 之前只在事件处理函数中实现了批处理,在 v18 中所有更新都将自动批处理,包括 promise 链、setTimeout 等异步代码以及原生事件处理函数;

4.7 useState()的 state 是否可以直接修改?是否可以引起组件渲染?

首先声明,我们不应当直接修改 state 的值,一方面是无法刷新组件(无法将新数据渲染到页面中),再有可能会对下次的更新产生影响。

唯一有影响的,就是后续要使用该变量的地方,会使用到新数据。但若其他 useState() 导致了组件的刷新,刚才变量的值,若是基本类型(比如数字、字符串等),会重置为修改之前的值;若是复杂类型,基于 js 的 对象引用 特性,也会同步修改 React 内部存储的数据,但不会引起视图的变化。