React: Babel編譯JSX生成代碼

上次咱們總結了 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() 方法,將上面的代碼運行起來並輸出預期的效果呢,咱們會在下一篇文章中介紹。

相關文章
相關標籤/搜索