JSX 转换为 ReactElement 的过程

在 React 开发时使用 JSX 语法构建用户界面。

JSX 语法看起来像 HTML,但实际上是 JavaScript 语法的扩展。

JSX 不能直接在浏览器环境中运行,在 React 项目运行之前 JSX 语法会被 Babel 转换。

JSX 被 Babel 编译为 React.createElement 方法的调用,createElement 方法在调用后返回的就是 ReactElement(React 元素)。

ReactElement 就是 Virtual DOM,实际上是描述 DOM 对象的 JavaScript 对象。

createElement

定位

React 的源码都存放在 packages 文件夹下。

因为 React.createElement 是挂载在 React 对象下,所以要在 packages/react 目录下寻找。

packages/react/package.json 中找到 react 包的入口文件:"main": "index.js"

packages/react/index.js 文件从 ./src/React 导出了一个包含 createElement 的对象:

// packages\react\index.js
/* 与 flow 有关的代码 */
/* ... */export {Children,createRef,Component,PureComponent,createContext,forwardRef,lazy,memo,useCallback,useContext,useEffect,useImperativeHandle,useDebugValue,useLayoutEffect,useMemo,useReducer,useRef,useState,Fragment,Profiler,StrictMode,Suspense,createElement, // 这里cloneElement,isValidElement,version,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,createFactory,useTransition,useDeferredValue,SuspenseList,unstable_withSuspenseConfig,block,DEPRECATED_useResponder,DEPRECATED_createResponder,unstable_createFundamental,unstable_createScope,jsx,jsxs,jsxDEV,
} from './src/React';

packages/react/src/React.js 中又从 ./ReactElement 导入了 createElement 方法:

// packages\react\src\React.js
/*...*/
import {createElement as createElementProd,createFactory as createFactoryProd,cloneElement as cloneElementProd,isValidElement,jsx as jsxProd,
} from './ReactElement';
/*...*/

最终在 packages/react/src/ReactElement.js 中找到了 createElement 方法的定义:

export function createElement(type, config, children) {}
// 示例
// createElement('div', null, <第一个子元素>, <第二个子元素>...)

介绍

createElement 接收的参数:

  • type - 元素类型:<div><span> or <app>组件
  • config - 配置属性,包含:
    • 普通元素属性:props
    • 特殊属性:React 内部为了实现某些功能而存在的属性:refkeyselfsource
    • 如果元素没有属性,值为 null
  • children 及后面接收的参数 - 表示元素的元素
    • 子元素会作为参数从第三个参数开始传入
    • 所以方法内使用 arguments 获取所有子元素(第三个及之后的参数)

createElement 方法中一共做了4件事:

  1. 分离 props 属性和特殊属性

    1. 首先判断 config 是否不为 null
    2. 如果不为 null,则首先分别提取特殊属性refkeyselfsource
    3. 然后遍历config,将普通元素属性全部提取到一个变量 props
  2. 将子元素挂载到 props.children
    1. 子元素就是第三个及之后的参数
    2. 如果子元素是多个,props.children 是数组
    3. 如果子元素是一个,props.children 是对象
  3. 为 props 属性赋默认值(defaultProps)
    1. 这里其实处理的是组件,因为普通元素的 type 为字符串,组件的 type 为组件构造函数,所以只有组件才会有 defaultProps
    2. 如果有 defaultProps 则遍历属性,如果属性未赋值,则将默认值赋给它
  4. 创建并返回 ReactElement
    1. 最后通过调用 ReactElement方法,创建 ReactElement 并返回

源码

