从源码角度解析:react hook为啥不能放入条件语句中

在我们刚开始学习react时,react官方文档就提示我们react hook必须在函数组件顶层使用,不能在条件语句或者循环结构中使用。

之所以会有这样的规定,这和react hook的实现原理脱离不了关系。

本文就从源码的角度,帮助大家快速理解其中的原理。

虽然是以源码角度来解析,但本文并不会展示过多的源码,旨在于尽量简洁的说明其中的道理。

1,hooks的加载

首先准备一个函数组件案例:

js 复制代码
export default function MyFun(props) {
  // hook加载
  const [count, setCount] = useState(1)
  useEffect(() => {
    console.log('useEffect DOM渲染之后执行')
  }, [])
  function handleClick() {
    setCount(2)
  }
  return (
    <div className='MyFun'>
      <div>MyFun组件</div>
      <div>state: {count}</div>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

我们都知道在react应用中,函数组件加载的一个重点就是:会调用一次我们定义的函数,也就是案例中的MyFun

而在函数调用的过程中,就会遇到我们定义的hooks,react内部就会开始执行hooks的加载处理。

当然首先我们得知道,react组件的加载处于Fiber Reconciler协调流程之中,这个流程的主要作用就是创建FiberTree【虚拟DOM树】,Fiber即react中的虚拟DOM,每一个函数组件都会创建与之对应的Fiber节点,对于Fiber节点你只需要理解为一个JS对象,它有很多属性存储着与组件相关的信息,而与hook相关的部分内容就存储在fiber.memoizedState属性之中。

每一个hook在首次加载时,都会执行一个mountWorkInProgressHook方法:

比如useState加载时:

js 复制代码
function mountState(initialState) {
    const hook = mountWorkInProgressHook();
    ...
}

这个方法内部会为我们在组件中定义的hook创建一个对应的hook对象:

js 复制代码
// hook对象
const hook: Hook = {
  memoizedState: null,
  baseState: null,
  baseQueue: null,
  queue: null,
  next: null, // 指向下一个hook对象
};

这个hook对象的属性存储着我们组件中对应hook的相关信息,但是在这里我们只需要关心它的next属性即可:

在加载第一个hookuseState时,就会将第一个hook对象直接赋值给当前函数组件对应的Fiber节点的memoizedState属性。

js 复制代码
fiber.memoizedState = hook;

所以此时函数组件Fiber节点的memoizedState属性指向为:

加载第二个hookuseEffect时,就会将上一个hooknext属性指向当前新建的hook对象。

所以此时函数组件Fiber节点的memoizedState属性指向为:

ini 复制代码
useState => hook1
useEffect => hook2

在函数组件在加载完成后,fiber.memoizedState属性形成了一个这样的单向链表。

2,hooks的更新

来到函数组件的更新过程:函数组件的更新同样会调用一次MyFun,在这个过程中react内部就会开始执行hooks的更新处理。

同理每一个hook在更新时,都会执行一个updateWorkInProgressHook方法:

js 复制代码
function updateState(initialState) {
    const hook = updateWorkInProgressHook();
    ...
}

重点来了: updateWorkInProgressHook方法内部会引用current.memoizedState属性的内容。

函数组件更新时,current代表旧的节点内容,其实就是新旧虚拟DOM概念,current变成了旧的虚拟DOM。

所以hook更新时会引用函数组件加载阶段 就已形成的hook链表,会按照这个链表顺序来取出对应的原hook对象,利用原hook信息生成新的hook对象参与计算或者更新。

这里取出顺序为:首先从current.memoizedState取出第一个hook对象,后面就是从hook对象的next属性依次取出下一个更新的hook对象,hooks的更新就可以按照顺序复用上一次的相关信息。

hooks的更新会按照函数组件加载阶段就已经固定的hook链表顺序,这就是hook必须置于函数组件顶层使用的根本原因

如果我们将hook置于条件或者循环之中,在更新阶段就会出现无法与原来的hook链表相匹配的问题,将会导致渲染出现异常。

比如我们在条件语句之中新增一个hook

js 复制代码
export default function MyFun(props) {
  const [count, setCount] = useState(1)
  // 新增一个hook
  if (count === 2) {
    const [status, setStatus] = useState(false)
  }
  useEffect(() => {
    console.log('useEffect DOM渲染之后执行')
  }, [])
  function handleClick() {
    setCount(2)
  }
  return (
    <div className='MyFun'>
      <div>MyFun组件</div>
      <div>state: {count}</div>
      <button onClick={handleClick}>更新</button>
    </div>
  )
}

此时我们设置count === 2时才会执行新增的hookcount默认为1,所以在函数组件加载阶段时不会触发新增的hook。即函数加载阶段形成的hook链表中只有两个hook对象,但是当我们一点击更新按钮,变量条件得到满足,组件更新时就会出现三个hook,导致新增的useState复用了原来的第二个useEffecthook对象信息,这必然会导致渲染出错。即使新增的hook 为useEffect,也同样会导致渲染出错,因为在第三个hook更新时,找不到hook链表对应的hook对象,此时react同样会抛出错误:

js 复制代码
if (nextCurrentHook === null) {
  throw new Error('Rendered more hooks than during the previous render.');
}

如果我们设置count === 1就执行新增的hook,则函数组件在加载阶段就会向我们发出一个error提示:

sql 复制代码
React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render

循环结构也一样的道理,循环的次数变了或者循环结构中有条件语句,都是相同的原理。

结束语

本文从源码的角度,以尽量简洁的语句向大家解释了react hook必须于函数组件顶层使用的原因,想了解FiberTree创建过程或者函数组件具体的加载过程可以阅读本系列的其他文章。

相关推荐
byzh_rc7 分钟前
[微机原理与系统设计-从入门到入土] 微型计算机基础
开发语言·javascript·ecmascript
m0_471199637 分钟前
【小程序】订单数据缓存 以及针对海量库存数据的 懒加载+数据分片 的具体实现方式
前端·vue.js·小程序
编程大师哥9 分钟前
Java web
java·开发语言·前端
A小码哥10 分钟前
Vibe Coding 提示词优化的四个实战策略
前端
Murrays10 分钟前
【React】01 初识 React
前端·javascript·react.js
大喜xi13 分钟前
ReactNative 使用百分比宽度时,aspectRatio 在某些情况下无法正确推断出高度,导致图片高度为 0,从而无法显示
前端
helloCat14 分钟前
你的前端代码应该怎么写
前端·javascript·架构
电商API_1800790524714 分钟前
大麦网API实战指南:关键字搜索与详情数据获取全解析
java·大数据·前端·人工智能·spring·网络爬虫
康一夏16 分钟前
CSS盒模型(Box Model) 原理
前端·css
web前端12316 分钟前
React Hooks 介绍与实践要点
前端·react.js