12-编译器核心技术概览

144 阅读4分钟

1、模板DSL的编译器

编译器本质也只是一段程序,把源代码编译为目标代码。

编译的过程包括:词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成。如下图所示

以上的编译过程是所有源代码的基本过程,但每个编程语言具体又有所区别,拥有JS运行时的平台代码,编译过程是这样的

从图中可以看出,Vue.js模板编译器的目标代码其实就是渲染函数

详细过程如下:Vue.js模板编译器会首先对模板进行词法分析和语法分析,得到模板AST(abstract syntax tree抽象语法树)。

再将模板AST转换成JS AST。最后,根据JS AST 生成 JS代码,即渲染函数代码,流程如下图所示

AST实质上是一个具有层级的结构对象,其中主要包括

1.type属性区分节点

2.子节点存储在其children数组中

3.属性节点和指令节点存储在props数组中

4.不同类型的节点会使用不同对象属性进行描述

封装parse函数封装词法分析和语法分析,得到模板AST,如图所示

得到模板AST之后通过transform函数把模板AST转为 JS AST,如下图

再通过generate函数产生模板函数

串起来就是Vue.js模板编译为渲染函数的完整流程

2、parse的实现原理与状态机

parse(解析器)原理:解析器的入参是字符串模板,逐个读取字符串模板中的字符,并根据规则将整个字符串切割为一个个Token(词法记号)

根据规则,规则指的是,有限状态自动机,

有限状态:就是有限个状态

自动机:指随着字符的输入,解析器会自动的在不同状态间迁移

解析器状态机大致流程如上图,简单描述为:

1.初始状态,遇到< 符号进入标签开始状态,接下来遇到的字母,在没遇到 > 符号前都算做开始标签。

2.遇到了 > 符号,开始标签结束,返回到解析器的初始状态。

3.初始状态遇到了字母,接下来的内容是文本状态,再次遇到 < 符号,又进入到标签开始状态。

4.遇到了 / 符号,进入结束标签状态,再遇到字母,进入到结束标签名称状态。

5.最后,遇到了 > 符号,返回解析器的初始状态

这个流程就是解析器状态机的大致流程,其代码如下

//定义状态机的状态
  const State = {
    initial: 1, // 初始状态
    tagOpen: 2, // 标签开始状态
    tagName: 3, // 标签名称状态
    text: 4, // 文本状态
    tagEnd: 5, // 结束标签状态
    tagEndName: 6 // 结束标签名称状态
  }
  // 判断是否是字母
  function isAlpha(char) {
    return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
  }
  // 接收模板字符串作为参数,切割为Token返回
  function tokenize(str) {
    // 状态机的当前状态:初始状态
    let currentState = State.initial
    // 缓存字符
    const chars = []
    // 存储Token, 并作为函数返回值
    const tokens = []
    // while循环开启自动机,字符串没用完,就一直运行
    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
  }
  
  function parse(str) {
    const tokens = tokenize(str)

    const root = {
      type: 'Root',
      children: []
    }
    const elementStack = [root]

    while (tokens.length) {
      const parent = elementStack[elementStack.length - 1]
      const t = tokens[0]
      switch (t.type) {
        case 'tag':
          const elementNode = {
            type: 'Element',
            tag: t.name,
            children: []
          }
          parent.children.push(elementNode)
          elementStack.push(elementNode)
          break
        case 'text':
          const textNode = {
            type: 'Text',
            content: t.content
          }
          parent.children.push(textNode)
          break
        case 'tagEnd':
          elementStack.pop()
          break
      }
      tokens.shift()
    }

    return root
  }

  const ast = parse(template)
  console.log(ast)

3、构造AST

AST的构造方式,对于通用用途语言(GPL)来说,为其构造AST,通常使用递归下降算法

注意:Vue.js模板的DSL不具有运算符,不需要考虑运算符优先级

1.AST的结构

AST在结构上与模板是“同构”,具有树型结构,如下图所示

构建AST,其实就是对Token表进行扫描的过程,顺序扫描整个Token列表,需要维护一个栈elementStack,用于维护元素间的父子关系,遇到一个开始标签,构造Element类型的AST节点,并将其压入栈中,遇到结束节点,就弹出当前栈顶节点,操作如下10张图。

4、AST的转换与插件化架构

1、节点的访问

对AST进行转换,需要可以访问AST的每一个节点,才可以对特定节点进行修改,AST是树型结构,需要一个深度优先的遍历算法。在此之前需要编写一个dump算法打印节点信息

	function dump(node, indent = 0) {
    const type = node.type
    const desc = node.type === 'Root'
      ? ''
      : node.type === 'Element'
        ? node.tag
        : node.content

    console.log(`${'-'.repeat(indent)}${type}: ${desc}`)

    if (node.children) {
      node.children.forEach(n => dump(n, indent + 2))
    }
	}

2、转换上下文与节点操作