/*** 创建 React Element* type      元素类型* config    配置属性* children  子元素* 1. 分离 props 属性和特殊属性* 2. 将子元素挂载到 props.children 中* 3. 为 props 属性赋默认值 (defaultProps)* 4. 创建并返回 ReactElement*/
export function createElement(type, config, children) {/*** propName -> 属性名称* 用于后面的 for 循环*/let propName;/*** 存储 React Element 中的普通元素属性 即不包含 key ref self source*/const props = {};/*** 待提取属性* React 内部为了实现某些功能而存在的属性*/let key = null;let ref = null;let self = null;let source = null;// 如果 config 不为 nullif (config != null) {// 如果 config 对象中有合法的 ref 属性if (hasValidRef(config)) {// 将 config.ref 属性提取到 ref 变量中ref = config.ref;}// 如果在 config 对象中拥有合法的 key 属性if (hasValidKey(config)) {// 将 config.key 属性中的值(转换成字符串)提取到 key 变量中key = '' + config.key;}self = config.__self === undefined ? null : config.__self;source = config.__source === undefined ? null : config.__source;// 遍历 config 对象,将普通元素属性存储到 propsfor (propName in config) {// 如果当前遍历到的属性是对象自身属性// 并且在 RESERVED_PROPS 对象中不存在该属性(排除特殊属性)if (hasOwnProperty.call(config, propName) &&!RESERVED_PROPS.hasOwnProperty(propName)) {// 将满足条件的属性添加到 props 对象中 (普通属性)props[propName] = config[propName];}}}/*** 将第三个及之后的参数挂载到 props.children 属性中* 如果子元素是多个 props.children 是数组* 如果子元素是一个 props.children 是对象*/// 由于从第三个参数开始及以后都表示子元素// 所以减去前两个参数的结果就是子元素的数量const childrenLength = arguments.length - 2;// 如果子元素的数量是 1if (childrenLength === 1) {// 直接将子元素挂载到到 props.children 属性上// 此时 children 是对象类型props.children = children;// 如果子元素的数量大于 1} else if (childrenLength > 1) {// 创建数组, 数组中元素的数量等于子元素的数量const childArray = Array(childrenLength);// 开启循环 循环次匹配子元素的数量for (let i = 0; i < childrenLength; i++) {// 将子元素添加到 childArray 数组中// i + 2 的原因是实参集合的前两个参数不是子元素childArray[i] = arguments[i + 2];}// 将子元素数组挂载到 props.children 属性中props.children = childArray;}/*** 如果当前处理是组件(组件的 type 是 function 对象)* 看组件身上是否有 defaultProps 属性* 这个属性中存储的是 props 对象中属性的默认值* 遍历 defaultProps 对象 查看对应的 props 属性的值是否为 undefined* 如果为undefined 就将默认值赋值给对应的 props 属性值*/// 将 type 属性值视为函数 查看其中是否具有 defaultProps 属性if (type && type.defaultProps) {// 将 type 函数下的 defaultProps 属性赋值给 defaultProps 变量const defaultProps = type.defaultProps;// 遍历 defaultProps 对象中的属性 将属性名称赋值给 propName 变量for (propName in defaultProps) {// 如果 props 对象中的该属性的值为 undefinedif (props[propName] === undefined) {// 将 defaultProps 对象中的对应属性的值赋值给 props 对象中的对应属性的值props[propName] = defaultProps[propName];}}}// 开发环境判断开发者是否错误使用了 props 属性if (__DEV__) {/*...*/}// 返回 ReactElementreturn ReactElement(type,key,ref,self,source,// 在 Virtual DOM 中用于识别自定义组件ReactCurrentOwner.current,props,);
}

小细节

  1. 首先声明一个变量存储 for in 循环中的属性名称,避免后面两次循环遍历属性时,每次循环都重新声明一次,稍微提高了一些性能
let propName;
for (propName in config) {}
for (propName in defaultProps) {}
// 对比下面的方式稍微提高了一些性能
// for (let propName in config) {}
// for (let propName in defaultProps) {}
  1. 开发环境的代码

在源码中有很多开发环境下运行的代码,开发环境下执行的代码一般都是警告性代码,不是主逻辑代码,在解析源码时可以忽略。

if (__DEV__) {/*...*/}

