React源码分析1-jsx转换及React.createElement

news2025/1/15 12:57:16

jsx 的转换

我们从 react 应用的入口开始对源码进行分析,创建一个简单的 hello, world 应用:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class App extends Component {
  render() {
    return <div>hello, world</div>;
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

我们注意到,我们在 App 组件中直接写了 return <div>hello, world</div> 的 jsx 语句,那么 jsx 语法是如何被浏览器识别执行的呢?

另外我在第一次学习 react 的时候,就有一个疑惑: import React, { Component } from 'react' 这段代码中,React 似乎在代码中没有任何地方被用到,为什么要引入呢?

16.x 版本及之前

我们在 react16.8 版本的代码中,尝试将 React 的引用去掉:

// import React, { Component } from 'react';
import { Component } from 'react'; // 去掉 React 的引用
import ReactDOM from 'react-dom';

export default class App extends Component {
  render() {
    return <div>hello, world</div>;
  }
}

ReactDOM.render(<App />, document.getElementById('root'));

运行应用程序,发现会提示 'React' must be in scope when using JSX 的 error:

这是因为上述的类组件 render 中返回了 <div>hello, world</div> 的 jsx 语法,在React16版本及之前,应用程序通过 @babel/preset-react 将 jsx 语法转换为 React.createElement 的 js 代码,因此需要显式将 React 引入,才能正常调用 createElement。我们可以在 Babel REPL 中看到 jsx 被 @babel/preset-react 编译后的结果

17.x 版本及之后

React17版本之后,官方与 bbel 进行了合作,直接通过将 react/jsx-runtime 对 jsx 语法进行了新的转换而不依赖 React.createElement,转换的结果便是可直接供 ReactDOM.render 使用的 ReactElement 对象。因此如果在React17版本后只是用 jsx 语法不使用其他的 react 提供的api,可以不引入 React,应用程序依然能够正常运行。

更多有关于 React jsx 转换的内容可以去看官网了解:介绍全新的JSX转换,在这里就不再过多展开了。

React.createElement 源码

虽然现在 react17 之后我们可以不再依赖 React.createElement 这个 api 了,但是实际场景中以及很多开源包中可能会有很多通过 React.createElement 手动创建元素的场景,所以还是推荐学习一下React.createElement源码。

React.createElement 其接收三个或以上参数:

  • type:要创建的 React 元素类型,可以是标签名称字符串,如 'div' 或者 'span' 等;也可以是 React组件 类型(class组件或者函数组件);或者是 React fragment 类型。
  • config:写在标签上的属性的集合,js 对象格式,若标签上未添加任何属性则为 null。
  • children:从第三个参数开始后的参数为当前创建的React元素的子节点,每个参数的类型,若是当前元素节点的 textContent 则为字符串类型;否则为新的 React.createElement 创建的元素。

函数中会对参数进行一系列的解析,源码如下,对源码相关的理解都用注释进行了标记:

export function createElement(type, config, children) {
  let propName;

  // 记录标签上的属性集合
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  // config 不为 null 时,说明标签上有属性,将属性添加到 props 中
  // 其中,key 和 ref 为 react 提供的特殊属性,不加入到 props 中,而是用 key 和 ref 单独记录
  if (config != null) {
    if (hasValidRef(config)) {
      // 有合法的 ref 时,则给 ref 赋值
      ref = config.ref;

      if (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    if (hasValidKey(config)) {
      // 有合法的 key 时,则给 key 赋值
      key = '' + config.key;
    }

    // self 和 source 是开发环境下对代码在编译器中位置等信息进行记录,用于开发环境下调试
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 将 config 中除 key、ref、__self、__source 之外的属性添加到 props 中
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // 将子节点添加到 props 的 children 属性上
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    // 共 3 个参数时表示只有一个子节点,直接将子节点赋值给 props 的 children 属性
    props.children = children;
  } else if (childrenLength > 1) {
    // 3 个以上参数时表示有多个子节点,将子节点 push 到一个数组中然后将数组赋值给 props 的 children
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    // 开发环境下冻结 childArray,防止被随意修改
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // 如果有 defaultProps,对其遍历并且将用户在标签上未对其手动设置属性添加进 props 中
  // 此处针对 class 组件类型
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // key 和 ref 不挂载到 props 上
  // 开发环境下若想通过 props.key 或者 props.ref 获取则 warning
  if (__DEV__) {
    if (key || ref) {
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }

  // 调用 ReactElement 并返回
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

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

由代码可知,React.createElement 做的事情主要有:

  • 解析 config 参数中是否有合法的 key、ref、__source 和 __self 属性,若存在分别赋值给 key、ref、source 和 self;将剩余的属性解析挂载到 props 上

  • 除 type 和 config 外后面的参数,挂载到 props.children

  • 针对类组件,如果 type.defaultProps 存在,遍历 type.defaultProps 的属性,如果 props 不存在该属性,则添加到 props 上

  • 将 type、key、ref、self、props 等信息,调用 ReactElement 函数创建虚拟 dom,ReactElement 主要是在开发环境下通过 Object.defineProperty 将 _store、_self、_source 设置为不可枚举,提高 element 比较时的性能:

    const ReactElement = function(type, key, ref, self, source, owner, props) {
      const element = {
        // 用于表示是否为 ReactElement
        $$typeof: REACT_ELEMENT_TYPE,
    
        // 用于创建真实 dom 的相关信息
        type: type,
        key: key,
        ref: ref,
        props: props,
    
        _owner: owner,
      };
    
      if (__DEV__) {
        element._store = {};
    
        // 开发环境下将 _store、_self、_source 设置为不可枚举,提高 element 的比较性能
        Object.defineProperty(element._store, 'validated', {
          configurable: false,
          enumerable: false,
          writable: true,
          value: false,
        });
    
        Object.defineProperty(element, '_self', {
          configurable: false,
          enumerable: false,
          writable: false,
          value: self,
        });
    
        Object.defineProperty(element, '_source', {
          configurable: false,
          enumerable: false,
          writable: false,
          value: source,
        });
        // 冻结 element 和 props,防止被手动修改
        if (Object.freeze) {
          Object.freeze(element.props);
          Object.freeze(element);
        }
      }
    
      return element;
    };
    

所以通过流程图总结一下 createElement 所做的事情如下:

React.Component 源码

我们回到上述 hello,world 应用程序代码中,创建类组件时,我们继承了从 react 库中引入的 Component,我们再看一下React.Component源码:

function Component(props, context, updater) {
  // 接收 props,context,updater 进行初始化,挂载到 this 上
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  // updater 上挂载了 isMounted、enqueueForceUpdate、enqueueSetState 等触发器方法
  this.updater = updater || ReactNoopUpdateQueue;
}

// 原型链上挂载 isReactComponent,在 ReactDOM.render 时用于和函数组件做区分
Component.prototype.isReactComponent = {};

// 给类组件添加 `this.setState` 方法
Component.prototype.setState = function(partialState, callback) {
  // 验证参数是否合法
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null
  );
  // 添加至 enqueueSetState 队列
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

// 给类组件添加 `this.forceUpdate` 方法
Component.prototype.forceUpdate = function(callback) {
  // 添加至 enqueueForceUpdate 队列
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

从源码上可以得知,React.Component 主要做了以下几件事情:

  • 将 props, context, updater 挂载到 this 上
  • 在 Component 原型链上添加 isReactComponent 对象,用于标记类组件
  • 在 Component 原型链上添加 setState 方法
  • 在 Component 原型链上添加 forceUpdate 方法
    这样我们就理解了 react 类组件的 super() 作用,以及 this.setStatethis.forceUpdate 的由来

总结

本章讲述了 jsx 在 react17 之前和之后的不同的转换,实际上 react17 之后 babel 的对 jsx 的转换就是比之前多了一步 React.createElement 的动作:

另外讲述了 React.createElementReact.Component 的内部实现是怎样的。通过 babel及 React.createElement,将 jsx 转换为了浏览器能够识别的原生 js 语法,为 react 后续对状态改变、事件响应以及页面更新等奠定了基础。

后面的章节中,将探究 react 是如何一步步将状态等信息渲染为真实页面的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/45084.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

科普下抖音的规则,为什么别人的内容很容易火,而我的很难?

今天给大家科普下抖音的规则&#xff0c;为什么别人的内容很容易火&#xff0c;而我的很难&#xff1f; 上一篇给大家讲了现在做抖音还来得及么&#xff1f;肯定的回答&#xff0c;一直都来得及。 既然来得及&#xff0c;那么我们怎么才能做好抖音呢&#xff1f; 在我看来&a…

Rust 基础(四)

十、泛型、Traits和生命周期 每种编程语言都有有效处理概念重复的工具。在Rust中&#xff0c;一个这样的工具就是泛型:具体类型或其他属性的抽象替身。我们可以表达泛型的行为&#xff0c;或者它们如何与其他泛型相关联&#xff0c;而不知道在编译和运行代码时它们的位置会是什…

[C++]C++入门--引用

​ &#x1f941;作者&#xff1a; 华丞臧 &#x1f4d5;​​​​专栏&#xff1a;【C】 博主Gitee 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎>在评论区指出。 推荐一款刷题网站 &#x1f449;LeetCode…

IPv6进阶:IPv6 过渡技术之IPv6 over IPv4 手动隧道

实验拓扑 R1-R3-R2之间的网络为IPv4环境&#xff1b;PC1及PC2处于IPv6孤岛。 实验需求 R1及R2为IPv6/IPv4双栈设备&#xff1b;在R1及R2上部署IPv6 over IPv4手工隧道使得PC1及PC2能够互相访问。 配置及实现 R3的配置如下 [R3] interface GigabitEthernet0/0/0 [R3-Gigabi…

【Java实战】工作中如何规范控制语句

目录 一、前言 二、控制语句规范 1.【强制】使用switch注意事项 2.【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时&#xff0c;必须先进行 null 判断。 3.【强制】在 if / else / for / while / do 语句中必须使用大括号。 4.【强制】三目运算符高…

[附源码]计算机毕业设计springboot本地助农产品销售系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【C++】string详细介绍及模拟实现string类

【C】string详细介绍及模拟实现string类 文章目录【C】string详细介绍及模拟实现string类1.什么是string2.string常用接口介绍2.1string类对象的常见构造2.2string类对象的容量操作2.3string类对象的访问及遍历操作2.4string类对象的修改操作2.5string类非成员函数3.string类的…

移动跨平台开发跨家选型参考建议

从 iPhone 诞生至今&#xff0c;智能手机风靡全球已将近20年&#xff0c;智能手机操作系统 iOS 和 Android 也成为当仁不让的顶流般的存在&#xff0c;而作为其背后的灵魂&#xff0c;移动应用也随着技术的发展已经越来越丰富。如果从技术层面来讲&#xff0c;移动 App 也从最开…

Cloud-computing 实验镜像 chinaskills_cloud_iaas.iso chinaskills_cloud_paas.iso

Cloud-computing 实验镜像 最近因新项目再次进行云计算环境的搭建&#xff0c; 找这两个镜像&#xff08; 找chinaskills_cloud_paas.iso chinaskills_cloud_iaas.iso&#xff09;颇为费劲&#xff0c;用尽九牛二虎之力总算找到了&#xff0c;该大侠还分享了诸多系统镜像和完…

高衍射效率的偏振无关透射光栅的分析与设计

摘要 光栅&#xff0c;特别是具有与波长相当的特征尺寸的光栅&#xff0c;具有偏振相关的光学特性。 这使得设计的具有高衍射效率的光栅难以用于任意偏振。 根据文献[T. Clausnitzer, et al&#xff0c;Proc. SPIE 5252,174-182&#xff08;2003&#xff09;]中报道的概念&…

VMware-AD域控管理

目录 新建AD用户[ 以张三[zhangsan]、李四[lisi]为例 ] 2.用户信息-属性-管理-编辑&#xff1a; 3.将张三设置为AD域控管理员&#xff0c; 在wqd.com域下新建几个部门&#xff08;IT、HR、PRD&#xff09; 对从主机&#xff08;win7&#xff09;进行AD接管 修改win7计算机名称&…

MATLB|电动车智能充电模式及电力高峰需求预测

目录 0 写在前面 1 电动车 1.1 电动车&#xff08;EV&#xff09; 1.2 电动汽车充电 1.3 智能充电和车联网&#xff08;V2G&#xff09; 1.4 V2G 应用 1.5 可再生能源可用性 1.6 基于价格的收费 2 电动车智能充电 2.1 智能充电 2.2 实时电价 2.3 智能充电模式——算…

国产CPU对比

关于国产CPU&#xff1a;龙芯、飞腾、鲲鹏、海光、申威、兆芯 CPU 是计算机系统的核心和大脑 n CPU&#xff0c;即中央处理器是计算机的运算和控制核心&#xff0c;其功能主要是解释计算机指令以及处理计算机软件中的数据. CISC实际上是以增加处理器本身复杂度作为代价&#xf…

论文翻译:多延迟块频域自适应滤波器

Multidelay Block Frequency Domain Adaptive Filter 作者&#xff1a; JIA-SIEN SOO 和 KHEE K. PANG 文章目录Multidelay Block Frequency Domain Adaptive Filter1.介绍2.MDF自适应滤波器3.仿真结果和性能分析4.计算的复杂性5.结论摘要-本文提出了一种灵活的多延迟块频域自…

农村城镇面板数据集:地级市人均消费与支出2012-2019各省农村数据2013-2019

1、2002-2019年地级市人均消费与支出数据 1、数据来源&#xff1a;wind 2、时间跨度&#xff1a;2012-2019 3、区域范围&#xff1a;287个地级市 4、指标说明&#xff1a; 包含以下四个指标&#xff1a;人均可支配收入&#xff08;农村&#xff09;、人均可支配收入&#…

在el-table表头上引入组件不能实时传参bug

文章目录场景还原解决方法出现原因场景还原 产品要求&#xff1a;点击表格的表头&#xff0c;能触发一个下拉的列表&#xff0c;列表能携带表格的筛选条件&#xff0c;获取相应的数据 写了一个demo&#xff0c;来还原一下bug出现的场景&#xff1a; <div id"demo&qu…

Day15--加入购物车-初始化vuex

1.加入购物车&#xff1a; 我的操作&#xff1a; ************************************************************************************************************* 2.购物车里面的商品数据在多个页面都会用到。所以把购物车里面的商品数据存储在vuex里面&#xff0c; 我的…

11月29日:thinkphp框架->请求

回忆上节知识点 thinkphp官方文档解释 Rest控制器&#xff1a;主要是对资源进行控制&#xff0c;在thinkphp6.0开始废弃&#xff0c;推荐使用资源控制器 Rest控制器使用符合RESTFul风格&#xff0c;RESTFul方法和标准模式的操作方法定义主要区别在于&#xff0c;需要对请求类型…

Linux圈子里的“鲁大师“dmidecode-尚文网络xUP楠哥

~~全文共1189字&#xff0c;阅读需约5分钟。 进Q群11372462&#xff0c;领取专属报名福利&#xff0c;包含云计算学习路线图代表性实战训练大厂云计算面试题资料! Linux系统内核中有这样一个叫做DMI的东东&#xff0c;英文全称是Desktop Management Interface&#xff0c;翻译…

Ubutun搭建集群遇到的一些问题

安装部署K8s集群时会遇到很多问题&#xff0c;以下都是我踩过的坑&#xff0c;还有一些小坑当时没来得及记录&#xff0c;后续如果有遇到的话再进行补充。此处非常感谢江城琉璃梦同学对我的帮助。 1.工作节点执行kubectl get nodes时拒绝连接 执行指令&#xff1a;kubectl ge…