《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函数写的不错,记录一下学习笔记,感兴趣的朋友可以去看看这本书,收获不少。

相关推荐
潜意识起点3 分钟前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛8 分钟前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode
IT女孩儿18 分钟前
JavaScript--WebAPI查缺补漏(二)
开发语言·前端·javascript·html·ecmascript
m0_748256563 小时前
如何解决前端发送数据到后端为空的问题
前端
请叫我飞哥@3 小时前
HTML5适配手机
前端·html·html5
@解忧杂货铺4 小时前
前端vue如何实现数字框中通过鼠标滚轮上下滚动增减数字
前端·javascript·vue.js
F-2H6 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05676 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
gqkmiss7 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247559 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php