实现一个 Mini React:核心功能详解 - 重构useState的实现 (1)

146 阅读5分钟

xdm,又要到饭了,又更新代码了!

总结一下上一篇完成的内容,

  1. 实现了mini fiber 的第3版,实现了fiber渲染真dom的过程。

有兴趣的可以点这里查看fiber 的迷你版实现 (3)

经过几版fiber的原理讲解,相信你对fiber至少有了一定的认识。有了对fiber的理解,现在就开始讲解fiber和hook的关联以及hook底层的原理。

还记得在最前面的几个章节讲解了 useState / useEffect 实现,比如 useState的实现,

//let state
let hooks = []
let currentHook = 0
function useState(initial) {
    //state = state || initial
    const hookIndex = currentHook
    hooks[hookIndex] = hooks[hookIndex] || initial
  
    const setState = newState=>{ 
      
      // 这里会更新组件内部状态值,
      //state = newState
      hooks[hookIndex] = newState
      
      // 也会更新ui
      renderApp()
    }
    
    const state = hooks[hookIndex]
    // 这里的index的改变控制了对应的多个usestate 的状态值
    currentHook++
    
    return [state, setState]
}

刚开始我们没有讲解fiber的实现,所以这个的实现没有fiber的参与。

组件ui的呈现其实就是一系列更新之后最后状态的呈现,用户会知道一个最后状态的值。那么真dom的更新会在commit阶段,这个前几章讲解fiber树已经解释了,所以hook的更新会体现在fiber上面。

我们复习一下在之前的fiber数据结构上面的2个属性, hooks, currentHook。 currentHook 是一个索引,由于组件可能存在多个hooks,所以currentHook 维护了当前属于哪个hook。hooks就是一个数组,搜集所有的hooks, 比如 useState / useEffect,这也需要了解可能存在多个相同的hook,比如一个组件有多个useEffect / useState。

class FiberNode {
  constructor(type, props, parent = null, sibling = null, child = null) {
    this.type = type; // 组件类型
    this.props = props; // 组件属性
    this.parent = parent; // 父节点
    this.sibling = sibling; // 兄弟节点
    this.child = child; // 子节点
    this.effectTag = null; // 标记需要执行的操作(如更新)
    this.state = null; // 组件的状态(useState)
    this.hooks = []; // 组件的 Hook 列表
    this.currentHook = 0; // 当前 Hook 的索引
    this.alternate = null; // 交替的 Fiber
    this.stateNode = null; // 真实 DOM 节点
  }
}

接下来, 重构useState,

function useState(initial) {
    // workInProgress , 当前fiber
   
    
    const oldHook = workInProgress.alternate && workInProgress.alternate.hooks && workInProgress.alternate.hooks[workInProgress.currentHook]
    
    const hook = {
        state: oldHook.state || initial
        queue: []
    }
    
    
    const actions = oldHook ? oldHook.queue : []
    actions.forEach(action => {
        hook.state = typeof action === 'function'action(hook.state) : action
    })
    
    if (oldHook)
        oldHook.queue = []
       
    const setState = action => {
        if (typeof action === 'function') {
            hook.queue.push(prevState=>action(prevState))
        } else hook.queue.push(action)
        
        requestHostCallback(()=>{
           workLoop() // fiber树的更新
        })
       
       
    }
    
    workInProgress.hooks.push(hook)
    workInProgress.currentHook++
    
    return [hook.state, setState]
}
  1. 首先尝试从当前的fiber树找出对应的fiber(当前的fiber树指的当前已经渲染的ui依赖的fiber树, react 还有另外一棵树,即 work in progress fiber树,即下一次ui更新依赖的fiber树)的获取值以及其所有的hooks(这里即是复用,尝试从当前ui渲染的fiber树找出需要的数据)
  2. 第一次进入,状态值即是initial (第一次运行workInProgress.alternate is null)
  3. 接着运行一遍所有当前fiber的hooks,确认状态值的一致性。(这里为什么需要重新运行一次所有的hooks呢?因为组件可能多次重新渲染(更新),useState 会重复运行,所以需要保持每次渲染的状态值的一致以及最新)
  4. 接着删除所有的hooks,重新收集
  5. 每当用户调用setState,正如你所知道的,setState 可以传入一个函数,第一个参数即是上次的状态值。 或者传入一个常数,即这个常数会覆盖上一次的状态值。这里进行了判断。接着把这个用户传入的新状态值(函数或者常数)压入队列
  6. 最后调用 requestHostCallback(workLoop) 进行fiber树的更新/构建,这个函数已经在前几篇里面实现了,
  7. 那么当第二次进入获取 oldHook,这个oldHook 就是上面6步完成的,即每次useState都缓存了本次的操作比如状态值以及hooks以便下次进行操作
  8. 最后更新currentHook的索引值

阅读了上面的实现,现在可以知道 setState 的更新属于异步。我们整理一下,

在 React 中,setState(或在函数组件中通过 useState 提供的 setState)的主要作用是更新组件的状态,并触发组件的重新渲染。在 Fiber 架构中,这一过程涉及以下几个步骤:

  1. 状态更新的调度

    • 当调用 setState 时,状态更新被添加到一个队列中。
    • 这些状态更新(动作)将被后续的渲染过程处理 (批量)。
  2. Fiber 树的构建和协调(Reconciliation)

    • React 会构建一个新的 Fiber 树,表示更新后的组件结构。
    • 新的 Fiber 树与旧的 Fiber 树进行比较,以确定哪些部分需要更新、插入或删除。
  3. 异步渲染和提交

    • React 使用调度机制(如 requestIdleCallback / Message channel 或更高级的调度策略)来异步处理 Fiber 树的构建和协调。
    • 一旦 Fiber 树的构建完成,React 会将变更提交到(commit 阶段)真实的 DOM 上,完成 UI 的更新。

这一章节讲解了更新了之前的useState函数实现,从源码角度的理解可以清楚的知道useState 属于异步更新。

但,这里的实现还没有完美,即现在每次的调用setState 都会重新渲染。这不是我们想要的,我们想要的多次更新merge只更新最新的那次数据,不是现在的每次都会单独更新一次。

下一篇将继续改进useState完成批量更新的相关实现

如果这样的长度/强度你觉得可以接受,觉得有帮助,可以继续阅读下一篇,实现一个 Mini React:核心功能详解 - 重构useState的实现 (2)。

如果文章对你有帮助,请点个赞支持一下!

啥也不是,散会。