React系列文章:Babel编译JSX生成代码

时间:2022-12-26 02:40:57

上次我们总结了 React 代码构建后的 webpack 模块组织关系,今天来介绍一下 Babel 编译 JSX 生成目标代码的一些规则,并且写一个简单的解析器,模拟整个生成的过程。

我们还是拿最简单的代码举例:

import {greet} from './utils';

const App = <h1>{greet('scott')}</h1>;

ReactDOM.render(App, document.getElementById('root'));

这段代码在经过Babel编译后,会生成如下可执行代码:

var _utils = __webpack_require__(1);

var App = React.createElement(
'h1',
null,
(0, _utils.greet)('scott')
); ReactDOM.render(App, document.getElementById('root'));

看的出来,App 是一个 JSX 形式的元素,在编译后,变成了 React.createElement() 方法的调用,从参数来看,它创建了一个 h1 标签,标签的内容是一个方法调用返回值。我们再来看一个复杂一些的例子:

import {greet} from './utils';

const style = {
color: 'red'
}; const App = (
<div className="container">
<h1 style={style}>{greet('scott')} hah</h1>
<p>This is a JSX demo</p>
<div>
<input type="button" value="click me" />
</div>
</div>
); ReactDOM.render(App, document.getElementById('root'));

编译之后,会生成如下代码:

var _utils = __webpack_require__(1);

var style = {
color: 'red'
}; var App = React.createElement(
'div',
{ className: 'container' },
React.createElement(
'h1',
{ style: style },
(0, _utils.greet)('scott'),
' hah'
),
React.createElement(
'p',
null,
'This is a JSX demo'
),
React.createElement(
'div',
null,
React.createElement(
'input',
{ type: 'button', value: 'click me' }
)
)
); ReactDOM.render(App, document.getElementById('root'));

从上面代码可以看出,React.createElement 方法的签名大概是下面这个样子:

React.createElement(tag, attrs, ...children);

第一参数是标签名,第二个参数是属性对象,后面的参数是 0 到多个子结点。如果是自闭和标签,只生成前两个参数即可,如下:

// JSX
const App = <input type="button" value="click me" />; // 编译结果
var App = React.createElement('input', { type: 'button', value: 'click me' });

现在,我们大概了解了由 JSX 到目标代码这中间的一些变化,那么我们是不是能够模拟这个过程呢?

要模拟整个过程,需要两个步骤:首先将 JSX 解析成树状数据结构,然后根据这个树状结构生成目标代码。

下面我们就来实际演示一下,假如有如下代码片段:

const style = {
color: 'red'
}; function greet(name) {
return `hello ${name}`;
} const App = (
<div className="container">
<p style={style}>saying {greet('scott')} hah</p>
<div>
<p>this is jsx-like code</p>
<i className="icon"/>
<p>parsing it now</p>
<img className="icon"/>
</div>
<input type="button" value="i am a button"/>
<em/>
</div>
);

我们在 JSX 中引用到了 style 变量和 greet() 函数,对于这些引用,在后期生成可执行代码时,会保持原样输出,直接引用当前作用域中的变量或函数。注意,我们可能覆盖不到 JSX 所有的语法规则,这里只做一个简单的演示即可,解析代码如下:

