返回

揭秘 React:Luster:JSX解析器背后的秘密

前端

从零实现一个 React:Luster(一):JSX 解析器

最近心情比较低落,摸鱼也摸到恐慌了,天天肝代码,肝到十一点还在肝,还担心时间不够用,把一个项目突然做不完。就必须赶在寒假前肝完这个项目。

这一个项目叫做 React:Luster,是一个与 React 非常相似的框架,但不同的是,他不是用 TypeScript 写的,而是 JavaScript,并且目前没有提供编译器,所以性能肯定比不过 React。

这个项目可能之后还会继续写,增加一些路由或者模板引擎的指令什么的,但是再过没多久寒假就有大块时间了就可能不摸这个鱼去开其它坑了,随缘吧。所以先写 JSX 的解析器吧,这个部分也比较独立。

其实JSX这个东西真的写起来很累,其实这就是个 DSL,用编译器去编译还是比较简单的,用解析器去解析还不如直接把 JSX 直接当字符串塞进虚拟 DOM 里,运行时再去解析JSX模板。但是自己写过一遍肯定是对JSX这种语法有更深的理解,这个就当是对编译原理的一个复习吧。

我们先来定义 JSX 的语法,其实 JSX 的语法相当简单,只有很少的一些语法糖。

JSX 的语法规则如下:

  • JSX 元素必须以 < 开头,以 > 结尾。
  • JSX 元素的名称必须是一个有效的 JavaScript 标识符。
  • JSX 元素可以有属性,属性必须写在元素的开头,属性的名称必须是一个有效的 JavaScript 标识符,属性的值必须是一个字符串。
  • JSX 元素可以有子元素,子元素必须写在元素的开头和结尾之间。
  • JSX 元素也可以有文本内容,文本内容必须写在元素的开头和结尾之间。

这个语法还是很简单的,我们现在可以来写一个解析器了。

解析器代码如下:

function parseJSX(input) {
  let tokens = [];
  let currentToken = null;
  let index = 0;

  while (index < input.length) {
    let char = input[index];

    if (char === '<') {
      if (currentToken && currentToken.type === 'text') {
        tokens.push(currentToken);
        currentToken = null;
      }

      currentToken = {
        type: 'tagStart',
        value: '<'
      };
    } else if (char === '>') {
      if (currentToken && currentToken.type === 'tagStart') {
        currentToken.value += '>';
        tokens.push(currentToken);
        currentToken = null;
      } else {
        throw new Error('Unexpected character: >');
      }
    } else if (char === '/') {
      if (currentToken && currentToken.type === 'tagStart') {
        currentToken.value += '/';
      } else {
        throw new Error('Unexpected character: /');
      }
    } else if (char === '=') {
      if (currentToken && currentToken.type === 'attributeName') {
        currentToken.value += '=';
      } else {
        throw new Error('Unexpected character: =');
      }
    } else if (char === '"') {
      if (currentToken && currentToken.type === 'attributeValue') {
        currentToken.value += '"';
      } else {
        currentToken = {
          type: 'attributeValue',
          value: '"'
        };
      }
    } else if (char === ' ') {
      if (currentToken && (currentToken.type === 'tagName' || currentToken.type === 'attributeName' || currentToken.type === 'attributeValue')) {
        tokens.push(currentToken);
        currentToken = null;
      }
    } else {
      if (currentToken) {
        currentToken.value += char;
      } else {
        currentToken = {
          type: 'text',
          value: char
        };
      }
    }

    index++;
  }

  if (currentToken) {
    tokens.push(currentToken);
  }

  return tokens;
}

function lexJSX(input) {
  let tokens = parseJSX(input);

  let lexedTokens = [];

  for (let i = 0; i < tokens.length; i++) {
    let token = tokens[i];

    if (token.type === 'text') {
      lexedTokens.push({
        type: 'text',
        value: token.value.trim()
      });
    } else if (token.type === 'tagStart') {
      let tagName = token.value.substring(1).trim();

      lexedTokens.push({
        type: 'tagStart',
        value: tagName
      });
    } else if (token.type === 'tagEnd') {
      let tagName = token.value.substring(2).trim();

      lexedTokens.push({
        type: 'tagEnd',
        value: tagName
      });
    } else if (token.type === 'attributeName') {
      let attributeName = token.value.substring(0, token.value.length - 1).trim();

      lexedTokens.push({
        type: 'attributeName',
        value: attributeName
      });
    } else if (token.type === 'attributeValue') {
      let attributeValue = token.value.substring(1, token.value.length - 1).trim();

      lexedTokens.push({
        type: 'attributeValue',
        value: attributeValue
      });
    }
  }

  return lexedTokens;
}

这个解析器将 JSX 代码转换为一个包含令牌的数组,每个令牌都表示 JSX 代码中的一个元素或属性。

我们现在可以来使用这个解析器来解析一个 JSX 代码示例:

<div>
  <h1>Hello, world!</h1>
</div>

这个 JSX 代码示例将被解析为以下令牌数组:

[
  { type: 'tagStart', value: '<div>' },
  { type: 'text', value: '\n  ' },
  { type: 'tagStart', value: '<h1>' },
  { type: 'text', value: 'Hello, world!' },
  { type: 'tagEnd', value: '</h1>' },
  { type: 'text', value: '\n' },
  { type: 'tagEnd', value: '</div>' }
]

这个令牌数组可以被用于创建一个虚拟 DOM 树,虚拟 DOM 树可以被用于渲染到真实 DOM 中。

这就是 JSX 解析器的工作原理。希望这个文章对您有所帮助。