React中JSX的本质与虚拟DOM

565 阅读4分钟

什么是JSX

JSX是一种JavaScript的语法扩展(extension),也在很多地方称之为JavaScript XML,因为看起就是一段XML语法。它用于描述我们的UI界面,并且其完成可以和JavaScript融合在一起使用。它不同于Vue中的模块语法,你不需要专门学习模块语法中的一些指令(比如v-for、v-if、v-else、v-bind)。

本文重点不在介绍JSX语法,而是去探讨JSX的本质。

import React, { Component } from 'react'

export default class App extends Component {
  render() {
    return (
      <div>
        <div>
          <h2>这是个标题</h2>
          <button>这是个按钮</button>
        </div>
      </div>
    )
  }
}

上面的代码中return()包裹的就是JSX的代码。

JSX的实质

实际上JSX仅仅是React.createElement函数的语法糖。React.createElement有三个参数分别是component,propschildren。所有的jsx最终都会被转换成React.createElement函数调用。 我们可以在源码中找到它所在的位置。

手把手教你从源码中找到React.createElement

PS:笔者这里下载的React的代码是16.13.1。

我们打开React的代码。

image.png

image.png

image.png

image.png

我们在这里的index.js可以找到creatElement,这里的所有东西就是React那个大的对象(import React from 'react'的那个)!当然这里的index.js只是一个中转站。如果我们按住ctrol/command点击进入createElement会进入它的真实位置。

image.png

我们点击createElementProd点击进去

它的真实位置就在ReactElement这里。 image.png

我们可以在outline(大纲处定位到响应的函数)

image.png

解读这里的createElement函数

这里的type参数为类型,config为属性(比如a 标签的 href 这种就是属性),children是个数组里面存放着子元素们。

我们以一段jsx代码来参考

import React, { Component } from 'react'
 
export default class App extends Component {
  render() {
    return (
      <div>
        <div className='header'>
          <h1 title = '标题'>我是一个标题</h1>
        </div>
        <div className='content'>
          <h2>我是页面的内容</h2>
          <button>按钮</button>
          <button>+1</button>
          <a href="http://www.bilibili.com">哔哩哔哩</a>
        </div>
        <div className='footer'>
          <p>我是尾部的元素</p>
        </div>
      </div>
    )
  }
}

笔者在前文赘述了,JSX仅仅是React.createElement函数的语法糖。为了验证这个说法,笔者在这里通过babel官网对这段代码进行转换并得到如下的结果。

class App extends _react.Component {
  render() {
    return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", {
      className: "header"
    }, /*#__PURE__*/_react.default.createElement("h1", {
      title: "\u6807\u9898"
    }, "\u6211\u662F\u4E00\u4E2A\u6807\u9898")), /*#__PURE__*/_react.default.createElement("div", {
      className: "content"
    }, /*#__PURE__*/_react.default.createElement("h2", null, "\u6211\u662F\u9875\u9762\u7684\u5185\u5BB9"), /*#__PURE__*/_react.default.createElement("button", null, "\u6309\u94AE"), /*#__PURE__*/_react.default.createElement("button", null, "+1"), /*#__PURE__*/_react.default.createElement("a", {
      href: "http://www.bilibili.com"
    }, "\u54D4\u54E9\u54D4\u54E9")), /*#__PURE__*/_react.default.createElement("div", {
      className: "footer"
    }, /*#__PURE__*/_react.default.createElement("p", null, "\u6211\u662F\u5C3E\u90E8\u7684\u5143\u7D20")));
  }
}

在这里我们会发现第三个参数里我们又可以传入React.createElement进去,以此套娃。 我们回到源码里,明明只有三个参数啊(type,config,children)。一个小小的children能传入这么多套娃的东西吗?

他这里其实做了一件事情。

createElement函数的意义是什么

刚才讲了那么多,那这个函数创建出来的目的是什么呢?我们回到源码里。

image.png

其实就是为了导出ReactElement对象。

React想利用ReactElement这个对象,想形成一个JS的对象树,这个对象树就是大名鼎鼎的虚拟DOM。

我们可以拦截这个对象看看。

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;

var _react = _interopRequireWildcard(require("react"));

function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }

function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

class App extends _react.Component {
  render() {
    var elementObj =  /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement("div", {
      className: "header"
    }, /*#__PURE__*/_react.default.createElement("h1", {
      title: "\u6807\u9898"
    }, "\u6211\u662F\u4E00\u4E2A\u6807\u9898")), /*#__PURE__*/_react.default.createElement("div", {
      className: "content"
    }, /*#__PURE__*/_react.default.createElement("h2", null, "\u6211\u662F\u9875\u9762\u7684\u5185\u5BB9"), /*#__PURE__*/_react.default.createElement("button", null, "\u6309\u94AE"), /*#__PURE__*/_react.default.createElement("button", null, "+1"), /*#__PURE__*/_react.default.createElement("a", {
      href: "http://www.bilibili.com"
    }, "\u54D4\u54E9\u54D4\u54E9")), /*#__PURE__*/_react.default.createElement("div", {
      className: "footer"
    }, /*#__PURE__*/_react.default.createElement("p", null, "\u6211\u662F\u5C3E\u90E8\u7684\u5143\u7D20")));

    console.log(elementObj); //拦截对象打印

    return elementObj
  }

}

exports.default = App;

我们会发现这里是一一对应的。

并且它们还是有子元素的,这些东西就形成了一个 虚拟DOM树 image.png

但是这个虚拟DOM怎么和真实的DOM对应起来呢?

依靠的就是这个render。通过render映射到root处,形成普通DOM。

image.png

总结

React从虚拟DOM转换成真实DOM的过程实际上是经过了:

从JSX -> createElement函数 -> ReactElement对象树 -> ReactDOM.render -> 普通DOM

比如说RN,其实和React就是在最后一步不同,React是转换成普通DOM,RN是转换成原生的控件。

为什么要使用虚拟DOM而不是原生DOM

最主要有两点原因

  • 原生DOM很难跟踪状态发生的改变,不方便我们对程序进行调试。
  • 操作真实DOM性能较低,传统的开发模式会进行频繁的DOM操作,这一做法性能非常的低。

所以在开发中,我们应该避免频繁的DOM操作。