ReactDOM 隐藏特性详解

目录
  • 前言
  • React DevTools 的原理
    • 渲染阶段
    • FiberRoot/FiberNode
    • memoizedState 与 React Hooks
  • 实践:突破 useDebugValue 的限制
    • useDebugValueAnywhere 的实现
    • 特定的 devtools
  • 总结

前言

有过 React 经验的开发者可能都使用过 React DevTools。

DevTools 提供了丰富的能力:展示组件树,组件的 props 与组件中 hook 的值。

React DevTools 是如何检测当前网页是否使用 React 以及是如何获取组件相关的众多数据呢?

React DevTools 的原理

打开 ReactDOM 代码时,用 devtools 为关键字搜索,你会发现许多与 React DevTools 相关的代码。

function injectInternals(internals) {
  if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
    // No DevTools
    return false;
  }
  var hook = __REACT_DEVTOOLS_GLOBAL_HOOK__;
  try {
    rendererID = hook.inject(internals); // We have successfully injected, so now it is safe to set up hooks.
    injectedHook = hook;
  } catch (err) {
    // ...
  } // DevTools exists
}

在浏览器控制台输入 __REACT_DEVTOOLS_GLOBAL_HOOK__ 详细看一下这个对象。

这个对象十分复杂,以下的几个方法倒是很值得关注。

onCommitFiberRoot

onCommitFiberUnmount

onPostCommitFiberRoot

渲染阶段

从名称来看,上面这几个方法与 ReactDOM 的渲染密切相关。

ReactDOM 在特定的阶段会调用这些的方法,比如:onCommitFiberRoot

function onCommitRoot(root, priorityLevel) {
  if (injectedHook && typeof injectedHook.onCommitFiberRoot === 'function') {
    try {
      // ...
      injectedHook.onCommitFiberRoot(rendererID, root, priorityLevel, didError);
    } catch (err) {}
  }
}

正是借助 __REACT_DEVTOOLS_GLOBAL_HOOK__,React DevTools 便与 ReactDOM 建立起了联系,从而拥有获取组件众多信息的能力。

FiberRoot/FiberNode

在新的 React 架构下,会先把 Virtual DOM 转成 FiberNode,然后再渲染 FiberNode。

onCommitFiberRoot 等方法中的传递的数据正是 FiberNode。

FiberNode 的结构是比较复杂的,可以简化为如下的结构:

interface ReactFiberRootNode {
  current: ReactFiberNode;
  // ...
}
interface ReactFiberNode {
  tag: number;
  stateNode: null | HTMLElement; // dom 节点
  memoizedProps?: Record<string, any>; // props
  memoizedState: ClassComponentState | HookLinkedQueue | null; // hooks
  child?: ReactFiberNode;
  sibling?: ReactFiberNode;
  return: ReactFiberNode; // parent
  // ...
}

从上面的结构可以看出,FiberNode 包含了非常多与组件相关的信息。

stateNode 为组件对应真实的 DOM 节点,memoizedProps 为组件的 props

当组件为函数式组件时,tag 为 0,memoizedState 保存了组件中的 hooks 信息。

当组件为类组件时,tag 为 1,memoizedState 则是组件的 state

如下图所示,FiberNode 节点形成一个链表结构。

只要能找到组件对应的 FiberNode,我们便可以做到在运行期间以无侵入的方法获取组件的众多信息。比如:通过 FiberNode 进行遍历,实现 findNativeNodesForFiber 方法,用以查找其对应的真实 DOM 节点。

function findNativeNodesForFiber(node?: ReactFiberNode) {
  // ...
  // 先遍历 child
  const { child } = node;
  collectStateNode();
  // 再遍历所有的 sibling
  let current = child?.sibling;
  while (current) {
    collectStateNode();
    current = current.sibling;
  }
  // ...
}

React DevTools 中审查元素功能正是基于类似的原理去实现。

memoizedState 与 React Hooks

