上次我们总结了 React 代码构建后的 webpack 模块组织关系,今天来介绍一下 Babel 编译 JSX 生成目标代码的一些规则,并且写一个简单的解析器,模拟整个生成的过程。
我们还是拿最简单的代码举例:
import {greet} from './utils';
const App = {greet('scott')}
;
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 = (
{greet('scott')} hah
This is a JSX demo
);
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 = ;
// 编译结果
var App = React.createElement('input', { type: 'button', value: 'click me' });
现在,我们大概了解了由 JSX 到目标代码这中间的一些变化,那么我们是不是能够模拟这个过程呢?
要模拟整个过程,需要两个步骤:首先将 JSX 解析成树状数据结构,然后根据这个树状结构生成目标代码。
下面我们就来实际演示一下,假如有如下代码片段:
const style = {
color: 'red'
};
function greet(name) {
return `hello ${name}`;
}
const App = (
saying {greet('scott')} hah
this is jsx-like code
parsing it now
);
我们在 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);
// 闭合标记 例如: