react 中 hook 的原理

动机

  • 组件之间很难重用有状态逻辑
  • 复杂的组件变得难以理解
  • 类 class 混淆了人和机器
  • 更符合 FP 的理解, React 组件本身的定位就是函数,一个吃进数据、吐出 UI 的函数

useState 底层依然是 useReducer

一、hook 底层原理


hook 原理解释 1

1. 状态管理的底层实现:

在React中,状态的管理是通过Fiber节点来完成的。Fiber节点是React内部的数据结构,用于表示组件树的节点。每个Fiber节点都有一个memoizedState字段,用于存储组件的状态。

当你在函数组件中调用useState时,React会创建一个新的Fiber节点,并在该节点的memoizedState字段中保存当前状态值。同时,React还会创建一个dispatch函数,用于更新状态。这个dispatch函数会在状态更新时被调用,并触发组件的重新渲染。

2. 副作用操作的底层实现:

副作用操作是通过Fiber节点的effect链表来管理的。每个Fiber节点都有一个firstEffect和lastEffect字段,它们分别指向第一个和最后一个副作用节点。副作用节点中保存了副作用操作的信息,例如要执行的函数、依赖项等。

当你在函数组件中调用useEffect时,React会创建一个新的副作用节点,并将其添加到当前Fiber节点的effect链表中。在组件渲染完成后,React会依次执行effect链表中的副作用操作。同时,React还支持在副作用操作中返回一个清理函数,用于在组件卸载前执行清理操作。

3. 上下文传递的底层实现:

上下文传递是通过React的上下文机制来实现的。当你调用useContext时,React会在组件树中向上查找最近的上下文提供者,并返回该上下文的值。

React内部会维护一个全局的上下文栈,在组件渲染过程中,React会根据当前组件的上下文提供者,将对应的上下文值压入栈中。当你调用useContext时,React会从栈顶获取最近的上下文值,并返回给组件。

4. useRef的底层实现:

useRef返回的是一个包含current属性的对象。这个对象在组件渲染过程中是不会改变的,因此可以用来存储组件中的持久性数据。在底层实现中,React会为每个组件创建一个对应的Hooks链表,useRef返回的对象会被添加到这个链表中,并在每次渲染时保持不变。

5. useMemo和useCallback的底层实现:

useMemo和useCallback都是基于memoization(记忆化)技术来优化性能的。在底层实现中,React会比较依赖项数组的前后两次值是否相等,如果相等则表示依赖项没有发生变化,可以直接使用上次计算的结果,从而避免重复计算。

总的来说,React Hook的底层实现是基于Fiber节点和Hooks链表的数据结构来完成的。通过这些数据结构,React能够管理组件的状态、副作用操作等,并在需要时触发相应的更新和渲染。



HOOK原理解释 2

React Hooks 的底层源码实现相当复杂,涉及到 React Fiber 架构、调度器、并发模式等多个方面。不过,我可以为你概述一下 Hooks 在 React 源码中的大致实现步骤和关键概念。

  1. Fiber 架构: Hooks 的实现紧密依赖于 React 的 Fiber 架构。Fiber 是 React 16 引入的新协调引擎,它允许 React 将渲染工作拆分成多个小任务,从而实现更好的性能优化和并发控制。
  2. 创建 Fiber 节点: 当你调用一个函数组件时,React 会为该组件创建一个 Fiber 节点。这个节点是组件在 Fiber 树中的表示,它包含了组件的类型、props、状态等信息。
  3. Hooks 数组: 每个函数组件都有一个与之关联的 Hooks 数组。这个数组用于存储组件中所有 Hooks 的状态和回调函数。当你调用一个 Hook 函数(如 useState 或 useEffect)时,React 会向这个数组中添加一个新的元素。
  4. Hook 对象的创建与更新: 当你调用 useState 或其他 Hook 时,React 实际上是在创建一个 Hook 对象,并将其添加到当前组件的 Hooks 数组中。这个对象包含了 Hook 的状态、更新函数以及任何相关的回调函数(如 useEffect 的清除函数)。在后续渲染中,React 会根据 Hooks 数组中的顺序和状态来更新这些对象。
  5. 状态更新: 当你调用状态更新函数(如 setState)时,React 会将一个新的更新任务添加到调度器中。调度器会根据优先级和并发策略来决定何时执行这个任务。当任务被执行时,React 会重新渲染组件,并根据 Hooks 数组中的状态来更新 UI。
  6. 副作用处理: useEffect Hook 的实现涉及到副作用队列的处理。当你调用 useEffect 时,React 会将传入的函数和依赖项数组保存到一个副作用对象中,并将这个对象添加到当前组件的副作用队列中。在组件渲染完成后,React 会依次执行队列中的副作用函数。
  7. 规则和约束的强制执行: 为了确保 Hooks 的正确使用,React 在源码级别强制执行了一些规则和约束。例如,Hooks 必须在函数组件的顶层调用,不能在循环、条件或嵌套函数中调用。React 通过在源码中添加检查和警告来确保这些规则被遵守。

