React源码解析系列(一) -- babel解析jsx的那些事儿

740 阅读5分钟

对于那些初次接触 React 的开发者来说,它就像是一份“美味的鸡腿堡”,让人一试成主顾。React 以其原生的 JavaScript 语言、丰富的生态系统以及流畅的开发体验,吸引着越来越多的开发者。啊!这种感觉就像是在美漫中找到了英雄归宿一般令人兴奋。

虚拟DOM:提升性能的秘密武器

先记住React 框架采用的是 MVC 体系。而React的一大优势在于其虚拟DOM机制。如果你对虚拟dom不太了解,可以先看看这篇文章

虚拟DOM存在于内存中,允许React高效地计算并最小化实际DOM更新。每当状态改变时,React会比较新的虚拟DOM树与旧的版本,找出差异,然后只对实际DOM进行必要的更新。这种方式显著减少了浏览器重绘和回流的次数,相较原生JS频繁使用dom,从而提高了应用的性能。

这时候我们得回到JSX,先看一看在babel的编译下,理解JSX底层处理机制。使用babel-preset-react-app看这里

关于 JSX 底层处理机制

我们从 react 应用的入口开始对源码进行分析,建立下面代码

import React from 'react'
import ReactDOM from 'react-dom/client'
let x = 10
let y = 20

ReactDOM.createRoot(document.getElementById('root')).render(
    <>
        <h2 className="title">10.24早安</h2>
        <div className="box">
            <span>{x}</span>
            <br />
            <span>{y}</span>
        </div>
    </>
)

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

先解决为啥React要引入

即使看起来 React 没有直接在代码中被使用,引入它仍然是必要的。这是因为 React 的 JSX 语法实际上是由 Babel(或其他 JSX 转换器)转换为普通的 JavaScript 代码

// JSX
<div className="example" /> 
// 转换后 
React.createElement("div", { className: "example" });

JSX 看起来像是 XML 或 HTML,但实际上它是 JavaScript 的一种语法糖。当你编写 JSX 时,例如上面 <div />,Babel 会将其转换为 React.createElement('div', null) 的调用

如果你不导入 React,当代码被编译时,没有在作用域找到,React.createElement 将无法解析,导致运行时错误

同样,对于类组件的 Component 继承

import React, { Component } from 'react'; 
class MyComponent extends Component { 
    render() { 
        return <div>Hello, world!</div>; 
} }

{ Component } 是从 react 库中解构出来的,创建MyComponent类组件

后面,react团队考虑系统兼容,React18 引入React Hook 。(React Hook 和类组件是两种不同的构建和管理插件的方式,看看后面再出篇文章讲吧)

很多情况下你可以不需要显式地导入 React,因为函数式组件不需要继承 Component,而且许多钩子(如 useState, useEffect)可以直接从 react 中解构出来。然而,如果你使用了 JSX,React 仍然需要被导入,除非你配置了 Babel 插件(如 @babel/preset-react)以启用自动引入 React 的功能(通过设置 runtime: 'automatic' 选项)

回来看babel编译JSX

使用babel 官网try it out进行编译,网址

image.png image.png

第一步:把我们编写的 JSX 语法,编译为虚拟 DOM 对象「virtualDOM」

虚拟 DOM 对象是框架自己内部构建的一套对象体系(对象的相关成员都是 React 内部规定的),基于这些属性描述出我们所构建视图中的 DOM 节点的相关特征!

  1. 基于 babel-preset-react-app 把 JSX 编译为 React.createElement(...) 这种格式!

    • 只要是元素节点,必然会基于 createElement 进行处理!

    • React.createElement(ele, props, ...children)

      • ele: 元素标签名「或组件」
      • props: 元素的属性集合(对象)「如果没有设置过任何的属性,则此值是 null」
      • children: 第三个及以后的参数,都是当前元素的子节点
  2. 再把 createElement 方法执行,创建出 virtualDOM 虚拟 DOM 对象「也有称之为:JSX 元素、JSX 对象、ReactChild 对象...」!!

