一文弄懂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中。

相关推荐
喵叔哟6 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django