需要注意的是,以上只是一个大致的概述,实际的源码实现要复杂得多。如果你对 React Hooks 的底层实现感兴趣,建议直接阅读 React 的源码或查阅相关的深入解析文章。

深度解析 hook 原理

1. 初始化

当一个函数组件被调用时,React 需要准备一个新的 Hooks 数组(实际上是 Fiber 节点上的一个字段)来存储该组件实例的 Hooks 信息。

2. 调用 Hooks 函数

每当你调用一个 Hook 函数(如 useState, useEffect 等),React 会做以下几件事:

  • 检查当前 Hook 是否已经在 Hooks 数组中存在(通过比较 Hook 的类型和依赖来确定)。
  • 如果存在,它会使用已存储的状态或副作用信息。
  • 如果不存在,它会创建一个新的 Hook 对象并将其添加到数组中。

每个 Hook 对象包含以下信息:

  • Hook 的类型(例如,是 useState 还是 useEffect)。
  • Hook 的状态(对于 useState 来说)。
  • 更新状态的函数(对于 useState 来说)。
  • 副作用函数和相关依赖(对于 useEffect 来说)。

3. 处理状态和副作用

  • 对于 ****useState:当状态需要更新时,React 会调度一个新的渲染任务。在下一次渲染过程中,新的状态值将从 Hook 对象中被读取,并传递给组件。
  • 对于 ****useEffect:在每次渲染后,React 会查看组件的副作用队列,并根据每个副作用对象的依赖数组来决定是否需要运行该副作用。如果需要运行,副作用函数会被执行;如果不需要,React 会跳过它并可能执行清理函数(如果提供的话)。

4. 遵守规则和约束

React 使用内部机制来确保 Hooks 的规则和约束被遵守:

  • Hooks 必须按相同的顺序被调用。这是通过维护一个内部的 "current hook" 索引来实现的,该索引在每次渲染期间递增。
  • Hooks 不能在条件、循环或嵌套函数中调用。这是因为这样做会破坏上述的 "current hook" 索引机制,导致状态和副作用的错误关联。

5. 并发模式和更新调度

在 React Fiber 架构中,组件的更新被视为一种任务,这些任务可以被打断和延迟,以便更高效地利用主线程的时间片。Hooks 的实现与这一架构紧密相连,意味着当状态更新时,相关的渲染任务会被添加到任务队列中,并根据优先级进行调度。

6. 优化和内存管理

为了优化性能和减少内存消耗,React 会尽可能地重用 Hooks 对象和相关的数据结构。这意味着在连续的渲染过程中,相同的 Hook 对象可能会被更新而不是被重新创建。此外,React 还使用了一系列的启发式算法和数据结构来避免不必要的计算和渲染工作。

总结

React Hooks 的底层实现是一个复杂且高度优化的系统,它充分利用了 JavaScript 的闭包和函数式编程特性来提供强大的状态管理和副作用处理功能。通过深入理解 Hooks 的内部工作原理,你可以更加有效地使用这一特性来构建高性能的 React 应用程序。然而,要完全理解 React 的源码实现需要深厚的 JavaScript 和计算机科学知识,并且需要投入大量的时间和精力来阅读和分析源码。

二、hook新理解 hookinit

一句话总结 hooks 函数组件解决没有 state ,生命周期,逻辑不能复用的一种技术方案

带着问题去理解

  • 1 在无状态组件每一次函数上下文执行的时候,react用什么方式记录了hooks的状态?
  • 2 多个react-hooks用什么来记录每一个hooks的顺序的 ? 换个问法!为什么不能条件语句中,声明hooks? hooks声明为什么在组件的最顶部?
  • 3 function函数组件中的useState,和 class类组件 setState有什么区别?
  • 4 react 是怎么捕获到hooks的执行上下文,是在函数组件内部的?
  • 5 useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值?
  • 6 useMemo是怎么对值做缓存的?如何应用它优化性能?
  • 7 为什么两次传入useState的值相同,函数组件不更新?
  • ...

