《Vue.js设计与实现》之编译器核心技术概览

前言

今天来了解一下编译器

编译器

编译器其实就是一段程序,将自己写的源代码转化成计目标代码可以被计算机执行。Vue.js模板编译器会首先对模板进行进行词法分析和语法分析,得到模板AST。接着将模板AST转换成JavaScript AST。最后,根据JavaSript AST 生成JavaScript代码,即渲染函数代码。

js 复制代码
// 编译前
<div>
    <h1 :id="dynamicId">Vue Template</h1>
</div>

// 编译后
function render() {
    return h('div', [ h('h1', { id: dynamicId}, 'Vue Template')])
}

词法分析,解析token

通过有限状态机,我们可以将模板解析成一个个Token,进而可以用它们构建一颗AST。

标签转成token列表

js 复制代码
tokenzie('<div><p>123</p><p>456</p></div>')

// 解析后的token列表
[
  {
    "type": "tag",
    "name": "div"
  },
  {
    "type": "tag",
    "name": "p"
  },
  {
    "type": "text",
    "content": "123"
  },
  {
    "type": "tagEnd",
    "name": "p"
  },
  {
    "type": "tag",
    "name": "p"
  },
  {
    "type": "text",
    "content": "456"
  },
  {
    "type": "tagEnd",
    "name": "p"
  },
  {
    "type": "tagEnd",
    "name": "div"
  }
]

方法实现

js 复制代码
const State = {
  initial: 1, // 初始状态
  tagOpen: 2, // 标签开始
  tagName: 3, // 标签名开始
  text: 4, // 文本状态
  tagEnd: 5, // 结束标签
  tagEndName: 6 // 结束标签名
}

function isAlpha(char) {
  return /\w/.test(char)
}

// 解析token
function tokenzie(str) {
  let currentState = State.initial; // 初始状态
  const chars = [] // 用于缓存字符
  const tokens = []

  // 添加token
  const addToken = (type) => {
    tokens.push({
      type,
      [type === 'text' ? 'content' : 'name']: chars.join('')
    })
    chars.length = 0;
  }
  
  while (str) {
    const char = str[0];
    str = str.slice(1) // 消费字符串
    if (isAlpha(char)) {
      chars.push(char) // 缓存文本
    }
    switch (currentState) {
      case State.initial:
        if (char === '<') {
          // 切换到标签开始
          currentState = State.tagOpen
        } else if (isAlpha(char)) {
          // 切换到文本状态
          currentState = State.text
        }
        break
      case State.tagOpen:
        if (isAlpha(char)) {
          currentState = State.tagName
        } else if (char === '/') {
          currentState = State.tagEnd
        }
        break
      case State.tagName:
        if (char === '>') {
          currentState = State.initial
          addToken('tag')
        }
        break
      case State.text:
        if (char === '<') {
          currentState = State.tagOpen
          addToken('text')
        }
        break
      case State.tagEnd:
        if (isAlpha(char)) {
          currentState = State.tagEndName
        }
        break
      case State.tagEndName:
        if (char === '>') {
          currentState = State.initial
          addToken('tagEnd')
        }
        break
    }
  }
  return tokens
}

语法分析,构建AST树

扫描token列表,利用栈维护父子关系,构建ast

方法执行

js 复制代码
parse('<div><p><span>123</span></p>')
// 转换后的结构
{
  "type": "Root",
  "children": [
    {
      "type": "Element",
      "tag": "div",
      "children": [
        {
          "type": "Element",
          "tag": "p",
          "children": [
            {
              "type": "Text",
              "content": "123"
            }
          ]
        },
        {
          "type": "Element",
          "tag": "p",
          "children": [
            {
              "type": "Text",
              "content": "456"
            }
          ]
        }
      ]
    }
  ]
}

方法实现

js 复制代码
function parse(str) {
  const tokens = tokenzie(str);
  // 创建根节点
  const root = {
    type: 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':
        parent.children.push({
          type: 'Text',
          content: t.content
        })
        break
      case 'tagEnd':
        elementStack.pop()
        break
    }
    tokens.shift()
  }
  return root
}

将AST转成JavaScript AST

转换后的json

js 复制代码
const res = parse('<div><p>123</p><p>456</p></div>')
transform(res)

// res.jsNode的值,描述function render () { return h('div', [h('p', '123'), h('p', '456')]) }
{
  "type": "FunctionDecl",
  "id": {
    "type": "Identifier",
    "name": "render"
  },
  "params": [],
  "body": [
    {
      "type": "ReturnStatement",
      "return": {
        "type": "CallExpression",
        "callee": {
          "type": "Identifier",
          "name": "h"
        },
        "arguments": [
          {
            "type": "StringLiteral",
            "value": "div"
          },
          {
            "type": "ArrayExpression",
            "elements": [
              {
                "type": "CallExpression",
                "callee": {
                  "type": "Identifier",
                  "name": "h"
                },
                "arguments": [
                  {
                    "type": "StringLiteral",
                    "value": "p"
                  },
                  {
                    "type": "StringLiteral",
                    "value": "123"
                  }
                ]
              },
              {
                "type": "CallExpression",
                "callee": {
                  "type": "Identifier",
                  "name": "h"
                },
                "arguments": [
                  {
                    "type": "StringLiteral",
                    "value": "p"
                  },
                  {
                    "type": "StringLiteral",
                    "value": "456"
                  }
                ]
              }
            ]
          }
        ]
      }
    }
  ]
}