// 解析JSX
const parseJSX = function () {
const TAG_LEFT = '<';
const TAG_RIGHT = '>';
const CLOSE_SLASH = '/';
const WHITE_SPACE = ' ';
const ATTR_EQUAL = '=';
const DOUBLE_QUOTE = '"';
const LEFT_CURLY = '{';
const RIGHT_CURLY = '}'; let at = -1; // 当前解析的位置
let stack = []; // 放置已解析父结点的栈
let source = ''; // 要解析的JSX代码内容
let parent = null; // 当前元素的父结点 // 寻找目标字符
let seek = (target) => {
let found = false; while (!found) {
let ch = source.charAt(++at); if (ch === target) {
found = true;
}
}
}; // 向前搜索目标信息
let explore = (target) => {
let index = at;
let found = false;
let rangeStr = ''; while (!found) {
let ch = source.charAt(++index); if (target !== TAG_RIGHT && ch === TAG_RIGHT) {
return {
at: -1,
str: rangeStr,
};
} if (ch === target) {
found = true;
} else if (ch !== CLOSE_SLASH) {
rangeStr += ch;
}
} return {
at: index - 1,
str: rangeStr,
};
}; // 跳过空格
let skipSpace = () => {
while (true) {
let ch = source.charAt(at + 1); if (ch === TAG_RIGHT) {
at--;
break;
} if (ch !== WHITE_SPACE) {
break;
} else {
at++;
}
}
}; // 解析标签体
let parseTag = () => {
if (stack.length > 0) {
let rangeResult = explore(TAG_LEFT); let resultStr = rangeResult.str.replace(/^\n|\n$/, '').trim(); if (resultStr.length > 0) {
let exprPositions = []; resultStr.replace(/{.+?}/, function(match, startIndex) {
let endIndex = startIndex + match.length - 1;
exprPositions.push({
startIndex,
endIndex,
});
}); let strAry = [];
let currIndex = 0; while (currIndex < resultStr.length) {
// 没有表达式了
if (exprPositions.length < 1) {
strAry.push({
type: 'str',
value: resultStr.substring(currIndex),
});
break;
} let expr = exprPositions.shift(); strAry.push({
type: 'str',
value: resultStr.substring(currIndex, expr.startIndex),
}); strAry.push({
type: 'expr',
value: resultStr.substring(expr.startIndex + 1, expr.endIndex),
}); currIndex = expr.endIndex + 1;
} parent.children.push(...strAry); at = rangeResult.at; parseTag(); return parent;
}
} seek(TAG_LEFT); // 闭合标记 例如: </div>
if (source.charAt(at + 1) === CLOSE_SLASH) {
at++; let endResult = explore(TAG_RIGHT); if (endResult.at > -1) {
// 栈结构中只有一个结点 当前是最后一个闭合标签
if (stack.length === 1) {
return stack.pop();
} let completeTag = stack.pop(); // 更新当前父结点
parent = stack[stack.length - 1]; parent.children.push(completeTag); at = endResult.at; parseTag(); return completeTag;
}
} let tagResult = explore(WHITE_SPACE); let elem = {
tag: tagResult.str,
attrs: {},
children: [],
}; if (tagResult.at > -1) {
at = tagResult.at;
} // 解析标签属性键值对
while (true) {
skipSpace(); let attrKeyResult = explore(ATTR_EQUAL); if (attrKeyResult.at === -1) {
break;
} at = attrKeyResult.at + 1; let attrValResult = {}; if (source.charAt(at + 1) === LEFT_CURLY) {
// 属性值是引用类型 seek(LEFT_CURLY); attrValResult = explore(RIGHT_CURLY); attrValResult = {
at: attrValResult.at,
info: {
type: 'ref',
value: attrValResult.str,
}
};
} else {
// 属性值是字符串类型 seek(DOUBLE_QUOTE); attrValResult = explore(DOUBLE_QUOTE); attrValResult = {
at: attrValResult.at,
info: {
type: 'str',
value: attrValResult.str,
}
};
} at = attrValResult.at + 1; skipSpace(); elem.attrs[attrKeyResult.str] = attrValResult.info;
} seek(TAG_RIGHT); // 检测是否为自闭合标签
if (source.charAt(at - 1) === CLOSE_SLASH) {
// 自闭合标签 追加到父标签children中 然后继续解析
if (stack.length > 0) {
parent.children.push(elem); parseTag();
}
} else {
// 有结束标签的 入栈 然后继续解析
stack.push(elem); parent = elem; parseTag();
} return elem;
}; return function (jsx) {
source = jsx;
return parseTag();
};
}();

在解析 JSX 时,有以下几个关键步骤:

1. 解析到 `<` 时,表明一个标签的开始,接下来开始解析标签名,比如 div。
2. 在解析完标签名之后,试图解析属性键值对,如果存在,则检测 `=` 前后的值,属性值可能是字符串,也可能是变量引用,所以需要做个区分。
3. 解析到 `>` 时,表明一个标签的前半部分结束,此时应该将当前解析到的元素入栈,然后继续解析。
4. 解析到 `/>` 时,表明是一个自闭合元素,此时直接将其追加到栈顶父结点的 children 中。
5. 解析到 `</` 时,表明是标签的后半部分,一个完整标签结束了,此时弹出栈顶元素,并将这个元素追加到当前栈顶父结点的 children 中。
6. 最后一个栈顶元素出栈,整个解析过程完毕。

接下来,我们调用上面的 parseJSX() 方法,来解析示例代码:

const App = (`
<div className="container">
<p style={style}>{greet('scott')}</p>
<div>
<p>this is jsx-like code</p>
<i className="icon"/>
<p>parsing it now</p>
<img className="icon"/>
</div>
<input type="button" value="i am a button"/>
<em/>
</div>
`); let root = parseJSX(App); console.log(JSON.stringify(root, null, 2));