在class状态中,通过一个实例化的class,去维护组件中的各种状态;

但是在function组件中,没有一个状态去保存这些信息,每一次函数上下文执行,所有变量,常量都重新声明,执行完毕,再被垃圾机制回收。

所以如上,无论setTimeout执行多少次,都是在当前函数上下文执行,此时num = 0不会变,之后setNumber执行,函数组件重新执行之后,num才变化。

所以, 对于class组件,我们只需要实例化一次,实例中保存了组件的state等状态。对于每一次更新只需要调用render方法就可以。

但是在function组件中,每一次更新都是一次新的函数执行,为了保存一些状态,执行一些副作用钩子,react-hooks应运而生,去帮助记录组件的状态,处理一些额外的副作用

看来对于第一次渲染组件,和更新组件,react-hooks采用了两套Api,本文的第二部分和第三部分,将重点两者的联系

本文将重点围绕四个中重点hooks展开,分别是负责组件更新的useState,负责执行副作用useEffect ,负责保存数据的useRef,负责缓存优化的useMemo, 至于useCallback,useReducer,useLayoutEffect原理和那四个重点hooks比较相近,就不一一解释了。

javascript 复制代码
import React , { useEffect , useState , useRef , useMemo  } from 'react'
function Index(){
    const [ number , setNumber ] = useState(0)
    const DivDemo = useMemo(() => <div> hello , i am useMemo </div>,[])
    const curRef  = useRef(null)
    useEffect(()=>{
       console.log(curRef.current)
    },[])
    return <div ref={ curRef } >
        hello,world { number } 
        { DivDemo }
        <button onClick={() => setNumber(number+1) } >number++</button>
     </div>
}

mountWorkInProgressHook

在组件初始化的时候,每一次hooks执行,如useState(),useRef(),都会调用mountWorkInProgressHook,

mountWorkInProgressHook到底做了写什么,让我们一起来分析一下:

react-reconciler/src/ReactFiberHooks.js -> mountWorkInProgressHook

csharp 复制代码
function mountWorkInProgressHook() {
  const hook: Hook = {
    memoizedState: null,  // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

mountWorkInProgressHook这个函数做的事情很简单,首先每次执行一个hooks函数,都产生一个hook对象,里面保存了当前hook信息,然后将每个hooks以链表形式串联起来,并赋值给workInProgress的memoizedState。也就证实了上述所说的,函数组件用memoizedState存放hooks链表。

至于hook对象中都保留了那些信息?我这里先分别介绍一下 :

memoizedState: useState中 保存 state 信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和 deps | useRef 中保存的是 ref 对象。

baseQueue : usestate和useReducer中 保存最新的更新队列。

baseState : usestate和useReducer中,一次更新中 ,产生的最新state值。

queue : 保存待更新队列 pendingQueue ,更新函数 dispatch 等信息。

next: 指向下一个 hooks对象。

那么当我们函数组件执行之后,四个hooks和workInProgress将是如图的关系。

知道每个hooks关系之后,我们应该理解了,为什么不能条件语句中,声明hooks。

我们用一幅图表示如果在条件语句中声明会出现什么情况发生。

如果我们将上述demo其中的一个 useRef 放入条件语句中,

csharp 复制代码
 let curRef  = null
 if(isFisrt){
  curRef = useRef(null)
 }

因为一旦在条件语句中声明 hooks ,在下一次函数组件更新, hooks 链表结构,将会被破坏, current 树的 memoizedState 缓存 hooks 信息,和当前 workInProgress 不一致,如果涉及到读取 state 等操作,就会发生异常。

上述介绍了 hooks通过什么来证明唯一性的,答案 ,通过hooks链表顺序。和为什么不能在条件语句中,声明hooks,接下来我们按照四个方向,分别介绍初始化的时候发生了什么?

初始化useState -> mountState

ini 复制代码
function mountState(
  initialState
){
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    // 如果 useState 第一个参数为函数,执行函数得到state
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    pending: null,  // 带更新的
    dispatch: null, // 负责更新函数
    lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,
    lastRenderedState: initialState, // 最后一次得到的 state
  });

  const dispatch = (queue.dispatch = (dispatchAction.bind( // 负责更新的函数
    null,
    currentlyRenderingFiber,
    queue,
  )))
  return [hook.memoizedState, dispatch];
}