transform方法

js 复制代码
// 转换
function transform(ast) {
  const context = {
    currentNode: null,
    parent: null,
    childrenIndex: -1,
    nodeTransforms: [transformText, transformElement, transformRoot] // 转换ast树过程中执行这几个方法
  }
  traverseNode(ast, context)
}

// 递归遍历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)
    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.childrenIndex = i
      traverseNode(children[i], context)
    }
  }
  
  // 从里向外执行回调,处理文本、元素节点
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

transformText转换文本

js 复制代码
function transformText(node) {
  if (node.type !== 'Text') {
    return
  }
  node.jsNode = {
      type: 'StringLiteral',
      value: node.content
  }
}

transformElement转换元素

js 复制代码
function transformElement(node) {
  return () => {
    if (node.type !== Type.Element) {
      return
    }
    
    // div => h('div')
    const callExp = {
        type: 'CallExpression',
        callee: {
            type: 'Identifier',
            name: 'h'
        },
        arguments: [{
          type: 'StringLiteral',
          value: node.tag
        }]
    }
    
    // 有子元素,将参数加入到arguments里,h('div', ...)
    node.children.length === 1
      ? callExp.arguments.push(node.children[0].jsNode)
      : callExp.arguments.push({
          type: 'ArrayExpression',
          elements: node.children.map(v => v.jsNode)
      })

    node.jsNode = callExp;
  }
}

transformRoot转换根节点

js 复制代码
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
        }
      ]
    }
  }
}

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

方法执行

js 复制代码
function compile(template) {
  const ast = parse(template) // ast模板
  transform(ast) // ast转换成JavaScript AST
  return generate(ast.jsNode) // 代码生成
}
compile('<div><p>123</p><p>456</p></div>')
function render () { 
  return h('div', [h('p', '123'), h('p', '456')]) 
}

方法实现

generate方法

js 复制代码
function generate(node) {
  const context = {
    code: '',
    currentIndent: 0,
    newline() { // 换行
      context.code += '\n' + '  '.repeat(context.currentIndent)
    },
    indent() { // 缩进2行
      context.currentIndent++
      context.newline()
    },
    deIndent() { // 取消缩进
      context.currentIndent--
      context.newline()
    },
    push(code) { // 拼接代码
      context.code += code
    }
  }
  genNode(node, context)
  return context.code
}

genNode方法

js 复制代码
function genNode(node, context) {
  const { push, indent, deIndent } = context
  switch (node.type) {
    case 'FunctionDecl':
      push(`function ${node.id.name} (`)
      genNodeList(node.params, context)
      push(`) {`)
      indent()
      node.body.forEach(n => genNode(n, context))
      deIndent()
      push('}')
      break;
    case 'ReturnStatement':
      push('return ')
      genNode(node.return, context)
      break;
    case 'CallExpression':
      const { callee, arguments: args } = node
      push(`${callee.name}(`)
      genNodeList(args, context)
      push(')')
      break;
    case 'StringLiteral':
      push(`'${node.value}'`)
      break;
    case 'ArrayExpression':
      push('[')
      genNodeList(node.elements, context)
      push(']')
      break;
  }
}

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

最后

这段时间看的《Vue.js设计与实现》一书的第17章,觉得对于从文本到编译成render函数写的不错,记录一下学习笔记,感兴趣的朋友可以去看看这本书,收获不少。

相关推荐
森叶16 分钟前
Electron 主进程中使用Worker来创建不同间隔的定时器实现过程
前端·javascript·electron
霸王蟹24 分钟前
React 19 中的useRef得到了进一步加强。
前端·javascript·笔记·学习·react.js·ts
霸王蟹25 分钟前
React 19版本refs也支持清理函数了。
前端·javascript·笔记·react.js·前端框架·ts
繁依Fanyi30 分钟前
ColorAid —— 一个面向设计师的色盲模拟工具开发记
开发语言·前端·vue.js·编辑器·codebuddy首席试玩官
codelxy33 分钟前
vue引用cesium,解决“Not allowed to load local resource”报错
javascript·vue.js
程序猿阿伟1 小时前
《社交应用动态表情:RN与Flutter实战解码》
javascript·flutter·react native
明似水2 小时前
Flutter 开发入门:从一个简单的计数器应用开始
前端·javascript·flutter
沐土Arvin2 小时前
前端图片上传组件实战:从动态销毁Input到全屏预览的全功能实现
开发语言·前端·javascript
Zww08912 小时前
el-dialog鼠标在遮罩层松开会意外关闭,教程图文并茂
javascript·vue.js·计算机外设
爱编程的鱼2 小时前
C#接口(Interface)全方位讲解:定义、特性、应用与实践
java·前端·c#