# React Hooks 详解之 useState

时间:2020-9-4 作者:admin

前言

关于 react hooks 的优点,这里就不详细阐述了,大家可以去查看文档
本文的主要重点是详细解释各种 hooks 的使用以及阐述一些简单的 hooks 实现来帮助我们理解 hooks。其中第一个 hooks 也是使用频率最高最重要的 Hooks 就是useState

useState

useState 的使用

Hooks 的最大的作用就是可以让你在不编写class的情况下使用state以及其他的 React 特性。而 useState 的功能就是让你在函数式组件中使用 state。
我们看下具体使用:

import React, { useState } from "react";
import ReactDom from "react-dom";

function Counter() {
  let [count, setCount] = useState(0); // 定义state:count
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

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

render();

在上面的代码中,Counter组件是一个函数式组件,通过 useState 传入一个初始值,然后返回了 countsetCountcount在组件每次被调用时都会发生变化,setCount用于修改 count的值,每次修改后都会触发 Count 组件的重新渲染。从上面的分析中,我们可以看到 useState 主要具有以下功能:

  1. 接受一个参数作为初始化值
  2. 返回一个数组,数组的第一个值为最新的状态 count,第二个值为一个函数用于修改状态 setCount
  3. setCount 设置后需要触发重新渲染

useState 的初步实现

根据上面分析的 useState 的功能,我们初步实现一个简单的 useState。

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

实现思路如下:

  • 外部声明一个 state,用来接收初始值和更新后的值。为什么要定义在 useState 函数外面?是因为如果定义在函数里面,每次重新渲染时,都会将这个值设置为初始值,那样的话就拿不到最新的值了。
  • 内部定义一个函数,函数用来更新 state,并触发重新渲染。函数会接收一个 newState,并将其赋值给外部的 state,然后调用 render 函数,实现组件的重新渲染。
  • 返回一个数组。数组中是最新的 state 以及更新 state 的方法。

我们将自己实现的 useState 替换 react 的 useState,观察功能的实现。

import React from "react";
import ReactDom from "react-dom";

// 自定义的useState
let state;
function useState(initialState) {
  state = state || initialState;
  function setState(newState) {
    state = newState;
    render();
  }
  return [state, setState];
}

// Counter组件
function Counter() {
  let [count, setCount] = useState(0);
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

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

上面的代码能够正常实现原来的 useState 的一些功能,但是这存在一些问题,那就是如果有多个 useState 怎么办?如果保存多个 state?我们看如下代码?

function Counter() {
  let [count, setCount] = useState(0);
  let [num, setNum] = useState(0); // 共用一个state保存状态,修改第二个会导致第一个被覆盖。
  return (
    <>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>+</button>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

由于我们所有的数据都共用同一个 state,因此修改其中一个会导致另外的被覆盖。为了解决这个问题,我们必须给每一个数据提供一个变量用来保存状态,从而避免冲突。解决办法是使用一个数组来进行保存。

let state = []; // state数组用来保存数据
let index = 0; // index用来对应每一个数组项
function useState(initialState) {
  let currentIndex = index; // currentIndex用来保存当前index
  state[currentIndex] = state[currentIndex] || initialState;
  function setState(newState) {
    state[currentIndex] = newState;
    render();
  }
  index += 1; // 每次修改完成之后index加1
  return [state[currentIndex], setState];
}

function render() {
  index = 0; // render时需要重新恢复index
  ReactDom.render(<Counter />, document.getElementById("root"));
}

实现思路如下:

  • state声明成数组,每一个数据对应数组的某一项
  • 声明一个索引index,每个数据对应一个索引值
  • setState通过操作索引去设置值
  • 每调用一次useState需要将 index+=1。这样的话确保多个数据具有不同的索引值
  • 返回的值也是通过索引获取
  • 每次 render重新渲染时需要将索引 index 置为 0,确保每个数据对应的索引每次都是一致的(render 渲染组件重新渲染,组件内所有的 useState 会执行一次,每个数据又会分配一个索引,因此每次需要将 index 置为 0,确保每次的索引一致。这也是为什么 hooks 不能写在 if,while 等条件判断中)。

上面最核心的一点就是确保每个 useState 的数据对应的 index 必须一致。
也就是说:

  • 第一次渲染时,count 对应的索引值为 0,num 对应的索引值为 1。
  • 第二次渲染时,count 对应的索引值仍然为 0,num 对应的索引值为 1。
  • 第三次渲染时,count 对应的索引值仍然为 0,num 对应的索引值为 1。

为什么需要保证 useState 的数据索引一致
我们定义多个数据,使用多次 useState,观察每次数据的索引值:

import React from "react";
import ReactDom from "react-dom";

let state = [];
let index = 0;
function useState(initialState) {
  console.log("index", index); // 观察
  let currentIndex = index;
  state[currentIndex] = state[currentIndex] || initialState;
  function setState(newState) {
    state[currentIndex] = newState;
    render();
  }
  index += 1;
  return [state[currentIndex], setState];
}

function Counter() {
  let [count, setCount] = useState(0); // 第一个useState的索引index
  let [num, setNum] = useState(0); // 第二个useState的索引index
  let [count1, setCount1] = useState(0); // 第三个useState的索引index
  let [num1, setNum1] = useState(0); // 第四个useState的索引index
  return (
    <>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>+</button>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

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

render();

我们查看最终的打印顺序为:

index 0
index 1
index 2
index 3

也就是说无论什么情况下:这四个数据对应的索引始终分别为:0、1、2 和 3。
不允许出现下面这种情况。

function Counter() {
  let [count, setCount] = useState(0);
  // 在条件语句中定义useState
  if (count % 2 == 0) {
    let [count1, setCount1] = useState(0);
  }
  if (num % 2 == 0) {
    let [num1, setNum1] = useState(0);
  }
  let [num, setNum] = useState(0);

  return (
    <>
      <p>{num}</p>
      <button onClick={() => setNum(num + 1)}>+</button>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+</button>
    </>
  );
}

如果我们在条件语句中定义了 useState,这样的话会导致可能第一次只有两个 useState,对应的 count 和 num 的索引为 0 和 1。但是下一次满足条件,有了四个 useState 了,对应的 count 和 num 的索引就变成 0 和 3 了。这样的话 num 的索引值发生了变化,它在不同情况下从数组中取得的值就是不一样了,不是它自身的值,这样就会导致错误。因此,uesState 如果定义在条件语句中就会出现如下报错:

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render

总结

到目前为止,我们介绍了

  • useState 的使用
  • useState 的初步实现
  • useState 实现过程中面临的问题,以及解决办法

通过上面的介绍,可以加深我们对 useState 的理解,当然这不是官方的实现方式,只是简化后便于理解的方式。目的只是为了帮助我们更好地使用 useState。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。