上次咱們總結了 React 代碼構建後的 webpack 模塊組織關係,今天來介紹一下 Babel 編譯 JSX 生成目標代碼的一些規則,而且寫一個簡單的解析器,模擬整個生成的過程。webpack
咱們仍是拿最簡單的代碼舉例:web
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'));
編譯以後,會生成以下代碼:ui
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 方法的簽名大概是下面這個樣子:this
React.createElement(tag, attrs, ...children);
第一參數是標籤名,第二個參數是屬性對象,後面的參數是 0 到多個子結點。若是是自閉和標籤,只生成前兩個參數便可,以下:code
// JSX const App = <input type="button" value="click me" />; // 編譯結果 var App = React.createElement('input', { type: 'button', value: 'click me' });
如今,咱們大概瞭解了由 JSX 到目標代碼這中間的一些變化,那麼咱們是否是可以模擬這個過程呢?orm
要模擬整個過程,須要兩個步驟:首先將 JSX 解析成樹狀數據結構,而後根據這個樹狀結構生成目標代碼。對象
下面咱們就來實際演示一下,假若有以下代碼片斷:ip
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() 方法,將上面的代碼運行起來並輸出預期的效果呢,咱們會在下一篇文章中介紹。