React JSX:每天都在用,但你真的了解它吗?

我们每天都在写JSX,JSX可以帮助我们方便快捷的构建组件、搭建页面。我们应该都知道JSX是HTML的语法糖,而本篇文章会带你更加了解JSX,帮助你理清JSX到真实DOM之间的转化,也帮助你在面试的时候可以脱颖而出。

JSX是什么?

React官网定义:JSX is a syntax extension to JavaScript. It is similar to a template language, but it has full power of JavaScript.(JSX是js的语法扩展。它类似于模板语言,但它拥有js的全部能力。)

通过Babel了解JSX的本质:Babel编译下的JSX

Babel是一个工具链,主要用于将ECMAScript2015+版本的代码转换为向后兼容的JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。比如模版字符串的语法在一些低版本浏览器里就不兼容,babel可以将它处理成es5语法。

相同的,babel也有将jsx转换成js代码的能力。

根据上图的编译结果我们可以看到,JSX会被编译为 React.createElement方法调用。React.createElement将返回一个叫作"React Element"的JS对象,也就是"虚拟DOM(节点)"

因此,JSX可以理解为React.createElement方法调用的js语法糖。

为什么不直接使用React.createElement创建元素?

  • jsx代码层次分明,嵌套关系清晰,可读性非常高。
  • jsx语法糖允许前端开发者使用我们最为熟悉的类 HTML标签语法来创建虚拟DOM。降低学习成本、提升研发效率、研发体验。

JSX背后的模块都做了什么事情?(源码调用链路分析)

既然我们知道了JSX是React.createElement的语法糖,那我们研究JSX实际就是研究React.createElement方法。下面是对React.createElement源码的分析:

js 复制代码
export function createElement(type, config, children) {
  // propName 变量用于储存后面需要用到的元素属性
  let propName; 
  // props 变量用于储存元素属性的键值对集合
  const props = {}; 
  // key、ref、self、source 均为 React 元素的属性,此处不必深究
  let key = null;
  let ref = null; 
  let self = null; 
  let source = null; 
 
  // config 对象中存储的是元素的属性
  if (config != null) { 
    // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    // 此处将 key 值字符串化
    if (hasValidKey(config)) {
      key = '' + config.key; 
    }
 
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
    for (propName in config) {
      if (
        // 筛选出可以提进 props 对象里的属性
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName) 
      ) {
        props[propName] = config[propName]; 
 
      }
    }
  }
  // childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
 
  const childrenLength = arguments.length - 2; 
  // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
  if (childrenLength === 1) { 
    // 直接把这个参数的值赋给props.children
    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
    props.children = childArray; 
 
  } 
  // 处理 defaultProps
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) { 
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
 
      }
    }
  }
 
  // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

入参共有三个:

  1. type:用于标识节点的类型。它可以是类似"h1""div"这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment 类型;
  2. config:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中;
  3. children:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的"子节点""子元素"。

下面通过调用实例帮助你更加理解入参:

js 复制代码
// DOM结构
<ul className="list">
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

//等价于以下的方法入参
React.createElement(
    //type
    "ul",
    // 传入config属性键值对
    { className: "list" },
    // children,本质是createElement的嵌套调用
    React.createElement("li", {
      key: "1"
    }, "1"), React.createElement("li", {
      key: "2"
    }, "2")
);

最后,React.createElement方法会返回ReactElement方法调用,那ReactElement方法内部又做了什么呢?以下是源码分析:

js 复制代码
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
    $$typeof: REACT_ELEMENT_TYPE,
    // 内置属性赋值
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录创造该元素的组件
    _owner: owner,
 
  };
 
  if (__DEV__) {
    // 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
  }
  return element;
 
};

ReactElement 其实只做了一件事情,那就是把传入的参数按照一定的规范,"组装"进了 element 对象里,并把它返回给了 React.createElement。请查看以下用例:

js 复制代码
const AppJSX = (<div className="App">
  <h1 className="title">I am the title</h1>
  <p className="content">I am the content</p>
</div>)
 
console.log(AppJSX)

上图就是reactElement对象实例。其本质上是以js对象形式存在的对DOM的描述,也就是"虚拟DOM(节点)"。

jsx创建的虚拟DOM怎么变成真实DOM的?

虚拟DOM转换成真实DOM是通过React.render方法,它的调用方法是这样的------

js 复制代码
ReactDOM.render(
    // 需要渲染的元素(ReactElement)
    element, 
    // 元素挂载的目标容器(一个真实DOM)
    container,
    // 回调函数,可选参数,可以用来处理渲染结束后的逻辑
    [callback]
)

第二个参数就是一个真实的 DOM 节点,这个真实的 DOM 节点充当"容器"的角色,React 元素最终会被渲染到这个"容器"里面去。以下是一个简单的用例:

js 复制代码
//index.html
<body>
    <div id="root"></div>
</body>

//main.js
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

总结

以上就是JSX从使用、到背后的调用链路、以及渲染成真实DOM的全过程。

我们可以得出结论------JSX本质是React.creatElement方法的js语法糖,其方法背后调用了ReactElement方法,通过这两个方法把我们编写的jsx拆解并组装,最后返回给我们一个js对象,这个对象里面包含对DOM的描述,即"虚拟DOM(节点)",最终通过React.render()方法,将虚拟DOM节点转化为真实节点。

感谢观看,如果想要继续了解React.render()的工作流程,以及其他React原理(如React Fiber架构、生命周期等),可以点进主页,会陆续在其他文章中分享。

相关推荐
xy345324 分钟前
Axure 9.0 中继器核心结构
前端·ui·html·axure·原型·产品设计
老王以为31 分钟前
走进 AI Agent 第四篇(上):知识获取管道——RAG 基础
前端·人工智能·全栈
夏天要喝冰可乐43 分钟前
一处写作,多平台分发:我给掘金做了一款开源 Chrome 插件
前端·chrome·vibecoding
货拉拉技术1 小时前
货拉拉开源Hadice:安卓 & 鸿蒙桌面调试工具
前端
程序猿DD2 小时前
OctaFuse Gateway 2.11.0:流式请求优化、用户折扣策略与错误契约升级
前端·后端
尾善爱看海2 小时前
《JavaScript 数组操作全攻略:12 类 API + 30 个实战场景 + 20 个避坑指南》
前端·javascript·面试
计算机魔术师2 小时前
Anthropic 估值冲 4 万亿,但企业客户已经偷偷换了便宜模型
前端
王六米。2 小时前
RAG知识库建设 文档解析 切分与索引如何衔接
服务器·前端·网络·企业数字化转型·智钳智能盒子
名字还没想好☜2 小时前
React 文件上传实战:预览 URL 回收、多文件逐个进度与拖拽放置区踩坑
前端·react·next.js
console.log('npc')2 小时前
06 — Model 层:数据模型与操作
前端·后端·node.js·express