virtualDOM = {
  $$typeof: Symbol(react.element),
  ref: null,
  key: null,
  type: 标签名「或组件」,
  // 存储了元素的相关属性 && 子节点信息
  props: {
    元素的相关属性,
    children: 子节点信息「没有子节点则没有这个属性、属性值可能是一个值、也可能是一个数组」
  }
}

第二步:把构建的 virtualDOM 渲染为真实 DOM

  • 真实 DOM:浏览器页面中,最后渲染出来,让用户看见的 DOM 元素!!

补充说明:第一次渲染页面是直接从 virtualDOM -> 真实 DOM;但是后期视图更新的时候,需要经过一个 DOM-DIFF 的对比,计算出补丁包 PATCH(两次视图差异的部分),把 PATCH 补丁包进行渲染!!

babel手写实操

createElement这个函数是 React 中用于创建虚拟 DOM 元素的核心函数之一,它模仿了 React 内部的 React.createElement 方法。

// 导出默认函数 createElement,用于创建虚拟 DOM 元素
export default function createElement(ele, props, ...children) {
    // 创建一个虚拟 DOM 对象,包含必要的属性
    let virtualDOM = {
        $$typeof: Symbol.for('react.element'), // 用于标识这是一个 React 元素对象
        key: null,                             // 元素的唯一标识符,用于优化更新操作
        ref: null,                             // 用于获取对 DOM 节点或组件实例的引用
        type: null,                            // 元素类型(可以是标签名、函数组件、类组件等)
        props: {}                              // 其他,元素的属性和子元素
    };

    // 设置虚拟 DOM 的类型为传入的 ele 参数
    virtualDOM.type = ele;

    // 如果 props 不为空,则使用展开运算符,展开到 virtualDOM.props 中
    if (props != null) {
        virtualDOM.props = { ...props };
    }

    // 处理子元素 children
    const len = children.length;
    if (len === 1) {
        // 如果只有一个子元素,直接将其赋值给 virtualDOM.props.children
        virtualDOM.props.children = children[0];
    } else if (len > 1) {
        // 如果有多个子元素,将它们作为‘一个数组 ’赋值给 virtualDOM.props.children
        virtualDOM.props.children = children;
    }

    // 返回创建的虚拟 DOM 对象
    return virtualDOM;
}

注意事项

  • $$typeof 属性:这是 React 内部用于标识虚拟 DOM 对象的一个特殊属性。它使用 Symbol.for('react.element') 来确保每个虚拟 DOM 对象都有一个唯一的标识符。这对于 React 的内部机制非常重要,因为它可以帮助 React 区分不同的对象类型。
  • keyref 属性:虽然在这段代码中 keyref 被初始化为 null,但在实际应用中,你可以根据需要为它们赋值。key 通常用于列表渲染中的唯一标识符,而 ref不仅仅是创建响应式ref工具,还 用于获取对 DOM 节点或组件实例的引用。
  • 子元素处理:这段代码处理了两种情况:单个子元素和多个子元素。如果你希望更严格地处理子元素,可以考虑添加更多的逻辑来处理 undefinednull 的情况,或者确保子元素的类型是有效的(例如,过滤掉非 React 元素)。
  • 扩展属性:使用 ...props 来扩展属性对象是一种常见的做法,但它可能会覆盖虚拟 DOM 对象中已有的属性。如果你需要保留某些特定的属性(如 children),可以在扩展之前进行检查或合并。

通过这种方式,createElement 函数可以创建一个完整的虚拟 DOM 对象,供 React 渲染引擎使用。这个虚拟 DOM 对象描述了如何在实际的 DOM 中创建和更新元素,从而实现了高效的 UI 更新。


我相信看到这里的小伙伴中肯定会出现绝世高手~

希望这篇文章对大家有帮助,欢迎评论区探讨学习,学会的话也还请给本文一个点赞支持哦~