React系列文章:Babel编译JSX生成代码
上次我们总结了 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生成代码的更多相关文章
- webpack打包调试react并使用babel编译jsx配置方法
http://lxj8749.iteye.com/blog/2287074 ********************************************** 安装webpack npm i ...
- apt 根据注解,编译时生成代码
apt: @Retention后面的值,设置的为CLASS,说明就是编译时动态处理的.一般这类注解会在编译的时候,根据注解标识,动态生成一些类或者生成一些xml都可以,在运行时期,这类注解是没有的~~ ...
- React系列文章:JSX生成真实DOM结点
在上一篇文章中,我们介绍了Babel是如何将JSX代码编译成可执行代码的,随后也实现了一个自己的解析器,模拟了Babel编译的过程. 现在我们再来回顾一下,假定有如下业务代码: const style ...
- React系列文章:无状态组件生成真实DOM结点
在上一篇文章中,我们总结并模拟了JSX生成真实DOM结点的过程,今天接着来介绍一下无状态组件的生成过程. 先以下面一段简单的代码举例: const Greeting = function ({name ...
- 一步步实现windows版ijkplayer系列文章之五——使用automake生成makefile
一步步实现windows版ijkplayer系列文章之一--Windows10平台编译ffmpeg 4.0.2,生成ffplay 一步步实现windows版ijkplayer系列文章之二--Ijkpl ...
- React系列文章:Webpack模块组织关系
现代前端开发离不开打包工具,以Webpack为代表的打包工具已经成为日常开发必备之利器,拿React技术栈为例,我们ES6形式的源代码,需要经过Webpack和Babel处理,才能生成发布版文件,在浏 ...
- react系列(一)JSX语法、组件概念、生命周期介绍
JSX React中,推出了一种新的语法取名为JSX,它给了JS中写HTML标签的能力,不需要加引号.JSX的语法看起来是一种模板,然而它在编译以后,会转成JS语法,只是书写过程中的语法糖. JSX的 ...
- React 系列文章(1): npm 手动搭建React 运行实例 (新手必看)
摘 要 刚接触React 开发, 在摸索中构建react 运行环境,总会遇到各种坑:本文,将用最短时间解决webpack+react 环境搭建问题. 1.如果你还没有React基础 看这里. 2.如果 ...
- TiDB 源码阅读系列文章(一)序
原创: 申砾 PingCAP 2018-02-28 在 TiDB DevCon2018 上,我们对外宣布了 TiDB 源码阅读分享活动,承诺对外发布一系列文章以及视频帮助大家理解 TiDB 源码.大 ...
随机推荐
- 默认以管理员身份运行VS2013/15/17
方法如下: 1.右击VS的快捷方式,选择[属性],打开属性对话框,再点击[高级]按钮,如下图所示: 2.再勾选[用管理员身份运行],点击[确定]即可: 然后就可以双击VS快捷方式,直接以管理员身份运行 ...
- nginx异常处理
1.nginx不转发消息头header问题 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_se ...
- C#哈希表(HashTable)和Dictionary比较
添加数据时Hashtable快.频繁调用数据时Dictionary快. Dictionary<K,V>是泛型的,当K或V是值类型时,其速度远远超过Hashtable. using Syst ...
- 关于XMLHttpRequest状态的讨论及处理方法
今天主要是讨论下XMLHttpRequest的响应状态问题.我们知道,XMLHttpRequest的响应阶段有5个,分别是: 请求未初始化 服务器连接已建立 请求已接收 请求处理中 请求已完成,且响应 ...
- 在chrome开发者工具中观察函数调用栈、作用域链与闭包
在chrome开发者工具中观察函数调用栈.作用域链与闭包 在chrome的开发者工具中,通过断点调试,我们能够非常方便的一步一步的观察JavaScript的执行过程,直观感知函数调用栈,作用域链,变量 ...
- SQL代码整理
--SQL代码整理: create database mingzi--创建数据库go--连接符(可省略)create table biao--创建表( lieming1 int not null,-- ...
- Windows下虚拟机安装Mac OS X —– VM12安装Mac OS X 10.11
____________________________________________________________________________________________________ ...
- 【linux】crontab失效
在linux上,crontab任务全部使用完整路径,但是任务无效. 检测crontab 服务是否启动, /etc/init.d/cron status /etc/init.d/cron restart
- hdu4812 逆元+树分治
逆元链接:https://www.cnblogs.com/zzqc/p/7192436.html 经典的树分治题 #pragma comment("linker,"/STACK:1 ...
- NBUT1457
不知道哪里的oj..做了交不上去.. 也是莫队的模板题 #include<iostream> #include<cstring> #include<cstdio> ...