React 浅析 useState 原理、优化

746 阅读3分钟

简易实现 useState

let _state

const myUseState = initialValue => {
  _state = _state === undefined ? initialValue : _state
  const setState = newValue => {
    _state = newValue
    render()
  }
  return [_state, setState]
}

const render = () => {
  ReactDom.render(<App/>, document.getElementById('root'))
}

一开始将 initialValue 赋值给全局变量 _state,setState 时,将 newValue 赋值给 _state 然后重新 rander

这样似乎实现了,但是有一个大问题,如果一个组件用了两个 useState 怎么办

我想了两种思路

  • 把_state 做成一个对象 ?

但是这个思路很快就行不通,因为 useState 并不知道 key 要设置成什么

  • 把 _state 做成数组 ?
let _state = []
let index = 0
const myUseState = initialValue => {
  const currentIndex = index
  _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]
  const setState = newValue => {
    _state[currentIndex] = newValue
    render()
  }
  index += 1
  return [_state[currentIndex], setState]
}

const render = () => {
  index = 0
  // 精髓,每次重新渲染时,要把index 进行重置,不然index 会错乱
  ReactDom.render(<App/>, document.getElementById('root'))
}

把 _state 做成数组,每个 useState 的数组拥有自己的 currentIndex ,当 setState 时,使用自己的 currentIndex 对数据进行修改

这个有可能就是 useState 的实现思路,所有 react 的 useState 不允许一下的代码发生

const App = () => {
  const [n, setN] = useState(0)
  let M , setM
  if (n % 2 === 1) {
  // 在调整判断中赋值
    [M, setM] = useState(0)
  }
  return (
    <div>
      {n}
      <button onClick={() => {setN(n + 1)}}>+1</button>
      {M}
      <button onClick={() => {setM(n + 1)}}>+1</button>
    </div>
  )
}

这样使用,会报如下错误,错误的大致意思时,在每个组件中,必须以完全相同的顺序调用 useState,不然会导致 bug

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)

上面的代码基本将 useState 的功能实现了,但是还有一个问题,App 用了 _state 和 index,那其他组件用什么?解决办法是,将 _state 和 index 放在对应的虚拟节点对象上

总结

  • 每个函数组件对应一个 React 节点
  • 每个节点保持着 state 和 index
  • useState 会读取 state[index]
  • index 由 useState 出现的顺序决定
  • setState 会修改 state,并触发更新

看一个有趣的题目

n 的 分身

const App = () => {
  const [n, setN] = myUseState(0)
  const log  = () => {
    setTimeout(() => {
      console.log(n)
    }, 2000)
  }
  return (
    <div>
      {n}
      <button onClick={() => {setN(n + 1)}}>+1</button>
      <button onClick={log}>log</button>
    </div>
  )
}

当点击+1 时,n + 1,当点击 log 时,3秒后 log 出 n

现在有趣的来了,当 n 等于 0 时

先点+1,后点log,3秒后 log 出 1

先点 log ,后点 +1 ,3秒后 log 出 0

这是因为 setN 并不会立刻改变 n ,而是重新渲染后再改变n,所有先点 log 时,log 读取的是旧的 n, 我们每次修改state都不会修改当前的state,而是会重新生成一个新的state,旧的state和新的state有可能同时存在,之后旧的state会被垃圾回收掉

看看这个时序图,就明白了

第一条就是我们正常操作先点+1再点log,它会先触发setN,setN执行就出触发render,这时候就会第二次渲染App,生成一个新的n,n是1,再点击三秒后就会log(1);
第二条就是我们先点log,它会三秒后打印出第一次这个n,这时候的n的值还是0,然后点击+1触发render,第二次渲染App,生成一个新的n=1,然后三秒钟到打印出第一次的n也就是0

补更一个知识点

setState 会帮我们合并属性吗?

const App = () => {
  const [user, setUser] = useState({name: 'Jacky', age: 18})
  const onClick = () => {
    setUser({age: 20})
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <h1>{user.age}</h1>
      <button onClick={onClick}>click</button>
    </div>
  )
}

在 setUset({age: 23}) 后,name 属性会自动合并吗? 不会

所以我们要改成这样,先把旧属性拷贝进去,再覆盖原来的属性

const onClick = () => {
    setUset({
      ...user,
      age: 22
    })
  }

结论是:setState 不可以局部更新

总结

  • 每次重新渲染,组件函数就会执行
  • setState 不可以局部更新
  • 对应的所有 state 都会出现 “分身”
  • 如果你不希望出现分身,可以使用 useRef / useContext 等