• 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(/^
    |
    $/, '').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() 方法,将上面的代码运行起来并输出预期的效果呢,我们会在下一篇文章中介绍。

  • 相关阅读:
    tomcat 乱码问题
    mongo获取lbs数据
    sping mvc+uploadify 上传文件大小控制3部曲
    jstack 查看线程状态
    nginx 限流配置
    查找占用CPU高线程
    redis sentinel无法启动问题
    GC垃圾回收机制
    ASP.NET 生命周期
    Javascript验证手机号码正则表达式
  • 原文地址:https://www.cnblogs.com/liuhe688/p/9618706.html
Copyright © 2020-2023  润新知