ReactElement

ReactElement方法其实就是声明创建了一个对象,并返回。

这个对象就是 JSX 转换后的元素对象。

/*** 接收参数 返回 ReactElement*/
const ReactElement = function (type, key, ref, self, source, owner, props) {const element = {/*** 组件的类型, 十六进制数值或者 Symbol 值(如果浏览器支持 Symbol)* React 在最终在渲染 DOM 的时候, 需要确保元素的类型是 REACT_ELEMENT_TYPE* 需要此属性作为判断的依据(只有 REACT_ELEMENT_TYPE 才能被渲染成真实 DOM 元素)*/$$typeof: REACT_ELEMENT_TYPE,/*** 元素具体的类型值 如果是元素节点 type 属性中存储的就是 div span 等等* 如果元素是组件 type 属性中存储的就是组件的构造函数*/type: type,/*** 元素的唯一标识* 用作内部 vdom 比对 提升 DOM 操作性能*/key: key,/*** 存储元素 DOM 对象或者组件 实例对象*/ref: ref,/*** 存储向组件内部传递的数据*/props: props,/*** 记录当前元素所属组件 (记录当前元素是哪个组件创建的)*/_owner: owner,};// 返回 ReactElementreturn element;
};

React 检测开发者是否错误的使用了 props 属性

createElement 方法 在开发环境中 React 会检测开发者,是否在组件内部通过 props 对象获取 keyref 属性。

这两个属性是 React 内部使用的属性,这种用法是不正确的,React 检测到这种行为会在控制台发出错误提示。

例如:

function App(props) {console.log(props.key) // 报错return <div>App works</div>
}React.render(<App/>, document.getElementById('root'))

检测过程:

  1. 首先判断是否是开发环境
  2. 然后判断是否有 key 或者 ref 属性
  3. 如果有:
    1. 获取组件的名字,因为要在报错时提示时哪个组件进行了错误操作:

      1. 如果是组件,则获取它的 displayNamename,如果未命名,则为 Unknown
      2. 如果是普通元素,则为它的 tag 名,也就是 type 的值
    2. 然后调用对应的方法,这些方法的内容就是:
      1. props 对象添加 keyref 属性
      2. 定义它们的 getter 方法,当触发时,也就是通过 props 对象获取 keyref 时报错
        1. 方法中使用了全局变量,保证报错只执行一次
