关于 React Hooks 的 useState 的原理解析

3,672 阅读3分钟

1、关于 useState

  • 在学习 React 的 useState 时,肯定会遇到一个疑问,useState 到底做了什么,可以获得一个既可以读,又可以写入的数组呢?
  • 执行 setN 的时候会发生什么?n 会变吗?App() 会重新执行吗?
  • 如果 App() 会重新执行,那么 useState(0) 的时候,每次的值会有不同吗?
function App() {
  const [n, setN] = useState(0);
  const [m, setM] = useState(0);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
      <p>{m}</p>
      <p>
        <button onClick={() => setM(m + 1)}>+1</button>
      </p>
    </div>
  );
}

分析:

  • setN
    • setN 一定会修改某个数据 x ,将 n+1 存入这个 x 中
    • setN 一定会触发 重新渲染
  • useState
    • useState 肯定会从 x 读取 n 的最新值
  • x
    • 每个组件都有自己的数据 x,我们将其命名为 state

2、手写一个简易版 useState 的代替品 useMyState

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state;
const myUseState = (initState) => {
    _state = ( _state === undefined ? initState : _state);
    const setState = (newState) => {
        _state = newState
        render()
    }
    return [_state,setState]
}
const render = () => ReactDOM.render(<App />, rootElement);

function App() {
    const [n, setN] = myUseState(0);
    return (
        <div className="App">
            <p>{n}</p>
            <p>
                <button onClick={() => setN(n + 1)}>+1</button>
            </p>
        </div>
    );
}
ReactDOM.render(<App />, rootElement);
  • 但是这样的代码也会遇到问题,如果除了 n 还有个 m 呢?

3、useMyState 方法进阶版

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

let _state = [];
let index = 0;
const myUseState = (initState) => {
  let currentIndex = index;
  _state[currentIndex] = ( _state[currentIndex] === undefined ? initState : _state[currentIndex]);
  const setState = (newState) => {
    _state[currentIndex] = newState
    index = 0
    render()
  }
  index += 1
  return [_state[currentIndex],setState]
}
const render = () => ReactDOM.render(<App />, rootElement);

function App() {
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(0);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
      <p>{m}</p>
      <p>
        <button onClick={() => setM(m + 1)}>+1</button>
      </p>
    </div>
  );
}
  • 使用数组方法的缺点:
    • useState 的调用顺序非常重要
    • 第一次渲染时 n 在第一个,m 第二个,k 第三个
    • 那么第二次渲染时的调用顺序必须完全一致
    • 因此,React 不允许出现 if...else..
    • 示例:
const [n, setN] = React.useState(0);
let m, setM;
if (n % 2 === 1) {
    [m, setM] = React.useState(0);
}

4、useState 的 n

function App() {
    const [n, setN] = React.useState(0);
    const log = () => setTimeout(() => console.log(`n: ${n}`), 3000);
    return (
        <div className="App">
            <p>{n}</p>
            <p>
                <button onClick={() => setN(n + 1)}>+1</button>
                <button onClick={log}>log</button>
        </p>
        </div>
    );
}

//先点击 +1,再点 loglog 的值为 1
//先点击 log,再点 +1,log 的值为 0
  • 因为每一次 useState 渲染出 App() 里的 n 都是一个新的 n,也就是说 n=0 和 n=1,是并存的,点击 log 后,log 出的是原来等于 0 的 n,而不是 +1 后生成的新的 n、

5、useRef

function App() {
    const nRef = React.useRef(0); 
    //nRef 是一个对象 { current:0 }
    const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
    return (
        <div className="App">
            <p>{nRef.current}</p>
            <p>
                <button onClick={() => (nRef.current += 1)}>+1</button>
                <button onClick={log}>log</button>
            </p>
        </div>
    );
}
  • 这样就可以解决上面 n 的分身的问题了,但是又会出现新的问题:UI 并不会进行实时的更新,也就是说 useRef 并不会触发渲染
  • 解决方法:添加使用 useState 来触发更新(不推荐)
  • 示例:
const nRef = React.useRef(0);
const update = React.useState()[1];
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
return (
    <div className="App">
        <p>{nRef.current} 这里并不能实时更新</p>
        <p>
            <button onClick={() => ((nRef.current += 1), update(nRef.current))}>
+1
            </button>
            <button onClick={log}>log</button>
        </p>
    </div>
);

6、总结

  • 每个函数组件对应一个 React 节点

  • 每个节点保存着 state 和 index

  • useState 会读取 state[index]

  • index 由 useState 出现的顺序决定

  • setState 会修改 state ,并触发更新

  • 每次渲染,组件函数就会再次执行

  • 对应的所有 state 都会出现一个“分身”

  • 如果不希望出现这样“分身”的情况

  • 可以使用 useRef / useContext 等