React的Hooks函数useState

前言

在React开发中,类组件开发是具备状态的,可以使用this.setState来更新状态,使组件重新渲染,但在函数组件中,是不具备状态,也没有实例的概念,调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文,且在函数组件中不涉及this的处理,因此React提供了Hooks函数,可以使用Hooks函数来控制函数组件的状态更新,useState就是其中最重要之一。

Hooks函数是只能在函数组件中使用的函数,函数组件的每一次渲染(或者是更新),都是把函数(重新)执行,产生一个全新的“私有上下文”,内部的代码也需要重新执行。

什么是 useState

useState 是 React 提供的一个 Hook,用于在函数组件中添加状态。通过 useState,你可以为组件引入内部状态,使其能够追踪和响应状态的变化。

基本语法

useState 返回一个包含两个元素的数组,第一个元素是当前状态的值,第二个元素是一个用于更新状态的函数。

const [state, setState] = useState(initialState);
  • state 是当前状态的值。
  • setState 是用于更新状态的函数。
  • initialState 是状态的初始值,只在组件的初始渲染时生效。

基本用法

让我们通过一个简单的计数器示例来演示 useState 的基本用法:

import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

root.render(
  <>
    <Counter></Counter>
  </> 
);

在这个例子中,我们使用 useState 创建了一个名为 count 的状态,初始值为 0。每次点击按钮时,会触发 setCount 操作,之后整个函数组件都会重新执行一遍,最后会把 count 的数量更新为 1。。

useState实现原理

useState的工作原理:
1、当你第一次调用 useState 时,React 会使用你提供的初始值来设置state
2、在后续的渲染中,useState 会返回当前的 state
3、当你调用 setState 函数时,React 会重新渲染组件,并使用你提供的新值来更新 state
根据以上工作原理,我们可以实现一个简单的 useState

const root = ReactDOM.createRoot(document.getElementById('root'));
let _state
function useState(initialValue) {
  _state = _state === undefined ? initialValue : _state
  function setState(newState) {
    _state = newState
    render()
  }
  return [_state, setState]
}

const Counter = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

function render() {
  root.render(
    <>
      <Counter></Counter>
    </> 
  );
}
render()

当点击按钮后,状态可以跟着改变了,至此,我们已经实现了一个非常简易的useState

但目前的实现仍然存在个问题,当在组件中同时使用两个useState

const root = ReactDOM.createRoot(document.getElementById('root'));
let _state
function useState(initialValue) {
  _state = _state === undefined ? initialValue : _state
  function setState(newState) {
    _state = newState
    render()
  }
  return [_state, setState]
}

const Counter = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>count Increment</button>
      <p>Num: {num}</p>
      <button onClick={() => setNum(num + 1)}>num Increment</button>
    </div>
  );
};

function render() {
  root.render(
    <>
      <Counter></Counter>
    </> 
  );
}
render()

运行上面的代码,会发现,不论点击哪个按钮 count、num 都会同时改变。这是由于,我们将所有数据都放在一个 _state 中,导致 state 冲突。

因此可以把_state存储在一个数组中,如 _state = [0, 0],这样可以保证数据一一对应,再次修改useState,如下:

const root = ReactDOM.createRoot(document.getElementById('root'));
let _states = []; // 存储所有 state 的数组
let _index = 0; // 当前正在处理的 state 的索引
function useState(initialValue) {
  const currentIndex = _index; // 在函数闭包中保存当前 state 的索引
  _states[currentIndex] = _states[currentIndex] === undefined ? initialValue : _states[currentIndex]; // 如果当前 state 未定义,则使用初始值

  function setState(newState) {
    _states[currentIndex] = newState; // 更新当前 state
    render()
    _index = 0; // 重置索引
  }
  _index++; // 在返回前,将索引加 1,以便下次调用 useState 时获取下一个 state
  return [_states[currentIndex], setState]
}

const Counter = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>count Increment</button>
      <p>Num: {num}</p>
      <button onClick={() => setNum(num + 1)}>num Increment</button>
    </div>
  );
};

