15.vue3中编译器原理(上)之parse的实现原理

256 阅读4分钟

1.编译器的概念

编译器其实就是一段程序,它用来将一种语言A翻译成另外一种语言B,我们称之为A为源代码,B为目标代码, 编译就是将源代码翻译为目标代码的过程。 完整的编译通常分为以下几个步骤,如图:

我们再vue模块中,通常就是组件模板的编译,而目标代码就是可以再浏览器运行的javaScript代码。

源代码:

<div>
  <h1 :id="dynamicId"> Vue Template</h1>
</div>  

目标代码:

function render(){
  return h('div',{
    h('h1',{id:dynamicId},'Vue Template')
  })
}

我们可以看到Vue.js中编译器的目标代码其实就是生成渲染函数。那我们应该怎么样去生成渲染函数,这个就是我们需要思考的问题。

我们简单把生成渲染函数的步骤分为三步:

  1. 将模板字符串解析为模板AST的解析器(parse)
  2. 将模版的AST转换为JavaScript AST的转换器(transformer)
  3. 用JavaScript AST生成渲染函数代码(generator)

整体流程图如下:

2.parser的实现原理

解析器的入参就是字符串模板,解析会逐个读取字符串模板中的字符,并根据一定的规则将整个字符串切割为一个Token。

推荐一个AST很好的转换的地址:astexplorer.net/

我们举个简单的例子

<p>Vue</p>

我们理解上述的字符串模板为三个Token.

  • 开始标签

  • 文本节点:Vue
  • 结束标签:

那解析器如何对模板进行分割呢?这个时候就得提到有限状态自动机

  1. 状态总数是有限的 (每一个标签代表一个状态)
    1. 初始状态
    2. 标签开始状态 (<)
    3. 标签名称状态 (p)
    4. 文本状态 (Vue)
    5. 结束标签状态 (</)
    6. 结束标签名称状态 (p)
    7. .....
  1. 任一时刻,只处在一种状态之中

比如 <p>Vue</p> 从左往右,要么是 开始标签状态,要么是 文本状态,要么是 结束状态,不可能没有状态。

  1. 某种条件下,会从一种状态转变到另一种状态

一开始为开始状态,然后切换到文本状态或者其他状态。即 我们一定是在某一前提条件下,由某一状态切换到另一个状态

那我们理解了上面的东西,按照有限状态自动机的状态迁移过程,我们可以很容易地编写对应的代码实现。因此,有限状态自动机可以帮助我们完成对模板的标记化(tokenized),最终我们将得到一系列 Token。

状态机的状态

 // 定义状态机的状态
 const State = {
  initial: 1, // 初始状态
  tagOpen: 2, // 标签开始状态
  tagName: 3, // 标签名称状态
  text: 4, // 文本状态
  tagEnd: 5, // 结束标签状态
  tagName:6 //结束状态名称状态
 }

匹配状态tokenize

function isAlpha(char) {
  return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}

function tokenize(str) {
  let currentState = State.initial
  const chars = []
  const tokens = []
  while(str) {
    const char = str[0]
    switch (currentState) {
      case State.initial:
        if (char === '<') {
          currentState = State.tagOpen
          str = str.slice(1)
        } else if (isAlpha(char)) {
          currentState = State.text
          chars.push(char)
          str = str.slice(1)
        }
        break
      case State.tagOpen:
        if (isAlpha(char)) {
          currentState = State.tagName
          chars.push(char)
          str = str.slice(1)
        } else if (char === '/') {
          currentState = State.tagEnd
          str = str.slice(1)
        }
        break
      case State.tagName:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '>') {
          currentState = State.initial
          tokens.push({
            type: 'tag',
            name: chars.join('')
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
      case State.text:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '<') {
          currentState = State.tagOpen
          tokens.push({
            type: 'text',
            content: chars.join('')
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
      case State.tagEnd:
        if (isAlpha(char)) {
          currentState = State.tagEndName
          chars.push(char)
          str = str.slice(1)
        }
        break
      case State.tagEndName:
        if (isAlpha(char)) {
          chars.push(char)
          str = str.slice(1)
        } else if (char === '>') {
          currentState = State.initial
          tokens.push({
            type: 'tagEnd',
            name: chars.join('')
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
    }
  }

  return tokens
}

通过tokenize,我们可以通过下面三个Token

 const tokens = tokenize(`<p>Vue</p>`)
  //得到结果
 tokens = [
    { type: 'tag', name: 'p' }, // 开始标签
    { type: 'text', content: 'Vue' }, // 文本节点
    { type: 'tagEnd', name: 'p' } // 结束标签
  ]

总而言之,通过有限自动机,我们能够将模板解析为一个个Token,进而可以用它们构建一棵 AST 了。但在具体构建 AST 之前,我们需要思考能否简化 tokenize 函数的代码。实际上,我们可以通过正则表达式来精简 tokenize 函数的代码。上文之所以没有从最开始就采用正则表达式来实现,是因为正则表达式的本质就是有限自动机。当你编写正则表达式的时候,其实就是在编写有限自动机