export function createElement(type, config, children) {/*...*//*** 在开发环境中 React 会检测开发者是否在组件内部* 通过 props 对象获取 key 属性或者 ref 属性* 如果开发者调用了 在控制台中报错误提示*/// 如果处于开发环境if (__DEV__) {// 元素具有 key 属性或者 ref 属性if (key || ref) {// 看一下 type 属性中存储的是否是函数 如果是函数就表示当前元素是组件// 如果元素不是组件 就直接返回元素类型字符串// displayName 用于在报错过程中显示是哪一个组件报错了// 如果开发者显式定义了 displayName 属性 就显示开发者定义的// 否者就显示组件名称 如果组件也没有名称 就显示 'Unknown'const displayName =typeof type === 'function'? type.displayName || type.name || 'Unknown': type;// 如果 key 属性存在if (key) {// 为 props 对象添加key 属性// 并指定当通过 props 对象获取 key 属性时报错defineKeyPropWarningGetter(props, displayName);}// 如果 ref 属性存在if (ref) {// 为 props 对象添加 ref 属性// 并指定当通过 props 对象获取 ref 属性时报错defineRefPropWarningGetter(props, displayName);}}}return ReactElement({/*...*/})
}
/***  指定当通过 props 对象获取 key 属性时报错*  props        组件中的 props 对象*  displayName  组件名称标识*/function defineKeyPropWarningGetter(props, displayName) {// 通过 props 对象获取 key 属性报错const warnAboutAccessingKey = function () {// 在开发环境中if (__DEV__) {// specialPropKeyWarningShown 控制错误只输出一次的变量if (!specialPropKeyWarningShown) {// 通过 specialPropKeyWarningShown 变量锁住判断条件specialPropKeyWarningShown = true;// 指定报错信息和组件名称console.error('%s: `key` is not a prop. Trying to access it will result ' +'in `undefined` being returned. If you need to access the same ' +'value within the child component, you should pass it as a different ' +'prop. (https://reactjs.org/link/special-props)',displayName,);}}};warnAboutAccessingKey.isReactWarning = true;// 为 props 对象添加 key 属性Object.defineProperty(props, 'key', {// 当获取 key 属性时调用 warnAboutAccessingKey 方法进行报错get: warnAboutAccessingKey,configurable: true,});
}/***  指定当通过 props 对象获取 ref 属性时报错*  props        组件中的 props 对象*  displayName  组件名称标识*/
function defineRefPropWarningGetter(props, displayName) {// 通过 props 对象获取 ref 属性报错const warnAboutAccessingRef = function () {if (__DEV__) {// specialPropRefWarningShown 控制错误只输出一次的变量if (!specialPropRefWarningShown) {// 通过 specialPropRefWarningShown 变量锁住判断条件specialPropRefWarningShown = true;// 指定报错信息和组件名称console.error('%s: `ref` is not a prop. Trying to access it will result ' +'in `undefined` being returned. If you need to access the same ' +'value within the child component, you should pass it as a different ' +'prop. (https://reactjs.org/link/special-props)',displayName,);}}};warnAboutAccessingRef.isReactWarning = true;// 为 props 对象添加 key 属性Object.defineProperty(props, 'ref', {get: warnAboutAccessingRef,configurable: true,});
}

isValidElement 验证有效 React 元素

isValidElement 方法被定义在 packages/react/src/ReactElement 文件中,用于验证是否是有效的 React 元素。

其中最主要的判断依据就是是否设置了正确的 $$typeof 属性,每个创建的 React 元素都包含这个属性,表示能够转换成真实 DOM 的 React 元素。

$$typeof的值是 十六进制数值或者 Symbol 值(如果浏览器支持 Symbol)

/*** 验证 object 参数是否是 ReactElement. 返回布尔值* 验证成功的条件:* object 是对象* object 不为 null* object 对象中的 $$typeof 属性值为 REACT_ELEMENT_TYPE*/
export function isValidElement(object) {return (typeof object === 'object' &&object !== null &&object.$$typeof === REACT_ELEMENT_TYPE);
}
// packages\shared\ReactSymbols.js
// 定义 REACT_ELEMENT_TYPE 的地方
export const REACT_ELEMENT_TYPE = hasSymbol? Symbol.for('react.element'): 0xeac7;

