一文弄懂useState的基本实现及源码解析

在日常利用React开发需求的过程当中,useState或许是绝大部分同学用的最频繁且最熟悉的Hook。但是对于这个Hook本身的实现原理,以及在它在React Runtime中的运行机制又是否能做到一清二楚呢?本文将从实现一个简易版的useState为切入点,深度剖析useState的整体运行进制,争取让大家能够彻底明白useState背后的那些事。

基本使用

useState提供了一个在Function Component中声明状态的方法,他接收一个初始值或者一个返回初始值的方法,同时以数组的形式,返回一个状态和改变状态的方法。在执行改变状态的方法后,组件会进行Rerender,并更新到最新的状态

js 复制代码
// 传入初始值
const [name, setName] = useState('Edward');

// 传入初始化函数(初始化函数只会在组件初始化的时候执行一次)
const createInitialTodos = () => {
  const initialTodos = [];
  for (let i = 0; i < 50; i++) {
    initialTodos.push({
      id: i,
      text: 'Item ' + (i + 1)
    });
  }
  return initialTodos;
}
const [todos, setTodos] = useState(createInitialTodos);

尝试实现一个useState

根据useState的基本使用,我们可以根据他的特性来实现一个简易版的useCustomState。

前置知识

在React16.8之后,针对Dom tree过大导致组件更新过程过长的问题,React引进了全新的reconcile结构,其核心就是新的Virtual Dom形式,Fiber架构。Fiber架构以链表的形式,利用child,sibling,return等属性将每个节点的父子和兄弟节点通通连接起来。

当组件树过于庞大,更新过程较长时,利用Fiber架构,可以将整个组件的ReRnder变成一个可中断可恢复的过程,不断地利用浏览器的空闲时间来完成Diff和Update的工作,从而在不阻塞页面正常刷新的前提下,完成组件的更新。

而我们的useState所缓存的组件状态,则是挂载在每个组件对应的Fiber节点里的memoizeState属性。所以在实现useState之前,我们需要先简单的构造一个Fiber节点。另外,在同一个组件中,我们可能会同时使用多个useState方法维护多个状态,所以我们还需要一个变量去维护当前组件所使用到的所有的hook。

js 复制代码
const fiber = {
    type: "FunctionComponent",
    memoizedState: null
}
const workInProgressHook = null; 

初始化阶段

然后,才开始正式实现我们的useCustomState方法。这个方法可以接受一个初始值或者返回初始值的函数,并且返回一个状态和改变状态的方法,同时,这个方法的调用会更新状态和重新绘制组件。

js 复制代码
// 表示组件是初次加载还是更新状态
let firstMount = true; 
const _renderApp = () => {}

const useCustomState = (initState) => {
    let state = typeof initState === 'function' ? initState() : initState;
    let hook;
    // 组件首次加载时,构造一个hook对象挂载在fiber上
    if(firstMount) {
       hook = {
         memoizedState: state,
         next: null
       }
       // 创建更新队列,这个队列是更新状态值的时候用的,会保存所有的更新行为
       let queue = {
           pending: null,
           lanes: null,
           next: null
       }
       hook.queue = queue
       if(!fiber.memoizedState) {
         fiber.memoizedState = workInProgressHook = hook;  
       }
       // 利用next属性将多个hook对象串连起来,既保存了hook的状态,也保存了hooks的顺序
       workInProgressHook = workInProgressHook.next = hook;
    }
    // 返回当前hook的状态值,触发更新状态和组件rerender的函数
    return [hook.memoizedState, dispatchAction.bind(null, hook)]
}
const dispatchAction(hook, action) = () => {}

通过以上代码,我们实现了,在初始化组件和hook时,我们对组件内使用到的所有hook,都通过workInProgressHook,以链表的形式,保存了所有的hook状态以及hook的调用顺序,这对我们后面的工作至关重要。

实现setState方法

下一步我们来实现,useState中返回的更改状态的方法(setState),该方法可以接收一个新的状态,或者是一个以前一个状态为参数并返回最新状态的函数。通过调用setState,我们可以触发组件的Rerender,并将状态更新到我们传入的值。

js 复制代码
const dispatchAction(hook, action) = () => {
    // 每次触发setState时,我们都新建一个update对象
    let update = {
        action,
        next: null
    }
    // 将setState的入参保存在hook.queue.pending中
    // 同时,当同一个hook的setState操作使用了多次时,我们需要通过hook.queue.pending.next属性串连起每一个setState操作。
    const pending = hook.queue.pending;
    if(pending === null) {
        update.next = update;
    } else {
        update.next = pending.next;
        pending.next = udpate;
    }
    queue.pending = update;
    
    // 虚构一个Render组件方法
    _renderApp()
}

通过上述代码,我们可以很清晰的看到,在触发setState之后,我们做了两件事,

  • 每次触发更新时,我们都会新建一个update对象,这个对象保存了当前更新的相关新,包含了最新的状态值action。
  • 使用环形链表,将所有的update对象串联起来,并保存在hook.queue.pending上。

使用环形链表的意义:

整个环形链表变量我们叫它 update,使得 queue.pending = update 那么此时 queue.pending 的最近一次更新,就是 update,最早的一次更新是 update.next

这样就快速定位到最早的一次更新了

如果是单链表,想找到最早的 一次更新,需要一层一层往下找

环形链表一次就找到了

更新阶段

当用户触发了setState的操作时,我们的组件会进行到更新阶段,整个函数组件会重新调用useState方法,但是此时的处理就不能跟初始化那样了,我们必须返回setState之后最新的数据状态。

js 复制代码
const useCustomState = (initState) => {
    let state = typeof initState === 'function' ? initState() : initState;
    let hook;
    // 组件首次加载时,构造一个hook对象挂载在fiber上
    if(firstMount) {
       hook = {
         memoizedState: state,
         next: null
       }
       // 创建更新队列,这个队列是更新状态值的时候用的,会保存所有的更新行为
       let queue = {
           pending: null,
           lanes: null,
           next: null
       }
       hook.queue = queue
       if(!fiber.memoizedState) {
         fiber.memoizedState = workInProgressHook = hook;  
       }
       // 利用next属性将多个hook对象串连起来,既保存了hook的状态,也保存了hooks的顺序
       workInProgressHook = workInProgressHook.next = hook;
    } else {
      // 更新阶段时,先取出当前操作的hook
      hook = workInProgressHook
      let baseState = hook.memoizedState
      // 如果存在更新操作,则遍历所有的更新操作,将其遍历完
      if(hook.queue.pending) {
          let firstAction = hook.queue.pending.next;
          do{
           baseState = typeof firstAction.action === 'function' ? firstAction.action(baseState) : firstAction.action
           firstAction = firstAction.next
          } while(firstAction !== hook.queue.pending)
          // 遍历之后,取最终值更新到hook对象里的memoizedState中
          hook.memoizedState = baseState;
          hook.queue.pending = null;
      }
      // 进行下一个hook的操作
      workInProgressHook = workInProgressHook.next
      
    }
    // 返回当前hook的状态值,触发更新状态和组件rerender的函数
    return [hook.memoizedState, dispatchAction.bind(null, hook)]
}

在上面的代码中,其实我们的做的事情非常简单,在更新流程里,判断当前hook是否存在对应的更新操作,如果有的话,可遍历所有的更新操作,取最终值,并更新到hook中。

从代码 workInProgressHook = workInProgressHook.next 中可以得知,在更新每个hook的状态时,我们是严格按照初始化时维护的hook链表指向顺序,依次进行的。这也直接回答了,为什么hook要定义在顶部,而不可以放在诸如条件判断的代码里了。倘若hook放在了一个条件判断的代码里,初始化组件和更新的时候,hook的数量发生了变化,那么指针的指向就会发生错乱,更新机制就会出现问题。

通过上述的代码,我们已经基本完成了一个简易版的useState了,通过这些代码,我们就可以更好的去理解React中对useState的实现了。

源码解析

在React的里,useState的处理是发生在Reconcile的过程中的renderWithHooks方法中。 这个方法将useState的执行区分成了两个阶段,对应了两个方法。分别为mountStateupdateState

mountState

在mountState中,react做的事情其实跟我们之前写的代码大同小异,创建了一个hook对象,并挂载一个记录更新操作的queue属性,同时创建一个更新状态的方法dispatch,并最终返回。

在dispatch方法中,react同样创建了一个update对象,并在创建后,对当前节点以及当前节点的祖先节点进行更新标记,最终利用scheduleUpdateOnFiber,去调度下一次的渲染。

updateState

当触发setState时,会触发组件的重渲染,此时useState会走到另外一个阶段,也就是updateState方法。

在updateState方法里,调用了updateReducer,updateReducer里会调用updateReducerImpl方法,这个方法主要是根据更新的优先级,去取出最终要更新的state,并更新到hook.memoizedState中。

相关推荐
IT_陈寒16 小时前
Python 3.12性能优化实战:5个让你的代码提速30%的新特性
前端·人工智能·后端
赛博切图仔16 小时前
「从零到一」我用 Node BFF 手撸一个 Vue3 SSR 项目(附源码)
前端·javascript·vue.js
爱写程序的小高16 小时前
npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree
前端·npm·node.js
loonggg16 小时前
竖屏,其实是程序员的一个集体误解
前端·后端·程序员
程序员爱钓鱼16 小时前
Node.js 编程实战:测试与调试 - 单元测试与集成测试
前端·后端·node.js
码界奇点16 小时前
基于Vue.js与Element UI的后台管理系统设计与实现
前端·vue.js·ui·毕业设计·源代码管理
时光少年16 小时前
Android KeyEvent传递与焦点拦截
前端
踢球的打工仔16 小时前
typescript-引用和const常量
前端·javascript·typescript
OEC小胖胖16 小时前
03|从 `ensureRootIsScheduled` 到 `commitRoot`:React 工作循环(WorkLoop)全景
前端·react.js·前端框架
时光少年16 小时前
ExoPlayer MediaCodec视频解码Buffer模式GPU渲染加速
前端