zoukankan      html  css  js  c++  java
  • 【REACT HOOKS】useState是如何保存并更新数据的?

    在项目中,我们通常会使用useState来初始化并更新数据。如下:

    function App(){
      const [num, setNum] = useState(0);
    
      function increment() {
        setTimeout(() => {
          setNum(num + 1);
        }, 1000);
      }
      
      return <button onClick={increment}>{num}</button>;
    }
    

    num初始化为0,点击按钮进行加一操作。但是在以上代码中,如果用户在一秒内点击五次按钮,最后依然会显示1。

    为什么呢?这就不得不聊聊useState是如何工作的了。

    hook如何保存数据

    在react中通过currentRenderingFiber来标识当前渲染节点,每个组件都有一个对应的fiber节点,用来保存组件的相关数据信息。
    每次函数组件渲染时,currentRenderingFiber就被赋值为当前组件对应的fiber,所以实际上hook是通过currentRenderingFiber来获取状态信息的。

    多个hook如何获取数据

    react hook允许我们在一个组件中多次使用hook。如下:

    function App(){
          const [num, setNum] = useState(0);
          const [name, setName] = useState('ashen');
          const [age, setAge] = useState(21);
    }
    

    currentRenderingFiber.memorizedState中保存一条hook对应数据的单向链表。

    const hookNum = {
          memorizedState: null,
          next: hookName
    }
    hookName.next = hookAge;
    currentRenderingFiber.memorizedState.next = hookNum;
    

    当函数组件渲染时,每执行到一个hook,就会将currentRenderingFiber.memorizedState的指针向后移一下。这也是hook的调用顺序不能改变的原因(不能再条件语句中使用hook)

    hook如何更新数据

    使用useState时,返回值数组的第二个参数是用来更新数据的,称为dispatchAction.
    每当调用dispatchAction时,都会创建一个update对象:

    const update = {
          // 更新数据
          action: action,
          // 指向下一个更新
          next: null
    }
    

    当我们多次更新state时,会形成一条环式更新链表
    在以上例子中,如果点击按钮多次进行自增操作

    function increment() {
      // 产生update1
      updateNum(num + 1);
      // 产生update2
      updateNum(num + 2);
      // 产生update3
      updateNum(num + 3);
    }
    
    update3 --next--> update1
      ^                 |
      |               update2
      |______next_______|
    
    

    这些dispatchAction产生的update对象也会保存在hook当中

    const hook = {
          memorizedState: null,
          next: null,
          baseState: null,
          baseQueue: null,
          queue: null
    }
    

    其中queue中保存了本次更新的链表,baseQueue中保存了本次更新开始时已有的链表,在计算state时,会基于baseState进行更新操作。在计算state完成后,新的state就会成为memorizedState。
    为什么更新不基于memoizedState而是baseState,是因为state的计算过程需要考虑优先级,可能有些update优先级不够被跳过。所以memoizedState并不一定和baseState相同。

    现在我们回到最开始的那个问题,在一秒内点击五次按钮进行更新,但是在这五次更新生成的时候,第一次的更新还没有进行,所以baseState并未改变,都是基于0进行更新,点击五次后依然是1。

    那么如何解决这个问题呢?实际上,更新state的函数不只可以传值,还可以传函数。

    function increment() {
        setTimeout(() => {
          setNum(num => num + 1);
        }, 1000);
      }
    

    在基于baseState和update更新state时:

    let newState = baseState;
    let firstUpdate = hook.baseQueue.next;
    let update = firstUpdate;
    
    if(typeof update === 'function'){
          newState = update.action(newState)
    }else {
        newState = action;
    }
    

    如上可见,当传入值时,由于5次action都是同一个值,所以结果为1.
    当传入函数时,每次更新都是基于上一次更新后的值进行改变,所以点击五次会变为5。

    而上例中,如果使用的是useReducer,由于第二个参数action只能是函数,所以不会产生上述问题。
    useState实际上就是内置了如下reducer的useReducer

    function basicStateReducer(state, action){
          return typeof action === 'function' ? action(state) : action;
    }
    

    可以用下图来概括本文内容

  • 相关阅读:
    跨域请求页面跳转
    media query学习笔记
    JSONP跨域数据调用
    【转】轮询、长轮询、iframe长连接、web socket
    [转]node.js学习笔记(二)
    【转】require.js学习笔记(二)
    【转】require.js学习笔记(一)
    计算星期
    确定母亲节
    计算时钟的夹角
  • 原文地址:https://www.cnblogs.com/ashen1999/p/14240191.html
Copyright © 2011-2022 走看看