Published on

深入Vue模板编译:手写AST解析与Render函数生成

第二篇:Vue模板编译原理(从HTML字符串到Render函数)

一、模板编译核心流程

Vue模板编译分为三个阶段:

  1. 解析(parse):将HTML字符串解析成AST抽象语法树
  2. 优化(optimize):标记AST中的静态节点(可选)
  3. 生成(generate):将AST转换成Render函数字符串

二、HTML解析成AST

1. 核心正则表达式

// compiler/parse-html.js
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`) // 匹配开始标签
const startTagClose = /^\s*(\/?)>/ // 匹配标签结束
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`) // 匹配结束标签
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ // 匹配属性
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g // 匹配{{}}插值

2. AST节点类型

const ELEMENT_TYPE = 1 // 元素节点
const TEXT_TYPE = 3 // 文本节点

// 创建AST元素节点
function createASTElement(tag, attrs) {
  return {
    type: ELEMENT_TYPE,
    tag,
    attrs,
    children: [],
    parent: null
  }
}

3. 解析主函数

// compiler/parse-html.js
export function parseHTML(html) {
  let root // 根节点
  let currentParent // 当前父节点
  const stack = [] // 标签栈
  
  // 处理开始标签
  function start(tagName, attrs) {
    const element = createASTElement(tagName, attrs)
    if (!root) root = element
    currentParent = element
    stack.push(element)
  }
  
  // 处理结束标签
  function end(tagName) {
    const element = stack.pop()
    currentParent = stack[stack.length - 1]
    if (currentParent) {
      element.parent = currentParent
      currentParent.children.push(element)
    }
  }
  
  // 处理文本
  function chars(text) {
    text = text.trim()
    if (text) {
      currentParent.children.push({
        type: TEXT_TYPE,
        text
      })
    }
  }
  
  // 截取字符串
  function advance(n) {
    html = html.substring(n)
  }
  
  // 解析开始标签
  function parseStartTag() {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: []
      }
      advance(start[0].length)
      
      // 解析属性
      let end, attr
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
        advance(attr[0].length)
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5] || true
        })
      }
      
      if (end) {
        advance(end[0].length)
      }
      return match
    }
    return false
  }
  
  // 主循环解析
  while (html) {
    const textEnd = html.indexOf('<')
    if (textEnd === 0) {
      // 解析开始标签
      const startTagMatch = parseStartTag()
      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs)
        continue
      }
      
      // 解析结束标签
      const endTagMatch = html.match(endTag)
      if (endTagMatch) {
        advance(endTagMatch[0].length)
        end(endTagMatch[1])
        continue
      }
    }
    
    // 解析文本
    let text
    if (textEnd > 0) {
      text = html.substring(0, textEnd)
    }
    if (text) {
      advance(text.length)
      chars(text)
    }
  }
  
  return root
}

三、AST生成Render函数

1. 处理插值表达式

// compiler/generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

function genText(text) {
  if (!defaultTagRE.test(text)) {
    return `_v(${JSON.stringify(text)})`
  }
  
  let tokens = []
  let lastIndex = 0
  defaultTagRE.lastIndex = 0
  let match
  
  while (match = defaultTagRE.exec(text)) {
    const index = match.index
    if (index > lastIndex) {
      tokens.push(JSON.stringify(text.slice(lastIndex, index)))
    }
    tokens.push(`_s(${match[1].trim()})`)
    lastIndex = index + match[0].length
  }
  
  if (lastIndex < text.length) {
    tokens.push(JSON.stringify(text.slice(lastIndex)))
  }
  
  return `_v(${tokens.join('+')})`
}

2. 处理子节点

function genChildren(el) {
  const children = el.children
  if (children && children.length) {
    return children.map(child => gen(child)).join(',')
  }
  return false
}

function gen(node) {
  if (node.type === 1) {
    return generate(node)
  } else {
    return genText(node.text)
  }
}

3. 处理属性

function genProps(attrs) {
  let str = ''
  for (let i = 0; i < attrs.length; i++) {
    const attr = attrs[i]
    if (attr.name === 'style') {
      const styleObj = {}
      attr.value.split(';').forEach(item => {
        if (item) {
          const [key, value] = item.split(':')
          styleObj[key.trim()] = value.trim()
        }
      })
      attr.value = styleObj
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},`
  }
  return `{${str.slice(0, -1)}}`
}

4. 生成Render函数

export function generate(el) {
  const children = genChildren(el)
  let code = `_c('${el.tag}',${
    el.attrs.length ? genProps(el.attrs) : 'undefined'
  }${
    children ? `,${children}` : ''
  })`
  return code
}

5. 编译入口

// compiler/index.js
import { parseHTML } from './parse-html'
import { generate } from './generate'

export function compileToFunction(template) {
  // 1. 解析HTML成AST
  const ast = parseHTML(template)
  
  // 2. 生成Render函数字符串
  const code = generate(ast)
  
  // 3. 创建Render函数
  const render = new Function(`with(this){return ${code}}`)
  
  return render
}

四、Render函数执行

1. 虚拟DOM创建

// vdom/create-element.js
export function createElement(vm, tag, data = {}, ...children) {
  return vnode(tag, data, data.key, children)
}

export function createTextNode(vm, text) {
  return vnode(undefined, undefined, undefined, undefined, text)
}

function vnode(tag, data, key, children, text) {
  return {
    tag,
    data,
    key,
    children,
    text
  }
}

2. 挂载Render方法

// render.js
import { createElement, createTextNode } from './vdom/create-element'

export function renderMixin(Vue) {
  Vue.prototype._c = function(...args) {
    return createElement(this, ...args)
  }
  
  Vue.prototype._v = function(text) {
    return createTextNode(this, text)
  }
  
  Vue.prototype._s = function(val) {
    return val == null ? '' : (typeof val === 'object' ? JSON.stringify(val) : val)
  }
  
  Vue.prototype._render = function() {
    const vm = this
    const { render } = vm.$options
    const vnode = render.call(vm)
    return vnode
  }
}

五、编译流程总结

  1. 模板输入<div>{{name}}</div>
  2. AST输出:生成抽象语法树
  3. Render函数_c('div',undefined,_v(_s(name)))
  4. 虚拟DOM:执行Render生成VNode
  5. 真实DOM:将VNode渲染成真实DOM

下一篇我们将讲解Vue的Diff算法实现,继续深入Vue核心原理!