生成的树状数据结构如下所示:

{
"tag": "div",
"attrs": {
"className": {
"type": "str",
"value": "container"
}
},
"children": [
{
"tag": "p",
"attrs": {
"style": {
"type": "ref",
"value": "style"
}
},
"children": [
{
"type": "str",
"value": "saying "
},
{
"type": "expr",
"value": "greet('scott')"
},
{
"type": "str",
"value": " hah"
}
]
},
{
"tag": "div",
"attrs": {},
"children": [
{
"tag": "p",
"attrs": {},
"children": [
{
"type": "str",
"value": "this is jsx-like code"
}
]
},
{
"tag": "i",
"attrs": {
"className": {
"type": "str",
"value": "icon"
}
},
"children": []
},
{
"tag": "p",
"attrs": {},
"children": [
{
"type": "str",
"value": "parsing it now"
}
]
},
{
"tag": "img",
"attrs": {
"className": {
"type": "str",
"value": "icon"
}
},
"children": []
}
]
},
{
"tag": "input",
"attrs": {
"type": {
"type": "str",
"value": "button"
},
"value": {
"type": "str",
"value": "i am a button"
}
},
"children": []
},
{
"tag": "em",
"attrs": {},
"children": []
}
]
}

在生成这个树状数据结构之后,接下来我们要根据这个数据描述,生成最终的可执行代码,下面代码可用来完成这个阶段的处理:

// 将树状属性结构转换输出可执行代码
function transform(elem) {
// 处理属性键值对
function processAttrs(attrs) {
let result = []; let keys = Object.keys(attrs); keys.forEach((key, index) => {
let type = attrs[key].type;
let value = attrs[key].value; // 需要区分字符串和变量引用
let keyValue = `${key}: ${type === 'ref' ? value : '"' + value + '"'}`; if (index < keys.length - 1) {
keyValue += ',';
} result.push(keyValue);
}); if (result.length < 1) {
return 'null';
} return '{' + result.join('') + '}';
} // 处理结点元素
function processElem(elem, parent) {
let content = ''; // 处理子结点
elem.children.forEach((child, index) => {
// 子结点是标签元素
if (child.tag) {
content += processElem(child, elem);
return;
} // 以下处理文本结点 if (child.type === 'expr') {
// 表达式
content += child.value;
} else {
// 字符串字面量
content += `"${child.value}"`;
} if (index < elem.children.length - 1) {
content += ',';
}
}); let isLastChildren = elem === parent.children[parent.children.length -1]; return (
`React.createElement(
'${elem.tag}',
${processAttrs(elem.attrs)}${content.trim().length ? ',' : ''}
${content}
)${isLastChildren ? '' : ','}`
);
} return processElem(elem, elem).replace(/,$/, '');
}

我们来调用一下 transform() 方法:

let root = parseJSX(App);

let code = transform(root);

console.log(code);

运行完上述代码,我们会得到一个目标代码字符串,格式化显示后代码结构是这样的:

React.createElement(
'div',
{className: "container"},
React.createElement(
'p',
{style: style},
"saying ",
greet('scott'),
" hah"
),
React.createElement(
'div',
null,
React.createElement(
'p',
null,
"this is jsx-like code"
),
React.createElement(
'i',
{className: "icon"}
),
React.createElement(
'p',
null,
"parsing it now"
),
React.createElement(
'img',
{className: "icon"}
)
),
React.createElement(
'input',
{type: "button", value: "i am a button"}
),
React.createElement(
'em',
null
)
);

我们还需要将上下文代码拼接在一起,就像下面这样:

const style = {
color: 'red'
}; function greet(name) {
return `hello ${name}`;
} const App = React.createElement(
'div',
{className: "container"},
React.createElement(
'p',
{style: style},
"saying ",
greet('scott'),
" hah"
),
React.createElement(
'div',
null,
React.createElement(
'p',
null,
"this is jsx-like code"
),
React.createElement(
'i',
{className: "icon"}
),
React.createElement(
'p',
null,
"parsing it now"
),
React.createElement(
'img',
{className: "icon"}
)
),
React.createElement(
'input',
{type: "button", value: "i am a button"}
),
React.createElement(
'em',
null
)
);

看上去是有几分模样了哈,那么如何实现 React.createElement() 方法,将上面的代码运行起来并输出预期的效果呢,我们会在下一篇文章中介绍。

