React Hooks之useState深入浅出

2,838 阅读5分钟

写这边文章主要是为了解决自己心中对useState用法的疑惑,希望能够帮助大家。

useState的用法其实非常简单,看官方文档非常容易掌握它的写法。但用了一段时间后,会有一些疑惑,比如:每次调用useState返回的 setXXX 方法以后触发函数重新执行,得到的新状态为什么能保留下来(明明调用 useState 传递的参数还是跟之前一样)。useState为什么不能放到 if 分支里?所以这遍文章主要回答下这2个问题。

为什么每次渲染可以保留最新的状态

可以根据自己以往的编程经验猜测出每次调用setXXX后 "状态" 肯定是保存到了某个地方,再次渲染的时候从那个地方把值拿出来,所以很容易写出了下面的代码:

// 触发页面重新渲染
function reRender() {
  ReactDOM.render(<App />, document.getElementById("root"));
}
// 内部变量保存更新后的数据
let _innerState = undefined;
// 更新状态,重新渲染组件
function _setInnerState(newState) {
    _innerState = newState;
    reRender();
}
function useState(initValue) {
  // 判断是不是第一次调用useState
  if (_innerState == null) {
    _innerState = initValue;
  }
  
  return [_innerState, _setInnerState];
}

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div className="comp">
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>点我+1</button>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <Counter />
    </div>
  );
}

上面的useState是自定义的函数,而非react hooks的useState。实现也很简单,为了保证每次执行Counter函数都能获取到最新的值,用变量 _innerState 来保存每次setCount后的值。第一次调用useState的时候 _innerState为空,所以会把传递的0赋值给 _innerState。点击按钮 值加1后,会把1复制赋值给 _innerState,这个时候组件Counter再次渲染,函数Counter重新执行,虽然useState(0)还是传递了参数0,但是由于已经不是第一次调用useState,所以传给useState的参数0被忽略,直接使用内部变量 _innerState的值(此时 _innerState 已经是1),所以渲染到页面的数据就是1而不是0

为什么useState不能放到if里面

上面的代码显然还有问题,如果在Counter函数里多次调用useState是不行的,因为只用 _innerState 保留了一个状态,如果在Counter函数中定义了多个状态(多次使用useState)会有问题,那么怎么办呢?很容易想到,用数组这个数据结构来存储状态。

// 触发页面重新渲染
function reRender() {
  ReactDOM.render(<App />, document.getElementById("root"));
}
// 用数组来存储
let _innerStateArr = [];
let _index = 0;
function _setInnerState(index, newValue) {
  _innerStateArr[index] = newValue;
  reRender();
  // 需要重置
  _index = 0;
}
const _updateFn = index => newValue => {
  _setInnerState(index, newValue);
};

function useState(initValue) {
  if (_innerStateArr[_index] == null) {
    _innerStateArr[_index] = initValue;
  }
  const cur = _innerStateArr[_index];

  return [cur, _updateFn(_index++)];
}

function Counter() {
  // 这里用useState定义了2个状态
  const [name, setName] = useState("james");
  const [count, setCount] = useState(0);
  function updateData() {
    setName("kobe.r.i.p" + (count + 1));
    setCount(count + 1);
  }
  return (
    <div className="comp">
      <p>{name}</p>
      <p>{count}</p>
      <button onClick={() => updateData()}>点我+1</button>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <Counter />
    </div>
  );
}

上面的代码阅读起来应该也没有难度。用_innerStateArr来存储每个状态。Counter函数中,useState("james")是第一次调用useState,所以把james的值存到了数组的第一个位置,即 _innerStateArr[0]='james'。useState(0)是第二次调用useState,所以把0的值存到了数组的第二个位置,即 _innerState[1]=0。所以就实现了在函数中多次调用useState的需求。

理解了上面的代码,就可以解释为什么useState不能放到 if 中的问题了。useState是根据你调用的顺序来决定是到底是使用哪个状态,如果你把useState放到了 if 中,顺序就被打乱,那么用useState得到的状态很可能是错误的,看这段代码:

function Counter() {
  const [name, setName] = useState("james");
  if (name === 'james'){
    // eslint-disable-next-line
    const [other] = useState('other')
  }
  const [count, setCount] = useState(0);
  function updateData() {
    setName("kobe.r.i.p" + (count + 1));
    setCount(count + 1);
  }
  return (
    <div className="comp">
      <p>{name}</p>
      <p>{count}</p>
      <button onClick={() => updateData()}>点我+1</button>
    </div>
  );
}

Counter函数代码基本没变,就是在 if 里增加了一个useState的调用。Counter被第一次调用时,if 分支满足条件,所以这个时候useState在Counter中调用了3次,它们的值被被赋予数组_innerStateArr对应的位置,此时count的值0被存到了数组 _innerStateArr第三个位置。而当点击按钮,Counter再次执行时,if 分支不满足条件,整个函数Counter里useState被调用了2次,useState(0)这个代码在Counter函数中是第二次被调用,既然是第二次被调用,根据调用顺序取到的就是数组 _innerStateArr第二个位置的值other,而不是1,这就是问题所在,所以不能在 if 使用useState。

总结

上面的代码解释清楚了useState的内部原理,用了2个内部变量_index和 _innerState,真实的React的useState肯定不是这么做的,它把 _innerState和 _index 放到了虚拟节点上存储,因为虚拟DOM和真实的DOM节点是一一对应的,每个函数组件又是一个虚拟DOM,这样每个组件内部就有它自己的 _innerState和 _index 了。

上面的代码只是帮助大家理解useState的工作流程,React Hooks的useState的实现代码肯定不是这样,但思路是一样的,希望对useState有同样疑惑的你有所帮助。