function render() {
  root.render(
    <>
      <Counter></Counter>
    </> 
  );
}
render()

再次运行,两个 useState 之间就不会相互影响了。至此,我们已经实现了一个单个组件可用的 useState。这只是一个非常简化的模拟,实际的 useState 实现要复杂得多,并且包含了许多优化和错误处理的代码。

在React中,如果有多个组件调用 useState,那么就会在每个组件都创建一个 _state_index,那这么多的_state_index 存放在哪里呢?那是存放在每个组件对应的虚拟 DOM 对象上的,而每个虚拟节点是 FiberNode``,_state 的真实名称应为 memorizedState

每个节点都会有一个对应的Fiber对象,他的数据解构如下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;  // 就是ReactElement的`?typeof`
  this.type = null;         // 就是ReactElement的type
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.firstContextDependency = null;

  // ...others
}

useState更新原理

运行下面代码:

const Counter = () => {
  console.log('函数执行了')
  const [count, setCount] = useState(0);
  const handleClick = ()  => {
    setCount(count + 1)
    setTimeout(() => {
      console.log('我是2s后输出的;',count)
    },2000)
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>count Increment</button>
    </div>
  );
};

浏览器效果
useState更新原理
当点击按钮触发handleClick方法,执行setCount后,count改为了1,Counter函数也重新执行了,但2s后在setTimeout里面却输出了:0,还是上次的值。为什么会这样呢?

因为当前组件每次重新render都会重新执行 useStateuseState 每次执行都会返回一个新的count,setTimeout函数中输出的count还是重新 render 之前的count,因此输出0。

useState更新多状态

运行下面代码:

const Counter = () => {
  const [state, setState] = useState({
    count:10,
    num:20
  });
  const handleClick =  () =>{
    setState({
      count:100
    })
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {state.count}</p>
      <p>Num: {state.num}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

当点击按钮触发handleClick,执行setState更新count的值为100,Counter重新执行后,发现页面num的值丢失了。
修改handleClick,把state修改前的值都传入setState

const Counter = () => {
  const [state, setState] = useState({
    count:10,
    num:20
  });
  const handleClick =  () =>{
    setState({
      ...state,
      count:100
    })
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {state.count}</p>
      <p>Num: {state.num}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

当点击按钮触发handleClick,执行setState更新count的值为100,发现页面的num可以正确显示了。

因此useState不能像类组件的setState函数一样,支持部分状态更新,所以当有多个状态需要更新时,执行多次useState,把不同状态分开进行管理。

const Counter = () => {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  const handleClick =  () =>{
    setCount(100)
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <p>Num: {num}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

useState异步操作

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

  • 遇到setState会立即将其放入到更新队列中,此时状态和视图还都未更新
  • 当所有的代码操作结束,会刷新队列,也就是通知更新队列中的所有任务执行:把所有放入的setState合并在一起执行,只触发一次状态更新和视图更新
const Counter = () => {
  console.log('Counter函数执行了')
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  const handleClick =  () =>{
    setCount(count + 1)
    setNum(num + 1)
    console.log(count,num)//0 0
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <p>Num: {num}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

点击按钮“改变状态”,触发handleClick,执行setCount、setNum之后直接log,这时log输出的两个值都是0。
因为:
1、执行setCount、setNum时,会把所有的setXXX操作放到更新队列里面,执行完所有操作之后才会一次清空更新队列,因此console.log先执行。
2、当执行setCount、setNum时,会重新执行Counter,因此每次console.log里面拿到的值结果都是上一次的。

useState同步操作

类组件是通过flushSync设置同步操作,执行函数会立即执行包裹在函数内或清空更新队列中的setState,就不会等到所有操作都完成了才去清空更新队列了。

//..
import { flushSync } from 'react-dom';
//...
const Counter = () => {
  console.log('Counter函数执行了')
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  const handleClick =  () =>{
    flushSync(() =>{
      setCount(count + 1)
      setNum(num + 1)
    })
    console.log(count, num)//0 0
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <p>Num: {num}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

浏览器效果
useState同步操作
上面点击按钮 ,触发handleClick,遇到flushSync,会立即刷新更新队列,执行包裹在内的setXXX,然后马上重新执行Counter函数。这理可以看出先执行的Counter函数,然后 在执行的console.log(count, num),最后结果输出0 0。

因此useState不管是异步更新队列的还是同步更新队列,在handleClick内拿到的状态都是更新之前的值。因为在每次执行Counter函数,都会形成一个新的作用域,类似一个新的闭包。
下面来看看这个代码,结果会是什么呢?

const Counter = () => {
  console.log('Counter函数执行了')
  const [count, setCount] = useState(0);
  const handleClick =  () =>{
    for(let i = 0; i<  10 ;i ++){
      setCount(count + 1)
    }
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

当点击按钮,触发handleClick,执行for循环后count最后的状态值是1。
因为:
1、for循环内执行了10次setCount,然而10次setCount都会添加到更新队列中,然后在其他事情都做完之后,批处理一次更新完毕所有队列中的数据和视图,因此Counter函数只执行了一次。
2、handleClick里面的所有count都是在上一级闭包中拿到的都是0,因此批处理中10个setCount都是将count更新为1。

useState函数更新

函数式更新,在函数内可以接收一个当前的状态值参数,并返回一个更新后的值!
运行下面代码:

const Counter = () => {
  const [count, setCount] = useState(0);
  const handleClick =  () =>{
    setCount((preCount)=>{
      return preCount + 1
    })
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

当点击按钮,触发handleClick,给setCount传递一个函数作为参数,这个函数的preCount是当前状态值,可以根据这个参数做一些需要累积的状态。比如:

const Counter = () => {
  const [count, setCount] = useState(0);
  const handleClick =  () =>{
    for(let i = 0; i < 10; i++){
      setCount(preCount=>{
        return preCount + 1
      })
    }
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>改变状态</button>
    </div>
  );
};

以上结果count的状态值是10。

useState初始化参数是函数

如果在组件内,一些初始化状态的值需要通过复杂的业务或计算获得,可以传入一个函数,在函数中计算并返回初始的state,此函数只在初始渲染时被调用。
在父组件给子组件传递属性作为子组件的初始值:

const Parent = () => {
  const [count, setCount] = useState(100);
  const handleClick =  () =>{
    setCount(preCount=>{
        return preCount + 1
    })
  }
  return (
    <div>
      <h2>React的Hooks函数useState</h2>
      <p>Count: {count}</p>
      <button onClick={handleClick}>改变状态</button>
      <Child x={count}  y={20}></Child>
    </div>
  );
};

子组件收到父组件传递的props内的x、y,然后通过useState内的函数初始化子组件的状态值:

function Child(props){
  const [num, setNum] = useState(() => {
    const { x, y} = props
    return x + y
  });
  const handleClick =  () =>{
    setNum(num + 1)
  }
  return (
    <div>
      <h2>我是子组件</h2>
      <div>子组件的Num:{num}</div>
      <button onClick={handleClick}>子组件改变状态</button>
    </div>
  )
}

上面子组件不管怎么改变状态和渲染页面都不会在执行useState内的函数。


 上一篇
React的Hooks函数useEffect和useLayoutEffect React的Hooks函数useEffect和useLayoutEffect
React 的 useEffect 是一个重要的 Hook,用于处理组件的副作用,而useLayoutEffect和它一样,都是处理函数组件内的副作用的,下面就来说说这两个Hook函数的用法。 useEffect useEffect是类组件
2022-04-11
下一篇 
React的合成事件 React的合成事件
什么是React的合成事件 React 合成事件(SyntheticEvent)是 React 框架提供的一种事件对象,它封装了不同浏览器的事件对象,使得开发者可以编写跨浏览器兼容的代码。合成事件对象中,也包含了浏览器内置事件对象中的一些属
2022-04-10
  目录