🔥🔥🔥 React18 源码学习 - React Element

前言

本文的React代码版本为18.2.0

可调试的代码仓库为:GitHub - yyyao-hh/react-debug at master-pure

React Element作为React架构的基石,理解其本质和工作原理对于深入掌握React至关重要。并且后面的章节都依托于前面基础知识的铺垫,所以务必仔细阅读本章内容!!!

基本概念

JSXJavaScript XML)是一种JavaScript的语法扩展,它允许我们在JavaScript中直接书写类似HTML的结构。它为我们提供了一种简洁、直观且高效的方式描述 UI 组件。

官方文档:JSX 简介 -- React

javascript 复制代码
/* src/index.js */

function APP() {
  return <div class="title">Hello, world!</div>;
};

从 JSX 到 React Element

实际上,JSX并不是React直接执行的代码,它会被编译成标准的JavaScript代码。

  • React17之前,JSX代码都会被转化为React.createElement函数进行处理。而这也是为什么之前的项目中需要引入React
javascript 复制代码
/* createElement: src/react/packages/react/src/ReactElement.js */

import React from "react"

// JSX 代码
<div className="title">Hello World</div>

// 编译为
React.createElement('div', { className: 'title' }, 'Hello World')
  • React17之后,Reactbabel进行了合作,支持了jsx-runtime。所以我们不用手动引入React,而是由编译器自动引入新的jsx函数 。
php 复制代码
/* jsx: src/react/packages/react/jsx-runtime.js */

// JSX 代码  
<div className="title">Hello World</div>

// 编译为
import { jsx as _jsx } from 'react/jsx-runtime'
_jsx('div', { className: 'title', children: 'Hello World' })

新模式配置:

lua 复制代码
{
  "plugins": [["@babel/plugin-transform-react-jsx", { "runtime": "automatic" }]]
}

使用create-react-appVite等现代工具链,通常默认使用新模式

但是无论是上面的哪种转换方式,最终结果都是被转化为一个普通的JavaScript对象,它描述了你希望在屏幕上看到的内容。

React Element 的数据结构

typescript 复制代码
/* src/react/packages/shared/ReactElementType.js */

export type ReactElement = {|
  $$typeof: any,
  type: any,
  key: any,
  ref: any,
  props: any,
  _owner: any

  // 省略 __DEV__ 环境的一些属性
|};

每个属性的含义如下:

属性 描述 示例值
$$typeof 唯一标识这是一个React Element,通常是一个Symbol Symbol(react.element)
type 元素的类型 - 字符串:表示原生DOM节点('div') - 函数或类:表示自定义组件(MyComp
key 列表项的唯一标识 'item-1'
ref 用于访问底层DOM节点或组件实例的引用 {current: null}
props 元素的属性集合(不包括keyref {className: 'app', children: ...}
_owner 创建此元素的组件对应的Fiber节点 FiberNode

需要特别注意的是$$typeof属性,它是一个Symbol类型的值,用于唯一标识React Element。这个设计主要是为了安全考虑------防止XSS攻击,因为JSON中不能包含Symbol类型的值,即使服务器注入了一个恶意的React Element对象,React也不会认它。

React Element 与 Fiber 节点的关系

理解React ElementFiber节点之间的关系至关重要,它们是React架构中不同但互补的概念。

不同的角色与职责

  • React Element :是一个普通的JavaScript对象,它描述了UI应该是什么样子。它是不可变的,每次更新都会创建新的Element对象。我们可以把它看作是UI的蓝图:它说明了需要渲染什么,但不会实际执行渲染工作。
  • Fiber :是React调和过程(Reconciliation)中的工作单元。它是在渲染过程中创建的,然后组成了Fiber树。与React Element不同,Fiber是可变的,它们包含组件状态、副作用标记、以及指向其他Fiber的指针(形成链表结构)。

渲染过程中的协作

React渲染过程的步骤,会遵循以下过程:

  1. 编译阶段:JSX被转译为createElement/jsx函数调用
  2. 触发更新:初始渲染、状态更新、属性更新等
  3. 渲染阶段:
    1. 调用函数组件(或类组件的render方法)获取新的React Element
    2. 将新的Element树与上一次渲染的Fiber树进行比较,生成新的Fiber树(Diff算法)
  1. 提交阶段:将更新应用到实际的DOM

创建过程

要真正理解React Element,我们需要深入createElement/jsx函数的源码,看看它是如何构造这个核心数据结构的。

注意:代码去除了开发环境相关的逻辑,并精简处理

createElement 函数分析

ini 复制代码
/* src/react/packages/react/src/ReactElement.js */

const RESERVED_PROPS = {
  key: true,
  ref: true,
};

export function createElement(type, config, children) {
  let propName;
  const props = {};

  let key = '' + config.key;
  let ref = config.ref;

  // 过滤掉保留属性(key, ref等), 将其他属性存到 props 对象
  for (propName in config) {
    if (
      hasOwnProperty.call(config, propName) &&
      !RESERVED_PROPS.hasOwnProperty(propName)
    ) {
      props[propName] = config[propName];
    }
  }

  // 处理 children 参数
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 处理默认 props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 创建并返回 React Element
  return ReactElement(...);
}

jsx 函数分析

逻辑与createElement类似,入参方面将key作为单独参数传递。

ini 复制代码
/* src/react/packages/react/src/jsx/ReactJSXElement.js */

const RESERVED_PROPS = {
  key: true,
  ref: true,
};

export function jsx(type, config, maybeKey) {
  let propName;
  const props = {};

  let key = config.key;
  let ref = config.ref;

  // 过滤掉保留属性(key, ref等), 将其他属性存到 props 对象(children 也在 config 中)
  for (propName in config) {
    if (
      hasOwnProperty.call(config, propName) &&
      !RESERVED_PROPS.hasOwnProperty(propName)
    ) {
      props[propName] = config[propName];
    }
  }

  // 处理默认 props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 创建并返回 React Element
  return ReactElement(...);
}

ReactElement 函数

rust 复制代码
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };
  
  return element;
};

要深入理解React Element的创建过程,最好的方法是在源码中设置断点并进行调试。

总结

很多人将React Element等同于虚拟DOM,实际上React Element确实是React虚拟DOM实现的一部分,而另一部分就是Fiber架构。当组件状态变化时,React会创建新的Element树,然后通过Diff算法比较新旧两棵树,找出最小变更,最后批量更新真实DOM

下一章我们将了解整个React应用渲染的起点:容器的挂载

相关推荐
且菜且折腾14 分钟前
react快捷键hook
javascript·react.js·ecmascript
晴栀ay2 小时前
React性能优化三剑客:useMemo、memo与useCallback
前端·javascript·react.js
Bigger3 小时前
Tauri (25)——消除搜索列表默认选中的 UI 闪动
前端·react.js·weui
hongkid3 小时前
React Native 如何打包正式apk
javascript·react native·react.js
aka_tombcato3 小时前
【开源自荐】 AI Selector:一款通用 AI 配置组件,让你的应用快速接入 20+ LLM AI厂商
前端·vue.js·人工智能·react.js·开源·ai编程
光影少年3 小时前
前端如何虚拟列表优化?
前端·react native·react.js
hxjhnct4 小时前
React 为什么不采用(VUE)绑定数据?
javascript·vue.js·react.js
time_rg18 小时前
react fiber与事件循环
前端·react.js
前端不太难18 小时前
用一张“状态扩散图”,定位 RN 列表性能风险
react.js·harmonyos
fe小陈19 小时前
react-nil 逻辑渲染器
react.js