React 16 源码解析笔记 02 - JSX 转换为 ReactElement 的过程相关推荐

  1. React深入学习与源码解析笔记

    ***当前阶段的笔记 *** 「面向实习生阶段」https://www.aliyundrive.com/s/VTME123M4T9 提取码: 8s6v 点击链接保存,或者复制本段内容,打开「阿里云盘」 ...

  2. 源码解析:Spring源码解析笔记(五)接口设计总览

    本文由colodoo(纸伞)整理 QQ 425343603 Java学习交流群(717726984) Spring解析笔记 启动过程部分已经完成,对启动过程源码有兴趣的朋友可以作为参考文章. 源码解析 ...

  3. obs 源码解析笔记

    obs 源码解析笔记 由于obs rtp音频传输有问题,所以可能需要修改obs源码,学习了两天,发现官方文档有些混乱,国内有关说明又少,特此记录,也方便以后自己查阅.这里主要涉及工作有关源码其他基本略 ...

  4. 震撼来袭,阿里高工的源码解析笔记手抄本,看完去怼面试官

    很多程序员一开始在学习上找不到方向,但我想在渡过了一段时间的新手期之后这类问题大多都会变得不再那么明显,工作的方向也会逐渐变得清晰起来. 但是没过多久,能了解到的资料就开始超过每天学习的能力,像是买了 ...

  5. Spring IOC源码解析笔记

    小伙伴们,你们好,我是老寇 Spring最重要的概念就算IOC和AOP,本篇主要记录Spring IOC容器的相关知识,本文适合有spring相关基础并且想了解spring ioc的相关原理的人看 本 ...

  6. mybatis源码深度解析_30天消化MyBatis源码解析笔记,吊打面试官,offer接到手软

    MyBatis 是一个优秀的 Java 持久化框架,SSM 框架组合(Spring + SpringMVC + Mybatis),依赖 MyBatis 搭建的项目更是数不胜数,在互联网公司的使用中,占 ...

  7. Vue源码解析(笔记)

    github vue源码分析 认识flow flow类型检查 安装flow sudo npm install -g flow-bin 初始化flow flow init 运行flow命令 命令: fl ...

  8. iperf3.1源码解析笔记(1)-网络压力测试工具iperf的使用

    文章目录 一 iperf背景与下载 二 安装及使用方式 三 注意事项 一 iperf背景与下载 iperf用于测试网络性能,支持TCP.UDP和SCTP.可得到网络带宽.延迟抖动和报文丢包的实际测试数 ...

  9. spring源码解析(二) @Autowired自动注入过程

    1.依赖注入到底有几种?两种.四种.五种? 两种: 手动:set(byType.byName).构造器 自动:xml中:set.构造器 autowired注解中:set.属性.构造器 重点不在于到底有 ...

  10. 【蓝牙sbc协议】sbc源码阅读笔记(三)——数据读写过程

    sbc_编码过程详解 编码部分源码 // sbcenc.c static void encode(char *filename, int subbands, int bitpool, int join ...

最新文章

  1. Open3d学习计划—高级篇 7(颜色映射)
  2. Hibernate的DetachedCriteria使用(含Criteria)
  3. CUBRID学习笔记 42 Hierarchical QuerySQL层级查询
  4. SAP Spartacus的开发人员来自全球各地
  5. 计算机视觉模式识别用到的几本优化的书籍
  6. 深度解读Microsoft Build 2020:提升开发效率,优化开发环境
  7. 萍萍多用户简历系统 v1.0
  8. 再谈内核模块加载(二)—模块加载流程(上)
  9. 计算机word如何计算年龄,Excel 如何自动计算年龄
  10. 微信开放平台(公众号)API学习笔记(1)-公众号测试账户
  11. WIN10外接显示器有妙招
  12. 简单拖拉拽就能做数据可视化分析图表
  13. 使用matlab作单位阶跃响应,基于MATLAB的控制系统单位阶跃响应分析[共7页]
  14. Antd如何在label里增加icon图标
  15. Python菜鸟学习手册14----标准库+代码实例
  16. 一图掌握ISACA五大资格证书体系
  17. 基于阿里平头哥的单片机软、硬件i2C驱动oled
  18. element UI实现el-dialog(弹框)可拖拽功能(网上借鉴)
  19. ChatGPT百科全书(全网最全面)
  20. 小米手机4c如何刷入开发版获取ROOT权限

热门文章

  1. 论文理解【Offline RL】 —— 【COIL】Curriculum Offline Imitating Learning
  2. js 禁止鼠标菜单键及键盘快捷键
  3. 非常简单Java web下Office转PDF功能实现 100%能用
  4. c语言题 设圆的半径,C语言初学者:编程序:设圆半径r圆柱高h,求圆周长面积圆柱体积圆球表面积...
  5. java 调用三角函数_Java中的三角函数方法
  6. linux下动态域名解析
  7. 微信公众号接口开发--回复消息
  8. 2019年9月中国编程语言排行榜
  9. lpx寒假作业案例5
  10. SQLSERVER月份简写转数字