react源码中的hooks

news/2024/9/20 1:30:09/

今天,让我们一起深入探究 React Hook 的实现方法,以便更好的理解它。但是,它的各种神奇特性的不足是,一旦出现问题,调试非常困难,这是由于它的背后是由复杂的堆栈追踪(stack trace)支持的。因此,通过深入学习 React 的新特性:hook 系统,我们就能比较快地解决遇到的问题,甚至可以直接杜绝问题的发生。

在开始讲解之前,我先声明我不是 React 的开发者或者维护者,所以我的理解可能也并不是完全正确。我确实非常深入地研究过了 React 的 hook 系统的实现,但是无论如何我仍无法保证这就是 React 实际的工作方式。话虽如此,我还是会用 React 源代码中的证据和引用来支持我的文章,使我的论点尽可能坚实。

React hook 系统概要示意图


我们先来了解 hook 的运行机制,并要确保它一定在 React 的作用域内使用,因为如果 hook 不在正确的上下文中被调用,它就是毫无意义的,这一点你或许已经知道了。

Dispatcher

dispatcher 是一个包含了 hook 函数的共享对象。基于 ReactDOM 的渲染状态,它将会被动态的分配或者清理,并且它能够确保用户不可在 React 组件之外获取 hook(详见源码)。

在切换到正确的 Dispatcher 以渲染根组件之前,我们通过一个名为 enableHooks 的标志来启用/禁用 hook。在技术上来说,这就意味着我们可以在运行时开启或关闭 hook。React 16.6.X 版本中也有对此的实验性实现,但它实际上处于禁用状态(详见源码)

当我们完成渲染工作后,我们将 dispatcher 置空并禁止用户在 ReactDOM 的渲染周期之外使用 hook。这个机制能够保证用户不会做什么蠢事(详见源码)。

dispatcher 在每次 hook 的调用中都会被函数 resolveDispatcher() 解析。正如我之前所说,在 React 的渲染周期之外,这些都无意义了,React 将会打印出警告信息:“hook 只能在函数组件内部调用”(详见源码)。

let currentDispatcher
const dispatcherWithoutHooks = { /* ... */ }
const dispatcherWithHooks = { /* ... */ }function resolveDispatcher() {if (currentDispatcher) return currentDispatcher  throw Error("Hooks can't be called")}function useXXX(...args) {const dispatcher = resolveDispatcher()return dispatcher.useXXX(...args)
}function renderRoot() {currentDispatcher = enableHooks ? dispatcherWithHooks : dispatcherWithoutHooks  performWork()  currentDispatcher = null
}

dispatcher 实现方式概览。

现在我们简单了解了 dispatcher 的封装机制,下面继续回到本文的核心 —— hook。下面我想先给你介绍一个新的概念:

hook 队列

在 React 后台,hook 被表示为以调用顺序连接起来的节点。这样做原因是 hook 并不能简单的被创建然后丢弃。它们有一套特有的机制,也正是这些机制让它们成为 hook。一个 hook 会有数个属性,在继续学习之前,我希望你能牢记于心:

  • 它的初始状态会在初次渲染的时候被创建。
  • 它的状态可以在运行时更新。
  • React 可以在后续渲染中记住 hook 的状态。
  • React 能根据调用顺序提供给你正确的状态。
  • React 知道当前 hook 属于哪个 fiber。

另外,我们也需要重新思考看待组件状态的方式。目前,我们只把它看作一个简单的对象:

{foo: 'foo',bar: 'bar',baz: 'baz',
}

旧视角理解 React 的状态

但是当处理 hook 的时候,状态需要被看作是一个队列,每个节点都表示一个状态模型:

{memoizedState: 'foo',next: {memoizedState: 'bar',next: {memoizedState: 'bar',next: null}}
}

新视角理解 React 的状态

单个 hook 节点的结构可以在源码中查看。你将会发现,hook 还有一些附加的属性,但是弄明白 hook 是如何运行的关键在于它的 memoizedStatenext 属性。其他的属性会被 useReducer() hook 使用,可以缓存发送过的 action 和一些基本的状态,这样在某些情况下,reduction 过程还可以作为后备被重复一次:

  • baseState —— 传递给 reducer 的状态对象。
  • baseUpdate —— 最近一次创建 baseState 的已发送的 action。
  • queue —— 已发送 action 组成的队列,等待传入 reducer。