上文中提到当组件为函数式组件时,memoizedState 保存了 React Hooks 相关的信息。与 FiberNode 类似,React Hooks 也形成一个链表。

export interface HookLinkedQueue {
  memoizedState: any; // 渲染时的值
  next: HookLinkedQueue | null;
  // ...
}

React Hook 将其数据都保存在 memoizedState 上。比如对于 useRef 来说,ref.current 值就是 memoizedState。类似的,可以实现 inspectSomeHooksOfFiber 来获取组件内使用特定 hook 中保存的值。

function inspectRefHooksOfFiber(node: ReactFiberNode) {
  let current: HookLinkedQueue | null = node.memoizedState;
  while (current) {
    retrieveValue(current);
    current = current.next;
  }
}

实践:突破 useDebugValue 的限制

useDebugValue 是 React 内置的一个 hook,用以在 React DevTools 中显示自定义 hook 的标签。它的限制是只能在 hook 中使用。借助前文介绍的知识点,我们可以实现一个增加版的 useDebugValue,你可以像普通的 hook 一样来使用它,没有其他限制。

useDebugValueAnywhere 的实现

useDebugValueAnywhere 实现比较简单,name 表明数据的名称,用一个特殊的 ref 对象来存储 debug 相关的数据。

export function useDebugValueAnywhere(name: string, data: any) {
  const ref = useRef({
    [DebugHookKey]: {
      name,
      data,
    },
  });
  // ...
}

特定的 devtools

参考 React DevTools 的逻辑,在 __REACT_DEVTOOLS_GLOBAL_HOOK__ 中注入我们的 onCommitFiberRoot 方法,从而确保 ReactDOM 每次渲染时,能获取最新的 FiberNode。

currentHook.onCommitFiberRoot = function (...args) {
  handleCommitFiberRoot(...args); // 注入
  oldOnCommitFiberRoot.apply(this, args);
};

接下来便是对 FiberNode 进行遍历。在遍历的过程中,检查每个 FiberNode 中 memoizedState 链表,检测组件的 hooks 中是否用到了 useDebugValueAnywhere

如果存在,就将值 FiberNode 与 hook 中的值保存起来。

{
  visitFiberNode(node?: ReactFiberNode) {
    if (!node) return;
    this.inspectFiber(node);
    this.visitFiberNode(node.child);
    let { sibling } = node;
    while (sibling) {
      this.visitFiberNode(sibling);
      sibling = sibling.sibling;
    }
  }
}

剩下的工作就是考虑以何种形式去展示收集到的 debug 信息。在 PC 端可以直接输出数据到控制台;在移动端 vConsole 使用较多,那么就可以基于 vConsole 开发一个插件,实现一个极简版的 React DevTools,专门用以展示这些信息。

完整的代码

总结

本文剖析了 React DevTools 的原理,介绍隐藏在 ReactDOM 中的一些特性,并带领大家熟悉了一下 React Fiber 架构。基于上述原理,可以开发一个增加版的 useDebugValue

由于本文介绍的特性并非公开的 API,没有兼容性。当 React/ReactDOM 版本升级时,还需要再做适配,因此只适合用来开发 DevTools 之类的工具,不推荐业务开发使用。

以上就是ReactDOM 隐藏特性详解的详细内容,更多关于ReactDOM 隐藏特性的资料请关注我们其它相关文章!

(0)

