React Hook原理及使用之useState

1,735 阅读4分钟

useState的基本使用

借用官方的例子

import React, { useState } from 'react';

function Example() {
  // 声明一个叫 “count” 的 state 变量。
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useState函数的返回值是一个数组,函数可传入参数

  • 数组的第一项为需要改变状态的值;
  • 数组的第二项为一个可更新状态的函数。每次修改值后都会触发组件的重新渲染
  • useState函数可传入一个参数作为状态的初始值。

useState的原理

一个简单的实现

let state
function useState(initialState) {
    state = state || initialState;
    const setState = (newState) => {
        state = newState;
        render();
    }
    return [state, setState];
}

实现思路:
全局定义一个state用于存储需要进行状态管理的变量,初始时将initialState赋值给state
内部定义一个setState函数用于更新state,并触发render()函数进行渲染,在setState时对state进行更新后并重新渲染

一些问题

上面的useState函数看似基本能够满足useState的需求,但是如果有多个useState的情况下还能满足吗?答案是当然不会
还是延用官方的例子再来举个栗子。将其中的useState替换为我们自己写的函数

// 自己写的uesState
let state
function myUseState(initialState) {
    state = state || initialState;
    const setState = (newState) => {
        state = newState;
        render();
    }
    return [state, setState];
}

function Example() {
  // 声明一个叫 “count” 的 state 变量。
  const [count, setCount] = myUseState(0);
  const [num, setNum] = myUseState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click setCount
      </button>
      <p>You clicked {num} times</p>
      <button onClick={() => setNum(num + 1)}>
        Click setNum
      </button>
    </div>
  );
}

当点击setCount时,state为:1;
当点击setNum时,state会覆盖count的值。

继续优化

let state = []; // 用来保存数据
let index = 0; // 用来对应每一个数组项
function useState(initialState) {
    let currentIndex = index;
    state[currentIndex] = state[currentIndex] || initialState;
    function setState(newState) {
        state[currentIndex] = newState;
        render();
    }
    index++;
    return[state[currentIndex], setState];
}

function render() {
    index = 0;
    ReactDom.render(<Counter />, document.getElementById("root"));
}

实现思路:

  1. 将state申明为数组,数组中的每一项都对应一个可变状态的数据;
  2. 定义一个index,用于和需要存放的数据对应;
  3. 调用一次useState需要将index++,保证每个数据对应不同的索引值;
  4. ==注意:每次调用render函数的时候,需要重置index。(这是因为render重新渲染,组件内的useState会重新执行,每个数据会重新分配index。因此必须确保每个useState对应相同的index)==

一个小tips

const App = () => {
  const [count, setCount] = useState(0);
  if (count % 2 === 1) {
  // 在调整判断中赋值
    [num, setNum] = useState(0)
  }
  return (
    <div>
      {count}
      <button onClick={() => {setCount(count + 1)}}>+1</button>
      {num}
      <button onClick={() => {setNum(num + 1)}}>+1</button>
    </div>
  )
}

这样使用会报如下错误:

ESLint: React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.(react-hooks/rules-of-hooks)

翻译过来的大概意思是:react的“useState”在条件语句中被申明。但该hooks在每个组件下必须以相同的顺序进行调用。
错误的具体原因是:我们在条件语句中定义useState的话,第一次只会执行一个useState给count赋值,也只有一个索引,但是第二次会有两个索引。这样会出现bug
因此不能在条件语句中定义state

总结

  • 在使用state的时候,每次重新渲染,组件函数都会重新执行一次
  • 每一个组件函数都对应一个react节点,每个节点都有一个自己的index和state
  • useState设置数据时会根据index去一一对应
  • index的值根据使用useState的顺序决定
  • 使用setState去修改state并触发更新

一些注意的点

1. setState不能局部更新对象,如果state是一个对象,在set的时候只修改某一个属性会导致其他属性为undefined,==因为setState不会合并属性==

// false
function App() {
  const [user,setUser] = useState({name:'tom', age: 18})
  const onClick = ()=>{
    setUser({
      name: 'marry',
    })
  }
  return (
    <div className="App">
      <div>{user.name}</div>
      <div>{user.age}</div> // click之后,这里不会有年龄
      <button onClick={onClick}>Click</button>
    </div>
  );
}

// true
const onClick = ()=>{
    setUser({
      ...user,
      name: 'marry',
    })
  }

2. 不可在原地址上修改对象

// false
const onClick = ()=>{
    user.name='marry';
    setUser(user);
}

因为在原地址上修改数据,react会认为user的地址没改变,所以会认为数据没改变,所以不会更新ui,因此在set的时候需要传入新的对象

3. setState是异步函数

由于setState是异步了,所以需要在setState之后执行的操作需要写在setState的回调函数里,才能保证所得到的state的值是最新的

  • 如果是类组件使用回调如下
this.state = {userName: xxx};
this.setState(
	{
		userName: xxx
	}, ()=> {
	    // 需要后续执行的部分
	    ...
	}
);
  • 如果是函数组件,即使用hook
    hook中的setState本身是不支持回调的,其中一个办法是使用uesEffect(下一期会详细探索其用法)
const [user,setUser] = useState({name:'tom', age: 18})
useEffect(()=>{
    ... // 在这里变相的实现回调
},[user]);