不幸的是,我还没有完全掌握 reducer 的 hook,因为我没办法复现它任何的边缘情况,所以讲述这部分就很困难。我只能说,reducer 的实现和其他部分相比显得很不一致,甚至它自己源码中的注解都声明“不确定这些是否是所需要的语义”;所以我怎么可能确定呢?!

所以我们还是回到对 hook 的讨论,在每个函数组件调用前,一个名为 prepareHooks() 的函数将先被调用,在这个函数中,当前 fiber 和 fiber 的 hook 队列中的第一个 hook 节点将被保存在全局变量中。这样,我们无论何时调用 hook 函数(useXXX()),它都能知道运行上下文。

let currentlyRenderingFiber
let workInProgressQueue
let currentHook// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123
function prepareHooks(recentFiber) {currentlyRenderingFiber = workInProgressFibercurrentHook = recentFiber.memoizedState
}// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148
function finishHooks() {currentlyRenderingFiber.memoizedState = workInProgressHookcurrentlyRenderingFiber = nullworkInProgressHook = nullcurrentHook = null
}// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:115
function resolveCurrentlyRenderingFiber() {if (currentlyRenderingFiber) return currentlyRenderingFiberthrow Error("Hooks can't be called")
}
// 源代码:https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:267
function createWorkInProgressHook() {workInProgressHook = currentHook ? cloneHook(currentHook) : createNewHook()currentHook = currentHook.nextworkInProgressHook
}function useXXX() {const fiber = resolveCurrentlyRenderingFiber()const hook = createWorkInProgressHook()// ...
}function updateFunctionComponent(recentFiber, workInProgressFiber, Component, props) {prepareHooks(recentFiber, workInProgressFiber)Component(props)finishHooks()
}

相关参考视频讲解:进入学习

hook 队列实现的概览。

一旦更新完成,一个名为 finishHooks() 的函数将会被调用,在这个函数中,hook 队列中第一个节点的引用将会被保存在已渲染 fiber 的 memoizedState 属性中。这就意味着,hook 队列和它的状态可以在外部定位到。

const ChildComponent = () => {useState('foo')useState('bar')useState('baz')return null
}const ParentComponent = () => {const childFiberRef = useRef()useEffect(() => {let hookNode = childFiberRef.current.memoizedStateassert(hookNode.memoizedState, 'foo')hookNode = hooksNode.nextassert(hookNode.memoizedState, 'bar')hookNode = hooksNode.nextassert(hookNode.memoizedState, 'baz')})return (<ChildComponent ref={childFiberRef} />)
}

从外部读取某一组件记忆的状态


下面我们来分类讨论 hook,首先从使用最广泛的开始 —— state hook:

State hook

你一定会觉得很吃惊:useState hook 在后台使用了 useReducer,并且它将 useReducer 作为预定义的 reducer(详见源码)。这意味着,useState 返回的结果实际上已经是 reducer 状态,同时也是一个 action dispatcher。请看,如下是 state hook 使用的 reducer 处理器:

function basicStateReducer(state, action) {return typeof action === 'function' ? action(state) : action;
}

state hook 的 reducer,又名基础状态 reducer。

所以正如你想象的那样,我们可以直接将新的状态传入 action dispatcher;但是你看到了吗?!我们也可以传入 action 函数给 dispatcher,这个 action 函数可以接收旧的状态并返回新的。(在本篇文章写就时,这种方法并没有记录在 React 官方文档中,很遗憾的是,它其实非常有用!)这意味着,当你向组件树发送状态设置器的时候,你可以修改父级组件的状态,同时不用将它作为另一个属性传入,例如:

const ParentComponent = () => {const [name, setName] = useState()return (<ChildComponent toUpperCase={setName} />)
}const ChildComponent = (props) => {useEffect(() => {props.toUpperCase((state) => state.toUpperCase())}, [true])return null
}

根据旧状态返回新状态。