React系列文章:Babel编译JSX生成代码的更多相关文章

  1. webpack打包调试react并使用babel编译jsx配置方法

    http://lxj8749.iteye.com/blog/2287074 ********************************************** 安装webpack npm i ...

  2. apt 根据注解,编译时生成代码

    apt: @Retention后面的值,设置的为CLASS,说明就是编译时动态处理的.一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的~~ ...

  3. React系列文章:JSX生成真实DOM结点

    在上一篇文章中,我们介绍了Babel是如何将JSX代码编译成可执行代码的,随后也实现了一个自己的解析器,模拟了Babel编译的过程. 现在我们再来回顾一下,假定有如下业务代码: const style ...

  4. React系列文章:无状态组件生成真实DOM结点

    在上一篇文章中,我们总结并模拟了JSX生成真实DOM结点的过程,今天接着来介绍一下无状态组件的生成过程. 先以下面一段简单的代码举例: const Greeting = function ({name ...

  5. 一步步实现windows版ijkplayer系列文章之五——使用automake生成makefile

    一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...

  6. React系列文章:Webpack模块组织关系

    现代前端开发离不开打包工具,以Webpack为代表的打包工具已经成为日常开发必备之利器,拿React技术栈为例,我们ES6形式的源代码,需要经过Webpack和Babel处理,才能生成发布版文件,在浏 ...

  7. react系列(一)JSX语法、组件概念、生命周期介绍

    JSX React中,推出了一种新的语法取名为JSX,它给了JS中写HTML标签的能力,不需要加引号.JSX的语法看起来是一种模板,然而它在编译以后,会转成JS语法,只是书写过程中的语法糖. JSX的 ...

  8. React 系列文章&lpar;1&rpar;: npm 手动搭建React 运行实例 &lpar;新手必看&rpar;

    摘 要 刚接触React 开发, 在摸索中构建react 运行环境,总会遇到各种坑:本文,将用最短时间解决webpack+react 环境搭建问题. 1.如果你还没有React基础 看这里. 2.如果 ...

  9. TiDB 源码阅读系列文章(一)序

    原创: 申砾 PingCAP  2018-02-28 在 TiDB DevCon2018 上,我们对外宣布了 TiDB 源码阅读分享活动,承诺对外发布一系列文章以及视频帮助大家理解 TiDB 源码.大 ...

随机推荐

  1. Leetcode Simplify Path

    Given an absolute path for a file (Unix-style), simplify it. For example,path = "/home/", ...

  2. jquery遍历对象&comma;数组&comma;集合

    1.jquery 遍历对象 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <HTM ...

  3. poj3855Blast the Enemy&excl;(多边形重心)

    链接 #include <iostream> #include<cstdio> #include<cstring> #include<algorithm&gt ...

  4. Spring Tool Suite中的Tomcat启动状态修改java代码保存立刻生效

  5. Android他们控制的定义(一)

    培养自己的控制步骤定义: 1.要理解View作品  2. 分享到继承View子类 3. 要定义自己的View类添加属性  4. 绘制控件  5. 响应用户消息  6 .自己定义回调函数  一.View ...

  6. 4&period;锁定--Java的LockSupport&period;park&lpar;&rpar;实现分析

    LockSupport类是Java6(JSR166-JUC)引入的一个类,提供了主要的线程同步原语. LockSupport实际上是调用了Unsafe类里的函数.归结到Unsafe里,仅仅有两个函数: ...

  7. C语言一些知识点总结

    一.关键字 1. 什么是关键字 1> 关键字就是C语言提供的有特殊含义的符号,也叫做“保留字” 2> C语言一共提供了32个关键字,这些关键字都被C语言赋予了特殊含义 auto doubl ...

  8. 关于构造函数和原型prototype对象的理解

    构造函数     1.什么是构造函数 构造函数,主要用于对象创建的初始化,和new运算符一起用于创建对象,一个类可以有多个构造函数,因为函数名相同,所以只能通过参数的个数和类型不同进行区分,即构造函数 ...

  9. &lpar;转载&rpar;CPU、内存、硬盘、指令以及他们之间的关系

    CPU.内存.硬盘.指令以及他们之间的关系 最近读完<程序是怎样跑起来的>以及<深入理解计算机系统>的3.6.9章节后对计算机的组成有了更深入细致的了解,现总结一下对CPU.内 ...

  10. Python dict 将元祖转成字典

    dict 关键字 dict3=dict(((),(),())) #dict 只有一个参数 输出:{'a': 97, 'b': 98, 'c': 99}