相关推荐

  • react-router-dom v6 通过outlet实现keepAlive 功能的实现

    本文主要介绍了react-router-dom v6 通过outlet实现keepAlive 功能的实现,具体如下: keepAlive代码: import React, { useRef, useEffect, useReducer, useMemo, memo } from 'react' import { TransitionGroup, CSSTransition } from 'react-transition-group' import { useLocation } from 'r

  • react-router-dom V6的配置使用实践

    目录 一.关于书写方面 二.路由的嵌套方面优化 三.关于路由的灵活配置化 四.关于路由鉴权方面 最近在搭建PC项目的React框架,涉及到React Router,开发同学有时就需要去尝试点新的东西,在开发过程中才不会枯燥的,基于这个理念推动,就在搭建的时候用V6的版本开始了(虽然只是个新版本,也不算啥新东西)...... V6的版本相对于V5,做了很多的优化,有书写方面的.路由的嵌套.路由配置化.鉴权方面等等,下面就简单的介绍下如何使用 一.关于书写方面 路由注册的时候由的Switch改为了R

  • React前端DOM常见Hook封装示例下

    目录 引言 useFullscreen useHover useDocumentVisibility 引言 本文是深入浅出 ahooks 源码系列文章的第十五篇,这个系列的目标主要有以下几点: 加深对 React hooks 的理解. 学习如何抽象自定义 hooks.构建属于自己的 React hooks 工具库. 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择. 上文指路:React前端DOM常见Hook封装示例上 本篇接着针对关于 DOM 的各个 Hook 封装进行解读. useF

  • 详解react-router-dom v6版本基本使用介绍

    目录 Routes Route Navigate NavLink useRoutes 嵌套路由 路由传参 编程式导航 Routes 代替Switch组件,不会向下匹配 用来包裹Route Route 必须被Routes组件包裹 component属性变成element caseSensitive 路径大小写敏感属性,默认是不敏感(访问/about = /ABOUT) index 与path属性是互斥的,index表示为当前路由的根 可以用作layout组件,不写element属性,写了要与 Ou

  • react-router-domV6版本的路由和嵌套路由写法详解

    目录 1-单级路由 2-嵌套路由(about路径进行嵌套) ReactRouterv6使用路由嵌套和重定向 1 - 单级路由 <NavLink to="/home">Home</NavLink> <NavLink to="/about">about</NavLink> <Routes>   <Route path='/home' element={<Home/>}/>   <R

  • react-router-dom入门使用教程(路由的模糊匹配与严格匹配)

    目录 模糊匹配 开启严格匹配 Redirect的使用 嵌套路由使用 模糊匹配 <!-- 编写路由链接 --> <NavLink to="/home/a/b">Home</NavLink> <!-- 注册路由 --> <Switch> <Route path="/home" component={Home} /> </Switch> 点击Home,进入路径/home/a/b,此时模糊匹

  • ReactDOM 隐藏特性详解

    目录 前言 React DevTools 的原理 渲染阶段 FiberRoot/FiberNode memoizedState 与 React Hooks 实践:突破 useDebugValue 的限制 useDebugValueAnywhere 的实现 特定的 devtools 总结 前言 有过 React 经验的开发者可能都使用过 React DevTools. DevTools 提供了丰富的能力:展示组件树,组件的 props 与组件中 hook 的值. React DevTools 是如

  • python学习字符串驻留与常量折叠隐藏特性详解

    下面是Python字符串的一些微妙的特性,绝对会让你大吃一惊. 案例一: 案例二: 案例三: 很好理解, 对吧? 说明: 这些行为是由于 Cpython 在编译优化时, 某些情况下会尝试使用已经存在的不可变对象而不是每次都创建一个新对象. (这种行为被称作字符串的驻留[string interning]) 发生驻留之后, 许多变量可能指向内存中的相同字符串对象. (从而节省内存) 在上面的代码中, 字符串是隐式驻留的. 何时发生隐式驻留则取决于具体的实现. 这里有一些方法可以用来猜测字符串是否会

  • Android5.0新特性详解之全新的动画

    在Material Design设计中,为用户与app交互反馈他们的动作行为和提供了视觉上的连贯性.Material主题为控件和Activity的过渡提供了一些默认的动画,在android L上,允许自定义这些动画: Touch feedback 触摸反馈 Circular Reveal 圆形展示 Curved motion 曲线运动 View state changes 视图状态变化 Vector Drawables 矢量图动画 Activity transitions 活动转场 触摸反馈 触

  • React团队测试并发特性详解

    目录 引言 遇到的困境 1. 如何表达渲染结果? 2. 如何测试并发环境? React的应对策略 实现一个渲染器 如何测试并发环境? 总结 引言 React18进入大家视野已经有一段时间了,不知道各位有没有尝试并发特性呢? 当启用并发特性后,React会从同步更新变为异步.带优先级.可中断的更新. 这也为编写单元测试带来了一些难度. 本文来聊聊React团队如何测试并发特性. 遇到的困境 主要有两个问题需要面对. 1. 如何表达渲染结果? React可以对接不同宿主环境的渲染器,大家最熟悉的渲染

  • Linux正则表达式特性详解及BRE与ERE的异同点

    Linux正则表达式(Regular Expression)主要遵从POSIX BRE或者POSIX ERE标准.什么是POSIX呢,POSIX Portable Operating System Interface 可移植操作系统接口ERE是BRE的扩展版本,具体更强的处理能力,并增加了一些元字符(metacharactor). BRE主要的能力集有: 1) 普通字符(Literal text),如a,b,c等 2)非打印字符,包括TAB,回车,换行,回车换行(WINDOWS) 3)任意字符.

  • InnoDb 体系架构和特性详解 (Innodb存储引擎读书笔记总结)

    后台线程 •Master Thread 核心后台线程,主要负责将缓冲池的数据异步刷新到磁盘.例如脏页的刷新,插入缓冲的合并,undo 页的回收等. 每秒一次的操作: 1.日志缓冲刷新到磁盘,即使该事务还没有提交.该操作总是会发生,这个就是为了再大的事务,提交时间都很短. 2.当IO压力很小时(1s内发生的IO次数小于5% innodb_io_capacity)合并5% innodb_io_capacity 的插入缓冲. 3.当脏页比例大于 innodb_max_dirty_pages_cnt,

  • IOS点击按钮隐藏状态栏详解及实例代码

    IOS点击按钮隐藏状态栏详解 前言: 最近学习IOS的基础知识,实现隐藏状态栏的功能,这里就记录下来,希望对大家有所帮助 实例代码: @interface SecondViewController () @property (nonatomic, assign,getter=isHideStatus) BOOL hideStatus; @end @implementation SecondViewController - (void)viewDidLoad { [super viewDidLoa

  • C++子类父类成员函数的覆盖和隐藏实例详解

    C++子类父类成员函数的覆盖和隐藏实例详解 函数的覆盖 覆盖发生的条件: (1) 基类必须是虚函数(使用virtual 关键字来进行声明) (2)发生覆盖的两个函数分别位于派生类和基类 (3)函数名和参数列表必须完全相同 函数的隐藏 隐藏发生的条件: (1)子类和父类的函数名相同,参数列表可以不一样 看完下面的例子就明白了 #include "iostream" using namespace std; class CBase{ public: virtual void xfn(int

  • SQL Server 2012 FileTable 新特性详解

    FileTable是基于FILESTREAM的一个特性.有以下一些功能: •一行表示一个文件或者目录. •每行包含以下信息: • •file_Stream流数据,stream_id标示符(GUID). •用户表示和维护文件及目录层次关系的path_locator和parent_path_locator •有10个文件属性 •支持对文件和文档的全文搜索和语义搜索的类型列. •filetable强制执行某些系统定义的约束和触发器来维护命名空间的语义 •针对非事务访问时,SQL Server配置FIL

  • JavaScript_ECMA5数组新特性详解

    var arr = [ 1, 2, 3, 4, 5, 4, 3, 2, 1 ]; 新加位置的方法: indexOf lastIndexOf 1.1个参数的时候表示传值 返回索引位置(index从0开始) var index = arr.indexOf(4); alert(index); //3 2. 2个参数的时候 第一个参数表示起始位置 第二个参数还是值 var index = arr.indexOf(4,4); alert(index); //5 3.他们查找数组比较的时候 '===' la

随机推荐