React中JSX背后的故事


本篇文章主要讲解 JSX的来龙去脉,不讲解 JSX的语法(不了解的可参考: JSX 简介)。如果你对以下三个问题能够清晰解答,本篇文章就可以不用看了。

  1. JSX 的本质是什么,它和 JS 之间到底是什么关系?
  2. 为什么要用 JSX?不用会有什么后果?
  3. JSX 背后的功能模块是什么,这个功能模块都做了哪些事情?

如果不能清晰地回答,那希望看完本篇文章你能有所收获。


先举个小例子让你回忆一下JSX,以下代码中函数组件App中返回的部分即为JSX(<div className="App">Hello World!</div>):

import React from "react";
import ReactDOM from "react-dom";

function App() {
  return <div className="App">Hello World!</div>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

JSX

1. JSX的本质: React.createElement这个 JavaScript 调用的语法糖

JSX的本质是JavaScript 的语法扩展React官方对JSX的描述

JSX is a syntax extension to JavaScript. It is similar to a template language, but it has full power of JavaScript.
JSX 是一个 JavaScript 语法扩展。它类似于模板语言,但它具有 JavaScript 的全部能力。

这个描述的意思是使用JSX语法可以扩展JavaScript 的功能:在JavaScript 中可以像写HTML一样来构建UI(原生JS是不具备这种能力的),但编译后最终其实还是纯JS代码。JSX 的定位是 JavaScript 的“扩展”,而非 JavaScript 的“某个版本”,所以浏览器并不会像天然支持 JavaScript 一样地支持 JSX。要使JSX在JavaScript中生效,我们需要借助Babel(Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。类似的,Babel 也具备将 JSX 语法转换为 JavaScript 代码的能力。)进行编译:JSX 会被编译为 React.createElement(), React.createElement() 将返回一个叫作“React Element”的 JS 对象。

如下一个Babel编译的例子
Babel编译
可以看到,所有的 JSX 标签都被转化成了 React.createElement 调用,这也就意味着,我们写的 JSX 其实写的就是 React.createElement。

这里我们可以得出结论:JSX 的本质其实是React.createElement这个 JavaScript 调用的语法糖

2. 为什么要用 JSX: 在降低学习成本的同时,也提升了研发效率与研发体验

从上文Babel编译的例子中我们已经初步体会到使用JSX相对于直接使用JS编写代码的的简洁性差别,看起来好像还可以接受,那让我们看看稍微复杂点的例子:
Babel
可以很明显地看出,JSX 代码层次分明、嵌套关系清晰;而 React.createElement 代码则给人一种非常混乱的“杂糅感”,这样的代码不仅读起来不友好,写起来也费劲。实际项目中只会比例子更加复杂,相信没有人愿意用React.createElement的方式来编写吧?

这里我们可以得出结论:JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。

3. JSX 背后的功能模块是什么,这个功能模块都做了哪些事情?

从上文我们已知所有的 JSX 标签最终都被转化成了 React.createElement 调用,那React.createElement这个函数到底做了些什么呢?让我们进入React的源码(版本17.0.1)一探究竟:

/**
 * 源码位置:packages\react\src\ReactElement.js
 * React的创建元素方法
 */
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];
      }
    }
  }
  if (__DEV__) {
  	// 这里是一些针对 __DEV__ 环境下的处理,暂时可以不用管
  }
  // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

上面源码不复杂,其实该函数并没有做很多复杂的事情,基本上是在进行格式化数据的操作,执行到最后会 return 一个针对 ReactElement 的调用。让我们通过一个流程图来更清晰地理解createElement的逻辑处理过程:
createElement处理过程
createElement 就像是开发者和 ReactElement 调用之间的一个“转换器”、一个数据处理层。它可以从开发者处接受相对简单的参数,然后将这些参数按照 ReactElement 的预期做一层格式化,最终通过调用 ReactElement 来实现元素的创建。整个过程如下图所示:
React.createElement

3.1 入参解读:创造一个元素需要知道哪些信息

export function createElement(type, config, children)

createElement函数包含3入参,分别如下:

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

3.2 出参解读:初识虚拟 DOM

从源码中我们可以看到出参是一个针对 ReactElement 的调用,让我们继续看看ReactElement 源码

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 其实只做了一件事情,那就是创建,说得更精确一点,是组装:ReactElement 把传入的参数按照一定的规范,“组装”进了 element 对象里,并把它返回给了 React.createElement,最终 React.createElement 又把它交回到了开发者手中。整个过程如下图所示:
ReactElement
我们可以将JSX打印出来看看,来验证一下:

import React from "react";
import ReactDOM from "react-dom";

function App() {
  return <div className="App">Hello World!</div>;
}

const myJSX = <div className="App">Hello World!</div>
console.log(myJSX)

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

结果如下:
打印结果
这个 ReactElement 对象实例,本质上是以JavaScript 对象形式存在的对 DOM 的描述,也就是老生常谈的“虚拟 DOM”(准确地说,是虚拟 DOM 中的一个节点)。

既然是“虚拟 DOM”,那就意味着和渲染到页面上的真实 DOM 之间还有一些距离,这个“距离”,就是由大家喜闻乐见的ReactDOM.render方法来填补的。

在每一个 React 项目的入口文件中,都少不了对 ReactDOM.render 函数的调用。下面简单介绍下 ReactDOM.render 方法的入参规则:

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

ReactDOM.render 方法可以接收 3 个参数,其中第二个参数就是一个真实的 DOM 节点,这个真实的 DOM 节点充当“容器”的角色,React 元素最终会被渲染到这个“容器”里面去。这个真实 DOM 一定是确实存在的。比如,在 App 组件对应的 index.html 中,已经提前预置 了 id 为 root 的根节点:

<body>
    <div id="root"></div>
</body>

这里我们可以得出结论:JSX 背后的功能模块是什么React.createElement,该函数并没有做很多复杂的事情,基本上是在进行格式化数据的操作,执行到最后会 return 一个针对 ReactElement 的调用。

4. 总结

问题回答
JSX 的本质是什么,它和 JS 之间到底是什么关系?JSX 的本质其实是React.createElement这个 JavaScript 调用的语法糖
为什么要用 JSX?不用会有什么后果?JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。
JSX 背后的功能模块是什么,这个功能模块都做了哪些事情?JSX 背后的功能模块是什么React.createElement,该函数并没有做很多复杂的事情,基本上是在进行格式化数据的操作,执行到最后会 return 一个针对 ReactElement 的调用

若对您有帮助,支持一下哦~
打赏

参考内容:
JSX
JSX 代码是如何“摇身一变”成为 DOM 的?

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 代码科技 设计师:Amelia_0503 返回首页