最后,effect hook —— 它对于组件的生命周期影响很大,那么它是如何工作的呢:

effect hook

effect hook 和其他 hook 的行为有一些区别,并且它有一个附加的逻辑层,这点我在后文将会解释。在我分析源码之前,首先我希望你牢记 effect hook 的一些属性:

  • 它们在渲染时被创建,但是在浏览器绘制运行。
  • 如果给出了销毁指令,它们将在下一次绘制前被销毁。
  • 它们会按照定义的顺序被运行。

注意,我使用了“绘制”而不是“渲染”。它们是不同的,在最近的 React 会议中,我看到很多发言者错误的使用了这两个词!甚至在官方 React 文档中,也有写“在渲染生效于屏幕之后”,其实这个过程更像是“绘制”。渲染函数只是创建了 fiber 节点,但是并没有绘制任何内容。

于是就应该有另一个队列来保存这些 effect hook,并且还要能够在绘制后被定位到。通常来说,应该是 fiber 保存包含了 effect 节点的队列。每个 effect 节点都是一个不同的类型,并能在适当的状态下被定位到:

  • 在修改之前调用 getSnapshotBeforeUpdate() 实例(详见源码)。

  • 运行所有插入、更新、删除和 ref 的卸载(详见源码)。

  • 运行所有生命周期函数和 ref 回调函数。生命周期函数会在一个独立的通道中运行,所以整个组件树中所有的替换、更新、删除都会被调用。这个过程还会触发任何特定于渲染器的初始 effect hook(详见源码)。

  • useEffect() hook 调度的 effect —— 也被称为“被动 effect”,它基于这部分代码(也许我们要开始在 React 社区内使用这个术语了?!)。

hook effect 将会被保存在 fiber 一个称为 updateQueue 的属性上,每个 effect 节点都有如下的结构(详见源码):

  • tag —— 一个二进制数字,它控制了 effect 节点的行为(后文我将详细说明)。
  • create —— 绘制之后运行的回调函数。
  • destroy —— 它是 create() 返回的回调函数,将会在初始渲染运行。
  • inputs —— 一个集合,该集合中的值将会决定一个 effect 节点是否应该被销毁或者重新创建。
  • next —— 它指向下一个定义在函数组件中的 effect 节点。

除了 tag 属性,其他的属性都很简明易懂。如果你对 hook 很了解,你应该知道,React 提供了一些特殊的 effect hook:比如 useMutationEffect()useLayoutEffect()。这两个 effect hook 内部都使用了 useEffect(),实际上这就意味着它们创建了 effect hook,但是却使用了不同的 tag 属性值。

这个 tag 属性值是由二进制的值组合而成(详见源码):

const NoEffect = /*             */ 0b00000000;
const UnmountSnapshot = /*      */ 0b00000010;
const UnmountMutation = /*      */ 0b00000100;
const MountMutation = /*        */ 0b00001000;
const UnmountLayout = /*        */ 0b00010000;
const MountLayout = /*          */ 0b00100000;
const MountPassive = /*         */ 0b01000000;
const UnmountPassive = /*       */ 0b10000000;

React 支持的 hook effect 类型

这些二进制值中最常用的情景是使用管道符号(|)连接,将比特相加到单个某值上。然后我们就可以使用符号(&)检查某个 tag 属性是否能触发一个特定的行为。如果结果是非零的,就表示可以。

const effectTag = MountPassive | UnmountPassive
assert(effectTag, 0b11000000)
assert(effectTag & MountPassive, 0b10000000)

如何使用 React 的二进制设计模式的示例

这里是 React 支持的 hook effect,以及它们的 tag 属性(详见源码):

  • Default effect —— UnmountPassive | MountPassive.
  • Mutation effect —— UnmountSnapshot | MountMutation.
  • Layout effect —— UnmountMutation | MountLayout.

以及这里是 React 如何检查行为触发的(详见源码):

if ((effect.tag & unmountTag) !== NoHookEffect) {// Unmount
}
if ((effect.tag & mountTag) !== NoHookEffect) {// Mount
}

React 源码节选

所以,基于我们刚才学习的关于 effect hook 的知识,我们可以实际操作,从外部向 fiber 插入一些 effect:

function injectEffect(fiber) {const lastEffect = fiber.updateQueue.lastEffectconst destroyEffect = () => {console.log('on destroy')}const createEffect = () => {console.log('on create')return destroy}const injectedEffect = {tag: 0b11000000,next: lastEffect.next,create: createEffect,destroy: destroyEffect,inputs: [createEffect],}lastEffect.next = injectedEffect
}const ParentComponent = (<ChildComponent ref={injectEffect} />
)

这就是 hooks 了!阅读本文你最大的收获是什么?你将如何把新学到的知识应用于 React 应用中?希望看到你留下有趣的评论!


http://www.ppmy.cn/news/26751.html

相关文章

前端零基础入门-002-集成开发环境

本篇目标 了解市面上常用的前端集成开发环境&#xff08;ide&#xff09;掌握 HBuiberX 的使用&#xff1a;下载安装&#xff0c;新建项目、网页、运行网页。 内容摘要 本篇介绍了市面上流行的几款前端集成开发环境&#xff08;ide&#xff09;&#xff0c;并介绍了 Hbuilde…

揭开JavaWeb中Cookie与Session的神秘面纱

文章目录1&#xff0c;会话跟踪技术的概述2&#xff0c;Cookie2.1 Cookie的基本使用2.2 Cookie的原理分析2.3 Cookie的使用细节2.3.1 Cookie的存活时间2.3.2 Cookie存储中文3&#xff0c;Session3.1 Session的基本使用3.2 Session的原理分析3.3 Session的使用细节3.3.1 Session…

c++提高篇——queque容器

一、queque容器基本概念 Queue是一种先进先出(FIFO)的教据结构&#xff0c;它有两个出口 队列容器允许从一端新增元素&#xff0c;从另一端移除元素。队列中只有队头和队尾才可以被外界使用&#xff0c;因此队列不允许有遍历行为队列中进数据。 queque容器可以形象化为生活中…

ubantu python完整安装示例(python3.7.1演示)

文章目录前言准备源码包1.下载2.解压准备工作&#xff08;重要&#xff09;1.下载cmake(用于编译源码&#xff09;2.下载必要的Module注意事项编译安装链接并验证配置环境变量1.移除原3.5link2.更换默认python3 的版本为3.73.添加路径前言 为什么需要使用源码编译安装&#xf…

C++空指针和野指针

空指针&#xff1a;指针被赋值为空 例如&#xff1a; int* p nullptr;int* p NULL; 空指针指向的地址是00000000&#xff0c;但空指针不可以解引用 野指针&#xff1a;指针指向了不可控的位置 例如&#xff1a; 未初始化 int* p; //野指针 越界访问 int intArr[5]{0, 1, …

硬件设备 之一 详解 JTAG、SWD 接口、软 / 硬件断点、OpenOCD、J-link

JTAG 和 SWD 在嵌入式开发中可以说是随处可见&#xff0c;他们通常被用来配合 J-Link 、ULINK、ST-LINK 等仿真器在线调试嵌入式程序。此外&#xff0c;还有飞思卡尔芯片中的 Background debug mode&#xff08;BDM&#xff09; 接口&#xff0c;Atmel 芯片中的 debugWIRE &…

OAK相机跑各种yolo模型的检测帧率和深度帧率

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是助手…

使用loading动画让你的条件渲染页面更高级

前言在我们做项目的使用常常会使用条件渲染去有选择的给用户展示相关页面&#xff0c;如果渲染的数据或场景比较多比较复杂&#xff0c;那么往往需要3、4s的时间去完成&#xff0c;用户点击了之后就会陷入3、4s的空白期&#xff0c;并且这段时间屏幕是处于一种”未响应“的状态…

Spring IOC 容器 Bean 加载过程

Spring IOC 容器 Bean 加载过程 Spring 对于我们所有的类对象进行了统一抽象&#xff0c;抽象为 BeanDefinition &#xff0c;即 Bean 的定义&#xff0c;其中定义了类的全限定类名、加载机制、初始化方式、作用域等信息&#xff0c;用于对我们要自动装配的类进行生成。 Sprin…

漫画 | Python是一门烂语言?

