浅析useState

423 阅读4分钟

浅析useState

本文简单实现了一个useState,并以此分析useState的特点。

useState干了什么?

前面已经初步学习了一些比较重要的hooks,其中就包括useState。

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

我们知道,useState可以返回读写2个与数据(state)有关的接口。useState接受一个具体的值或者具有返回值的函数作为参数吗,组件初次渲染的时候会将该参数作为state的初始值。同时当调用setState时,会触发组件的Re render,并且在后续的渲染中,useState返回state始终是更新后的state。

现在就让我们一步一步地实现一个简单的useState。

实现一个简单的useState

  1. 首先我们需要返回2个接口,并且能够初始化state的值。

    function myUseState(initialValue){
        let state = initialValue
        function setState(newValue){
            state = newValue
        }
      	return [state,setState]
    }
    
    function App() {
      const [n, setN] = myUseState(1);
      console.log( n );
      return (
        <div>
          {n}
          <button onClick={() => { setN(n + 1); }}>+1</button>
        </div>
      );
    }
    

    现在,我们获取到了一个能够初始化一个state的函数,但是这个函数也仅仅只能够初始化一个state。

  2. 实现setState的功能

    实现setState的思路比较复杂,首先setState后,组件会触发再渲染,而再次渲染返回的state是更新之后的state

    let state;
    function myUseState(initialValue) {
      // 初次渲染和再次渲染返回的内容不同
      state = state === undefined ? initialValue : state;
      function setState(newValue) {
        state = newValue;
        // setState后触发再渲染
        render();
      }
      return [state, setState];
    }
    
    const rootElement = document.getElementById("root");
    const render = () => {
      ReactDOM.render(<App />, rootElement);
    };
    
    // App组件函数同上
    

    这里我们把state的声明提到了 myUseState 函数的外面。如果state的声明在myUseState 函数里面的话,那么每次再渲染时都会新声明一个state覆盖掉setState后的state。

    同时我们通过state = state === undefined ? initialValue : state进行判断组件是初次渲染还是再渲染,初次渲染时state的值为传入的值,再次渲染时state的值则是更新后的state。

    在setState中,我们新增了一个render函数,用于后续触发再渲染,更新页面UI。

  3. 解决组件中多个useState的问题

    在2.中我们基本实现了简单useState的功能。那么如果一个组件中存在多个useState怎么办呢?

    要用1个变量存储多组数据,很自然的就能想到使用对象{}或者是数组[]

    如果我们使用对象如:state = {n:0,m:0},那么我们在执行useState(initialValue)时,怎么知道我们需要的是哪个变量呢?当然这并不排除有方法能使用变量来实现,但是很明显React并不是这么做的。

    React使用的是数组进行数据的存储。所以我们需要一个新的index变量来记录数据的位置。

    let _state = [];
    let index = 0; 
    
    function myUseState(initialValue) {
      // currentIndex记录当前index,否则最后return的时候会return _state[index+1]
      const currentIndex = index;
      _state[currentIndex] =_state[currentIndex] ||  initialValue;
      index += 1;
      function setState(newValue) {
        _state[currentIndex] = newValue;
        render();
      }
      return [_state[currentIndex], setState];
    }
    
    const rootElement = document.getElementById("root");
    const render = () => {
      // 再渲染时,从头开始载入state数据
      index = 0;
      ReactDOM.render(<App />, rootElement);
    };
    

    根据上面的代码,我们不难发现,由于使用了数组进行存储数据,_state中存储的数据的顺序和组件中myUseState执行的顺序是一致的。这就很好地解释了为什么不能在 if-else 中使用useState,正是因为很有可能会打乱useState的执行顺序,导致数据存储位置的改变。

  4. 其他组件也需要使用state和index怎么办?

    其他组件也有可能需要使用到useState,又或者说如果我们使用了2次同一个组件,那么声明在外面的state和index变量名不就冲突了吗?

    因此我们需要把这两个变量名放在一个合适的地方,不会冲突的地方。

    那么每个组件有没有一个这样的地方呢?有,React组件的虚拟节点。React就是这么做的。这样更新UI时,只需要用Diff算法比对虚拟节点的不同就能够找出组件更新的地方,以此作为UI更新的依据。

    上面的过程,使用了简单的state和index作为简化,但是React实现的过程更为复杂:实际上React的节点是一个FiberNode(Fiber对象),而我们假设的state真实名称为 memorizedState,而index的实现并不是简单的依靠某个属性,而是通过链表进行实现的。

参考文章:

React Hooks 原理

阅读源码后,来讲讲React Hooks是怎么实现的