mountState到底做了些什么,首先会得到初始化的state,将它赋值给mountWorkInProgressHook产生的hook对象的 memoizedState和baseState属性,然后创建一个queue对象,里面保存了负责更新的信息。

这里先说一下,在无状态组件中,useState和useReducer触发函数更新的方法都是dispatchAction,useState,可以看成一个简化版的useReducer,至于dispatchAction怎么更新state,更新组件的,我们接着往下研究dispatchAction。

在研究之前 我们先要弄明白 dispatchAction 是什么?

作者:我不是外星人

链接:juejin.cn/post/694486...

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

scss 复制代码
const [ number , setNumber ] = useState(0)

dispatchAction 就是 setNumber , dispatchAction 第一个参数和第二个参数,已经被bind给改成currentlyRenderingFiber和 queue,我们传入的参数是第三个参数action

dispatchAction 无状态组件更新机制

ini 复制代码
function dispatchAction(fiber, queue, action) {

  // 计算 expirationTime 过程略过。
  /* 创建一个update */
  const update= {
    expirationTime,
    suspenseConfig,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  }
  /* 把创建的update */
  const pending = queue.pending;
  if (pending === null) {  // 证明第一次更新
    update.next = update;
  } else { // 不是第一次更新
    update.next = pending.next;
    pending.next = update;
  }
  
  queue.pending = update;
  const alternate = fiber.alternate;
  /* 判断当前是否在渲染阶段 */
  if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
    didScheduleRenderPhaseUpdate = true;
    update.expirationTime = renderExpirationTime;
    currentlyRenderingFiber.expirationTime = renderExpirationTime;
  } else { /* 当前函数组件对应fiber没有处于调和渲染阶段 ,那么获取最新state , 执行更新 */
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
          const currentState = queue.lastRenderedState; /* 上一次的state */
          const eagerState = lastRenderedReducer(currentState, action); /**/
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) { 
            return
          }
        } 
      }
    }
    scheduleUpdateOnFiber(fiber, expirationTime);
  }
}

无论是类组件调用setState,还是函数组件的dispatchAction ,都会产生一个 update对象,里面记录了此次更新的信息,然后将此update放入待更新的pending队列中,dispatchAction第二步就是判断当前函数组件的fiber对象是否处于渲染阶段,如果处于渲染阶段,那么不需要我们在更新当前函数组件,只需要更新一下当前update的expirationTime即可

如果当前fiber没有处于更新阶段。那么通过调用lastRenderedReducer获取最新的state,和上一次的currentState,进行浅比较,如果相等,那么就退出,这就证实了为什么useState,两次值相等的时候,组件不渲染的原因了,这个机制和Component模式下的setState有一定的区别

如果两次state不相等,那么调用scheduleUpdateOnFiber调度渲染当前fiber,scheduleUpdateOnFiber是react渲染更新的主要函数。

我们把初始化 mountState 无状态组件更新机制 讲明白了,接下来看一下其他的hooks初始化做了些什么操作?

初始化useEffect -> mountEffect

上述讲到了无状态组件中fiber对象memoizedState保存当前的hooks形成的链表。那么updateQueue保存了什么信息呢,我们会在接下来探索useEffect过程中找到答案。 当我们调用useEffect的时候,在组件第一次渲染的时候会调用mountEffect方法,这个方法到底做了些什么?

ini 复制代码
function mountEffect(
  create,
  deps,
) {
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag, 
    create, // useEffect 第一次参数,就是副作用函数
    undefined,
    nextDeps, // useEffect 第二次参数,deps
  );
}

每个hooks初始化都会创建一个hook对象,然后将hook的memoizedState保存当前effect hook信息。

有两个 memoizedState 大家千万别混淆了,我这里再友情提示一遍

  • workInProgress / current 树上的 memoizedState 保存的是当前函数组件每个hooks形成的链表。

  • 每个hooks上的memoizedState 保存了当前hooks信息,不同种类的hooks的memoizedState内容不同。上述的方法最后执行了一个pushEffect,我们一起看看pushEffect做了些什么?

pushEffect 创建effect对象,挂载updateQueue