这个电脑的主人是个程序员&#xff0c;他相继学习了C、Java、Python、Go&#xff0c; 但是似乎总是停留在Hello World的水平。 每天晚上&#xff0c;夜深人静的时候&#xff0c;这些Hello World程序都会热火朝天地聊天但是&#xff0c;这一天发生了可怕的事情随着各个Hello wor…

离散数学笔记_第一章:逻辑和证明(1)

1.1命题逻辑1.1.1 命题 1.1.2 逻辑运算符 定义1&#xff1a; 否定联结词定义2&#xff1a; 合取联结词定义3&#xff1a; 析取联结词定义4&#xff1a; 异或联结词1.1.3 条件语句 定义5&#xff1a; 条件语句定义6&#xff1a; 双条件语句1.1.1 命题 1.命题&#xff1a;是…

记一次 .NET 某医保平台 CPU 爆高分析

一&#xff1a;背景 1. 讲故事 一直在追这个系列的朋友应该能感受到&#xff0c;我给这个行业中无数的陌生人分析过各种dump&#xff0c;终于在上周有位老同学找到我&#xff0c;还是个大妹子&#xff0c;必须有求必应 &#x1f601;&#x1f601;&#x1f601;。 妹子公司的…

【结构体版】通讯录

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前是C语言学习者 ✈️专栏&#xff1a;项目 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x…

数据库期末复习总结-hnu

基本把所有知识点都罗列了&#xff08;吧&#xff09;&#xff0c;小部分知识点&#xff08;求闭包、多值依赖&#xff09;留到以后再补吧&#xff0c;先把坑留在这里。 整理不易&#xff0c;多多点赞&#xff08;orz&#xff09; 第一章 数据库系统 数据库&#xff1a;可长期…

Allegro无法打开10度走线命令的原因和解决办法

Allegro无法打开10度走线命令的原因和解决办法 做PCB设计的时候,10度走线也是较为常见的设计方式,Allegro支持10度走线,如下图 需要10度走线的时候,Options只需要勾选Route offset命令即可 但有时options处会看不到10度走线的命令,如下图

python--matplotlib(3)

前言 Matplotlib画图工具的官网地址是 http://matplotlib.org/ Python环境下实现Matlab制图功能的第三方库&#xff0c;需要numpy库的支持&#xff0c;支持用户方便设计出二维、三维数据的图形显示&#xff0c;制作的图形达到出版级的标准。 其他matplotlib文章 python--matpl…

力扣(LeetCode)418. 屏幕可显示句子的数量(2023.02.20)

给你一个 rows x cols 的屏幕和一个用 非空 的单词列表组成的句子&#xff0c;请你计算出给定句子可以在屏幕上完整显示的次数。 注意&#xff1a; 一个单词不能拆分成两行。 单词在句子中的顺序必须保持不变。 在一行中 的两个连续单词必须用一个空格符分隔。 句子中的单词总…

预告|2月25日 第四届OpenI/O 启智开发者大会昇腾人工智能应用专场邀您共启数字未来!

如今&#xff0c;人工智能早已脱离科幻小说中的虚构想象&#xff0c;成为可触及的现实&#xff0c;并渗透到我们的生活。随着人工智能的发展&#xff0c;我们正在迎来一个全新的时代——数智化时代。数据、信息和知识是这个时代的核心资源&#xff0c;而人工智能则是这些资源的…

react源码中的协调与调度

requestEventTime 其实在React执行过程中&#xff0c;会有数不清的任务要去执行&#xff0c;但是他们会有一个优先级的判定&#xff0c;假如两个事件的优先级一样&#xff0c;那么React是怎么去判定他们两谁先执行呢&#xff1f; // packages/react-reconciler/src/ReactFibe…

边玩边学,13个 Python 小游戏真有趣啊(含源码)

经常听到有朋友说&#xff0c;学习编程是一件非常枯燥无味的事情。其实&#xff0c;大家有没有认真想过&#xff0c;可能是我们的学习方法不对&#xff1f; 比方说&#xff0c;你有没有想过&#xff0c;可以通过打游戏来学编程&#xff1f; 今天我想跟大家分享几个Python小游…