likes
comments
collection
share

从零开始,解密 useState

作者站长头像
站长
· 阅读数 10

本文将通过一系列简单的示例来逐步演示如何实现 useState,以帮助你快速理解 useState 的原理。

useState 用法

首先,在 React 中,useState 的用法如下:

function App() {
  const [n, setN] = React.useState(0)
  return (
    <div className="App">
      <p>{n}</p>
      <div>
        <button onClick={() => setN(n + 1)}>+1</button>
      </div>
    </div>
  )
}

分析上面的代码,你会发现以下特点:

  1. setN 一定会改变数据,将 n + 1 重新存入某个地方。
  2. setN 会触发 <App/> 重新渲染(re-render)。
  3. useState 会从缓存的数据中获取最新的 n。

实现 useState

根据以上原理,我们可以实现一个简单 useState。

function useState(initialValue) {
  let state = initialValue
  function setState(newState) {
    state = newState
    render()
  }
}

function render() {
  return ReactDOM.render(<App/>, rootElement)
}

function App() {
  const [n, setN] = useState(0)
  return (
    <div className="App">
      <p>{n}</p>
      <div>
        <button onClick={() => setN(n + 1)}>+1</button>
      </div>
    </div>
  )
}

ReactDOM.render(App/>, rootElement)

但,如果你运行上面的代码,会发现点击按钮 n 没有任何变化。这说明我们对 state 的实现有误。

state 不能存储在 useState 函数中,因为 useState 会将 state 重置。我们需要一个不会被 useState 重置的变量,也就是一个外部变量,俗称闭包。再次改良我们的 useState,如下:

let _state

function useState(initialValue) {
  _state = _state === undefined ? initialValue : _state
  function setState(newState) {
    _state = newState
    render()
  }
  return [_state, setState]
}

上面的代码,我们将 _state 提取为一个外部变量,这样每次 useState 就不会重置 state。至此,我们已经实现了一个非常简易的 useState。

但目前的实现仍然存在问题,例如我们同时使用两个 useState:

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

let _state

function useState(initialValue) {
  _state = _state === undefined ? initialValue : _state
  function setState(newState) {
    _state = newState
    render()
  }
  return [_state, setState]
}

function render() {
  return ReactDOM.render(<App/>, rootElement)
}

function App() {
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  return (
    <div className="App">
      <p>{a}</p>
      <div>
        <button onClick={() => setA(a + 1)}>+1</button>
      </div>
      <p>{b}</p>
      <div>
        <button onClick={() => setB(b + 1)}>+1</button>
      </div>
    </div>
  )
}

ReactDOM.render(<App/>, rootElement)

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

首先想到的一种方案是:将 _state 改为一个 hash 对象,将值与每个 useState 一一对应,如 _state = { a: 0, b: 0 },但我们并不知道变量 a、b 的属性名是什么,所以这个方法有缺陷。

一种更好的方案是:把 _state 存储在一个数组中,如 _state = [0, 0],这样可以保证数据一一对应,再次改良我们的 useState,如下:

let _state = []
let index = 0

function useState(initialValue) {
  const currentIndex = index
  index += 1
  _state[currentIndex] = _state[currentIndex] || initialValue
  function setState(newState) {
    _state[currentIndex] = newState
    render()
  }
  return [_state[currentIndex], setState]
}

function render() {
  index = 0
  return ReactDOM.render(<App/>, rootElement)
}

// App 内容
function App() {
  ...
}

ReactDOM.render(<App/>, rootElement)

再次运行,两个 useState 之间就不会相互影响了。

至此,我们已经粗略实现了一个单个组件可用的 useState。但它仍然有一个和 React.useState 共同存在的缺点,例如运行如下代码:

fucntion App() {
  const [a, setA] = React.useState(0)
  let b, setB
  if (n % 2 === 1) {
    [b, setB] = React.useState(0)
  }

  return (
    <div className="App">
      <p>{a}</p>
      <div>
        <button onClick={() => setA(a + 1)}>+1</button>
      </div>
      <p>{b}</p>
      <div>
        <button onClick={() => setB(b + 1)}>+1</button>
      </div>
    </div>
  )
}

运行上面的代码,会导致报错,提示内容大概为:多次渲染组件 useState 的结果不一致。

这是因为:在 React 中,如果第一次渲染时 a 是第一个,b 是第二个,可能还有第三个 c...。那么,第二次渲染时,必须保证 useState 的顺序与第一次是完全一致的。所以 React 中不允许出现上面的代码。

最后,上面的代码只支持单个组件,如果有多个组件怎么办?

  • 答案是每个组件都创建一个 _state 和 index。

但又有一个问题,这么多 _state 和 index 存放在哪里?重名了怎么办?

  • 答案是可以存放在每个组件对应的虚拟 DOM 对象上。

总结

  1. 每个 React Component 函数都会对应一个 React 虚拟节点(对象)。
  2. 我们可以给每个节点保存独立的 state 和 index。
  3. useState 会读取对应的 state(state[index])。
  4. 其中 index 由 useState 出现的顺序决定。
  5. setState 会修改 state,并且触发更新。