谈谈JSX的编译原理

3,481 阅读3分钟

说起JSX,几乎无人不知:它在React扮演着描述了UI界面、关联渲染逻辑的重要角色。但JSX如何被编译、又如何变成一个个的元素,相信就没那么多人知道了。本文将对JSX编译结果分析,以加深对React里JSX工作原理的理解

准备JSX编译环境

需要安装三个包(开发依赖)

  • @babel/cli
  • @babel/core
  • @babel/preset-react

安装完毕在根目录添加配置文件.babelrc,内容如下:

{
  "presets": ["@babel/preset-react"]
}

最后在终端里输入命令: npx babel .\index.jsx -w -o .\index.jsindex.jsx编译到index.js,并开启修改监听模式

基本示例

const h1 = <h1>this is a H1 Tag</h1>

经过babel编译:

const h1 = React.createElement("h1", null, "this is a H1 Tag");

可以看到babel会对JSX进行解析,而后将DOM节点相关的数据片作为参数传递给React.createElement方法调用,这个方法是干嘛的呢?

React.createElement被链接到一个名为createElementWithValidation的函数,它有三类参数:

  • type:表示节点的类型
  • props:表示节点的属性
  • children-表示节点的子元素(可能有多个)
function createElementWithValidation(type, props, children) {
  //...
  var element = createElement.apply(this, arguments);
  //...
  return element;
}

这样编译结果就很好理解了: h1是节点的类型(type),属性为空(null),有一个子元素且为文本节点(children):"this is a H1 Tag"。

所以JSX最后会成为一个element(react元素),本质上是一个javascript对象。

JSX各种嵌入场景

我们可以将普通表达式、jsx本身作为整体被传入React.createElementchildren或者props属性

  • 表达式作为属性值
var className = "div_class"
const div = <div class={'prefix_'+className}></div>

编译为:

var className = "div_class";
const div = React.createElement("div", {
  class: 'prefix_' + className
});
  • JSX嵌套JSX(作为另一个JSX的子元素)
const childJsx = <span>child span</span>
const parentJsx = <div>{childJsx}</div>

编译为:

const childJsx = React.createElement("span", null, "child span");
const parentJsx = React.createElement("div", null, childJsx);
  • JSX最外层只能包含一个节点
//以下会报错,JSX expressions must have one parent element
const multipleWrapper = <p>first p</p><p>second p</p>

每个JSX都代表了一个树节点,这个节点可以作为一个其他节点的载体又可以挂载到其他树节点上,树节点有且只能有一个root。

  • 搭配逻辑运算符
let condition = true;
const jsxExamp = <div>{condition &&[1,2,3].join("-") }</div>

编译为:

let condition = true;
const jsxExamp = React.createElement("div", null, condition && [1, 2, 3].join("-"));

函数、class组件的编译

以上提到的都是JSX字面量的编译,JSX的编译不限于此,也可能发生在Class组件、函数组件的内部如:

function FnComp(){
  return <div>A Function Component</div>
}
class ClassComp extends React.Component{
  render(){
    return <div>
      <h1>A Class Component</h1>
      <FnComp/>
    </div>
  }
}

被编译为:

function FnComp() {
  return React.createElement("div", null, "A Function Component");
}
class ClassComp extends React.Component {
  render() {
    return React.createElement("div", null, React.createElement("h1", null, "A Class Component"), React.createElement(FnComp, null));
  }
}

最后

当然问题不止于此,以后开发再也不怕遇到JSX编译的错误了,因为我们可以借助Babel工具查看编译结果并分析查找出错原因咯。

再总结一下:

JSX会先经过babel的预编译和解析,然后将结果通过React.createElement完成替换,而React.createElement则创建成一个个的reactElement,而这些reactElement本质上是JavaScript对象,包含了节点的所有信息,后续会经过一系列的加工和调度渲染到网页上。