ini 复制代码
function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null,
  };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue
  if (componentUpdateQueue === null) { // 如果是第一个 useEffect
    componentUpdateQueue = {  lastEffect: null  }
    currentlyRenderingFiber.updateQueue = componentUpdateQueue
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {  // 存在多个effect
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

首先创建一个 effect ,判断组件如果第一次渲染,那么创建 componentUpdateQueue ,就是workInProgress的updateQueue。然后将effect放入updateQueue中。

假如一个组件这么写 那么

scss 复制代码
useEffect(()=>{
    console.log(1)
},[ props.a ])
useEffect(()=>{
    console.log(2)
},[])
useEffect(()=>{
    console.log(3)
},[])

最后workInProgress.updateQueue会以这样的形式保存:

拓展:effectList

effect list 可以理解为是一个存储 effectTag 副作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect ,和最后一个节点 lastEffect。 React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。 在 commit 阶段,React 拿到 effect list 数据后,通过遍历 effect list,并根据每一个 effect 节点的 effectTag 类型,执行每个effect,从而对相应的 DOM 树执行更改。

初始化useMemo -> mountMemo

不知道大家是否把 useMemo 想象的过于复杂了,实际相比其他 useState , useEffect等,它的逻辑实际简单的很。

ini 复制代码
function mountMemo(nextCreate,deps){
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

初始化useMemo,就是创建一个hook,然后执行useMemo的第一个参数,得到需要缓存的值,然后将值和deps记录下来,赋值给当前hook的memoizedState。整体上并没有复杂的逻辑。

初始化useRef -> mountRef

对于useRef初始化处理,似乎更是简单,我们一起来看一下:

ini 复制代码
function mountRef(initialValue) {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

mountRef初始化很简单, 创建一个ref对象, 对象的current 属性来保存初始化的值,最后用memoizedState保存ref,完成整个操作。

mounted 阶段 hooks 总结

react-hooks做的事情,在一个函数组件第一次渲染执行上下文过程中,每个react-hooks执行,都会产生一个hook对象,并形成链表结构,绑定在workInProgress的memoizedState 属性上,然后react-hooks上的状态,绑定在当前hooks对象的memoizedState 属性上。对于effect副作用钩子,会绑定在workInProgress.updateQueue上,等到commit阶段,dom树构建完成,在执行每个 effect 副作用钩子

三、hooks更新阶段

对于更新阶段,说明上一次 workInProgress 树已经赋值给了 current 树。存放hooks信息的memoizedState,此时已经存在current树上,react对于hooks的处理逻辑和fiber树逻辑类似。

对于一次函数组件更新,当再次执行hooks函数的时候,比如 useState(0) ,首先要从current的hooks中找到与当前workInProgressHook,对应的currentHooks,然后复制一份currentHooks给workInProgressHook,接下来hooks函数执行的时候,把最新的状态更新到workInProgressHook,保证hooks状态不丢失。

所以函数组件每次更新,每一次react-hooks函数执行,都需要有一个函数去做上面的操作,这个函数就是updateWorkInProgressHook,我们接下来一起看这个updateWorkInProgressHook。

updateWorkInProgressHook

ini 复制代码
function updateWorkInProgressHook() {
  let nextCurrentHook;
  if (currentHook === null) {  /* 如果 currentHook = null 证明它是第一个hooks */
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else { /* 不是第一个hooks,那么指向下一个 hooks */
    nextCurrentHook = currentHook.next;
  }
  let nextWorkInProgressHook
  if (workInProgressHook === null) {  //第一次执行hooks
    // 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else { 
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) { 
      /* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;
    const newHook = { //创建一个新的hook
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
    if (workInProgressHook === null) { // 如果是第一个hooks
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else { // 重新更新 hook
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这一段的逻辑大致是这样的:

  • 首先如果是第一次执行hooks函数,那么从current树上取出memoizedState ,也就是旧的hooks。
  • 然后声明变量nextWorkInProgressHook,这里应该值得注意,正常情况下,一次renderWithHooks执行,workInProgress上的memoizedState会被置空,hooks函数顺序执行,nextWorkInProgressHook应该一直为null,那么什么情况下nextWorkInProgressHook不为null,也就是当一次renderWithHooks执行过程中,执行了多次函数组件,也就是在renderWithHooks中这段逻辑。
ini 复制代码
  if (workInProgress.expirationTime === renderExpirationTime) { 
       // ....这里的逻辑我们先放一放
  }

这里面的逻辑,实际就是判定,如果当前函数组件执行后,当前函数组件的还是处于渲染优先级,说明函数组件又有了新的更新任务,那么循坏执行函数组件。这就造成了上述的,nextWorkInProgressHook不为 null 的情况。(多次更新会组合在一起成为类似递归循环)

  • 最后复制current的hooks,把它赋值给workInProgressHook,用于更新新的一轮hooks状态

接下来我们看一下四个种类的hooks,在一次组件更新中,分别做了那些操作。

1、updateState

ini 复制代码
function updateReducer(
  reducer,
  initialArg,
  init,
){
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current = currentHook;
  let baseQueue = current.baseQueue;
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
     // 这里省略... 第一步:将 pending  queue 合并到 basequeue
  }
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) { //优先级不足
        const clone  = {
          expirationTime: update.expirationTime,
          ...
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
      } else {  //此更新确实具有足够的优先级。
        if (newBaseQueueLast !== null) {
          const clone= {
            expirationTime: Sync, 
             ...
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        /* 得到新的 state */
        newState = reducer(newState, action);
      }
      update = update.next;
    } while (update !== null && update !== first);
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    }
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  const dispatch = queue.dispatch
  return [hook.memoizedState, dispatch];
}

首先将上一次更新的pending queue 合并到 basequeue,为什么要这么做,比如我们再一次点击事件中这么写,

typescript 复制代码
function Index(){
   const [ number ,setNumber ] = useState(0)
   const handerClick = ()=>{
    //    setNumber(1)
    //    setNumber(2)
    //    setNumber(3)
       setNumber(state=>state+1)
       // 获取上次 state = 1 
       setNumber(state=>state+1)
       // 获取上次 state = 2
       setNumber(state=>state+1)
   }
   console.log(number) // 3 
   return <div>
       <div>{ number }</div>
       <button onClick={ ()=> handerClick() } >点击</button>
   </div>
}
// 点击按钮, 打印 3

三次setNumber产生的update会暂且放入pending queue,在下一次函数组件执行时候,三次 update被合并到 baseQueue。结构如下图

接下来会把当前useState或是useReduer对应的hooks上的baseState和baseQueue更新到最新的状态。会循环baseQueue的update,复制一份update,更新 expirationTime,对于有足够优先级的update(上述三个setNumber产生的update都具有足够的优先级),我们要获取最新的state状态。,会一次执行useState上的每一个action。得到最新的state。

更新state

  • 问题一:这里不是执行最后一个action不就可以了嘛?

答案: 原因很简单,上面说了 useState逻辑和useReducer差不多。如果第一个参数是一个函数,会引用上一次 update产生的 state, 所以需要循环调用,每一个 update reducer,如果setNumber(2)是这种情况,那么只用更新值,如果是setNumber(state=>state+1),那么传入上一次的 state 得到最新state。

  • 问题二:什么情况下会有优先级不足的情况(updateExpirationTime < renderExpirationTime)?

答案: 这种情况,一般会发生在,当我们调用setNumber时候,调用scheduleUpdateOnFiber渲染当前组件时,又产生了一次新的更新,所以把最终执行reducer更新state任务交给下一次更新。

updateEffect

ini 复制代码
function updateEffect(create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookEffectTag, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber.effectTag |= fiberEffectTag
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    destroy,
    nextDeps,
  );
}

useEffect 做的事很简单,判断两次deps 相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect,这里注意 effect的标签,hookEffectTag,如果不相等,那么更新 effect ,并且赋值给hook.memoizedState,这里标签是 HookHasEffect | hookEffectTag,然后在commit阶段,react会通过标签来判断,是否执行当前的 effect 函数。

updateMemo

ini 复制代码
function updateMemo(
  nextCreate,
  deps,
) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
  const prevState = hook.memoizedState; 
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1]; // 之前保存的 deps 值
      if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

在组件更新过程中,我们执行useMemo函数,做的事情实际很简单,就是判断两次 deps是否相等,如果不想等,证明依赖项发生改变,那么执行 useMemo的第一个函数,得到新的值,然后重新赋值给hook.memoizedState,如果相等 证明没有依赖项改变,那么直接获取缓存的值。

不过这里有一点,值得注意,nextCreate()执行,如果里面引用了usestate等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep 数组中。每一次dep改变,重新执行,就不会出现问题了。

温馨小提示: 有很多同学说 useMemo 怎么用,到底什么场景用,用了会不会起到反作用,通过对源码原理解析,我可以明确的说,基本上可以放心使用,说白了就是可以定制化缓存,存值取值而已。

updateRef

javascript 复制代码
function updateRef(initialValue){
  const hook = updateWorkInProgressHook()
  return hook.memoizedState
}

函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象,所以解释了useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值。

一次点击事件更新

react-hooks三部曲另外两部

react进阶系列

react源码系列

开源项目系列

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-1 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试