上下文:实质上就是程序在某个范围内的“全局变量”

使用上下文context的好处:所有AST转换函数都可以通过context共享数据,上下文对象会维护程序的当前状态。

转换节点的最终版本

  function traverseNode(ast, context) {
    context.currentNode = ast

    const transforms = context.nodeTransforms
    for (let i = 0; i < transforms.length; i++) {
      transforms[i](context.currentNode, context)
      if (!context.currentNode) return
    }

    const children = context.currentNode.children
    if (children) {
      for (let i = 0; i < children.length; i++) {
        context.parent = context.currentNode
        context.childIndex = i
        traverseNode(children[i], context)
      }
    }
  }

3、进入与退出

在转换AST节点的过程中,通常需要根据子节点的情况来决定如何对当前节点进行转换,要求父节点的转换操作必须等待所有子节点转换完成之后再执行,其顺序执行工作流如图所示

当前的工作流仍然有着弊端,“不能回头”处理父节点,更理想的工作流应该是这样的

  function traverseNode(ast, context) {
    context.currentNode = ast

    const exitFns = []
    const transforms = context.nodeTransforms
    for (let i = 0; i < transforms.length; i++) {
      const onExit = transforms[i](context.currentNode, context)
      if (onExit) {
        exitFns.push(onExit)
      }
      if (!context.currentNode) return
    }

    const children = context.currentNode.children
    if (children) {
      for (let i = 0; i < children.length; i++) {
        context.parent = context.currentNode
        context.childIndex = i
        traverseNode(children[i], context)
      }
    }

    let i = exitFns.length
    while (i--) {
      exitFns[i]()
    }
  }

5、将模板AST转为JavaScript AST

使用一个对象来描述一个JavaScript AST节点,每个节点都具有type字段,用id字段来存储函数的名称

封装一些辅助函数

  // =============================== AST 工具函数 ===============================

  function createStringLiteral(value) {
    return {
      type: 'StringLiteral',
      value
    }
  }

  function createIdentifier(name) {
    return {
      type: 'Identifier',
      name
    }
  }

  function createArrayExpression(elements) {
    return {
      type: 'ArrayExpression',
      elements
    }
  }

  function createCallExpression(callee, arguments) {
    return {
      type: 'CallExpression',
      callee: createIdentifier(callee),
      arguments
    }
  }

  // =============================== AST 工具函数 ===============================

  function transformText(node) {
    if (node.type !== 'Text') {
      return
    }

    node.jsNode = createStringLiteral(node.content)
  }


  function transformElement(node) {

    return () => {
      if (node.type !== 'Element') {
        return
      }

      const callExp = createCallExpression('h', [
        createStringLiteral(node.tag)
      ])
      node.children.length === 1
        ? callExp.arguments.push(node.children[0].jsNode)
        : callExp.arguments.push(
          createArrayExpression(node.children.map(c => c.jsNode))
        )

      node.jsNode = callExp
    }
  }

转换Root如下所示

  function transformRoot(node) {
    return () => {
      if (node.type !== 'Root') {
        return
      }

      const vnodeJSAST = node.children[0].jsNode

      node.jsNode = {
        type: 'FunctionDecl',
        id: { type: 'Identifier', name: 'render' },
        params: [],
        body: [
          {
            type: 'ReturnStatement',
            return: vnodeJSAST
          }
        ]
      }
    }
  }

6、代码生成

接下来进行generate操作,与AST一样需要context,需要处理5种节点:

1.函数类型 2.返回类型 3.调用类型

4.字符串类型 5.数组类型

目前尚不完善的编译器完整代码如下

const State = {
    initial: 1,
    tagOpen: 2,
    tagName: 3,
    text: 4,
    tagEnd: 5,
    tagEndName: 6
  }
  
  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
  }
  
  function parse(str) {
    const tokens = tokenize(str)

    const root = {
      type: 'Root',
      children: []
    }
    const elementStack = [root]

    while (tokens.length) {
      const parent = elementStack[elementStack.length - 1]
      const t = tokens[0]
      switch (t.type) {
        case 'tag':
          const elementNode = {
            type: 'Element',
            tag: t.name,
            children: []
          }
          parent.children.push(elementNode)
          elementStack.push(elementNode)
          break
        case 'text':
          const textNode = {
            type: 'Text',
            content: t.content
          }
          parent.children.push(textNode)
          break
        case 'tagEnd':
          elementStack.pop()
          break
      }
      tokens.shift()
    }

    return root
  }

  function traverseNode(ast, context) {
    context.currentNode = ast

    const exitFns = []
    const transforms = context.nodeTransforms
    for (let i = 0; i < transforms.length; i++) {
      const onExit = transforms[i](context.currentNode, context)
      if (onExit) {
        exitFns.push(onExit)
      }
      if (!context.currentNode) return
    }

    const children = context.currentNode.children
    if (children) {
      for (let i = 0; i < children.length; i++) {
        context.parent = context.currentNode
        context.childIndex = i
        traverseNode(children[i], context)
      }
    }

    let i = exitFns.length
    while (i--) {
      exitFns[i]()
    }
  }


  function transform(ast) {
    const context = {
      currentNode: null,
      parent: null,
      replaceNode(node) {
        context.currentNode = node
        context.parent.children[context.childIndex] = node
      },
      removeNode() {
        if (context.parent) {
          context.parent.children.splice(context.childIndex, 1)
          context.currentNode = null
        }
      },
      nodeTransforms: [
        transformRoot,
        transformElement,
        transformText
      ]
    }
    // 调用 traverseNode 完成转换
    traverseNode(ast, context)
  }





  // =============================== AST 工具函数 ===============================

  function createStringLiteral(value) {
    return {
      type: 'StringLiteral',
      value
    }
  }

  function createIdentifier(name) {
    return {
      type: 'Identifier',
      name
    }
  }

  function createArrayExpression(elements) {
    return {
      type: 'ArrayExpression',
      elements
    }
  }

  function createCallExpression(callee, arguments) {
    return {
      type: 'CallExpression',
      callee: createIdentifier(callee),
      arguments
    }
  }

  // =============================== AST 工具函数 ===============================

  function transformText(node) {
    if (node.type !== 'Text') {
      return
    }

    node.jsNode = createStringLiteral(node.content)
  }


  function transformElement(node) {

    return () => {
      if (node.type !== 'Element') {
        return
      }

      const callExp = createCallExpression('h', [
        createStringLiteral(node.tag)
      ])
      node.children.length === 1
        ? callExp.arguments.push(node.children[0].jsNode)
        : callExp.arguments.push(
          createArrayExpression(node.children.map(c => c.jsNode))
        )

      node.jsNode = callExp
    }
  }

  function transformRoot(node) {
    return () => {
      if (node.type !== 'Root') {
        return
      }

      const vnodeJSAST = node.children[0].jsNode

      node.jsNode = {
        type: 'FunctionDecl',
        id: { type: 'Identifier', name: 'render' },
        params: [],
        body: [
          {
            type: 'ReturnStatement',
            return: vnodeJSAST
          }
        ]
      }
    }
  }

  const ast = parse(`<div><p>Vue</p><p>Template</p></div>`)
  transform(ast)

  console.log(ast)

  console.log(generate(ast.jsNode))

  // ============================ code generate ============================

  function generate(node) {
    const context = {
      code: '',
      push(code) {
        context.code += code
      },
      currentIndent: 0,
      newline() {
        context.code += '\n' + `  `.repeat(context.currentIndent)
      },
      indent() {
        context.currentIndent++
        context.newline()
      },
      deIndent() {
        context.currentIndent--
        context.newline()
      }
    }

    genNode(node, context)

    return context.code
  }

  function genNode(node, context) {
    switch (node.type) {
      case 'FunctionDecl':
        genFunctionDecl(node, context)
        break
      case 'ReturnStatement':
        genReturnStatement(node, context)
        break
      case 'CallExpression':
        genCallExpression(node, context)
        break
      case 'StringLiteral':
        genStringLiteral(node, context)
        break
      case 'ArrayExpression':
        genArrayExpression(node, context)
        break
    }
  }

  function genFunctionDecl(node, context) {
    const { push, indent, deIndent } = context

    push(`function ${node.id.name} `)
    push(`(`)
    genNodeList(node.params, context)
    push(`) `)
    push(`{`)
    indent()

    node.body.forEach(n => genNode(n, context))

    deIndent()
    push(`}`)
  }

  function genNodeList(nodes, context) {
    const { push } = context
    for (let i = 0; i < nodes.length; i++) {
      const node = nodes[i]
      genNode(node, context)
      if (i < nodes.length - 1) {
        push(', ')
      }
    }
  }

  function genReturnStatement(node, context) {
    const { push } = context

    push(`return `)
    genNode(node.return, context)
  }

  function genCallExpression(node, context) {
    const { push } = context
    const { callee, arguments: args } = node
    push(`${callee.name}(`)
    genNodeList(args, context)
    push(`)`)
  }

  function genStringLiteral(node, context) {
    const { push } = context

    push(`'${node.value}'`)
  }

  function genArrayExpression(node, context) {
    const { push } = context
    push('[')
    genNodeList(node.elements, context)
    push(']')
  }

总结

1、学习了编译器的工作流程:

分析模板,解析为模板AST

将模板AST解析为描述渲染函数的JavaScript AST

根据JavaScript AST生成渲染函数代码

2、解析器的实现原理,如何实现有限状态自动机,构建树型AST

3、AST是树型结构,需要编写深度优先遍历AST树,设置context转换上下文,上下文对象会维护程序的当前状态。

4、AST转为JavaScript AST,生成代码,为不同类型的节点编写对应的代码生成函数