Vue的模板编译器

813 阅读2分钟

前言

Vue的模板编译的继续研究

模板编译

将模板渲染成函数可以分为两个步骤,先将模板解析成AST(抽象语法树),然后再使用AST生成渲染函数。但是由于静态节点在数据变化时,也不会发生改变,所以可以标记静态节点,跳过更新。所以,在大体逻辑上,模板编译分三部分内容:

1.将模板编译成AST

2.遍历AST标记静态节点

3.使用AST生成渲染函数

解析器

主要将template传入的字符串,转化为AST对象。利用stack栈的数据结构来确认DOM之间的父子关系,构建抽象语法树结构。

正则解析字符串

  • 判断字符串是不是"<"开头,如果是就可以匹配是开始标签或者结束标签
  • 如果是开始标签处理时,会顺带处理标签属性,最后匹配">"标签之后,就可以将标签放入栈中,调用传入的options.start方法。
  • 如果是结束标签时,调用传入的options.end方法。标签出栈。
  • 字符串不是"<"开头,就是文本内容,截取需要的文本,调用传入的options.chars方法。
// 属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 动态属性
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 标签名匹配
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// <div 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// div> 匹配开始标签的关闭 xxxx>
const startTagClose = /^\s*(\/?)>/
// 匹配结束标签 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
export function parseHTML(html: string, options: any) {
  // 栈结构来解析DOM结构
  const stack: any = []
  let index = 0
  // last最后的html内容, lastTag上一次的tag标签
  let last, lastTag: any
  while (html) {
    last = html
    // 以<开头
    let textEnd = html.indexOf('<')
    // 可能是开始标签,或者结束标签
    if (textEnd === 0) {
      // 是结束标签 </div>
      const endTagMatch = html.match(endTag)
      if (endTagMatch) {
        const curIndex = index
        advance(endTagMatch[0].length)
        // 调用options.end方法,标签出栈,进入下次循环
        parseEndTag(endTagMatch[1], curIndex, index)
        continue
      }
      // 获取到 1.开始标签名 2.标签属性 3.开始标签结尾,<div class="box" 解析完成
      const startTagMatch = parseStartTag()
      // 开始标签组合成<div class="box">,我们才能进一步处理开始标签
      if (startTagMatch) {
        // 调用options.start方法,标签入栈,进入下次循环
        handleStartTag(startTagMatch)
        continue
      }
    }

    let text, rest, next
    // 不是标签,说明是文本
    if (textEnd >= 0) {
      // 如果有"123<d</span>"这种文本,我们去掉文本123,剩余"<d</span>",取的文本不完整
      // 在循环中,每次找<之前的文本,看是否符合标签要求,可以截取的完整的文本"123<d"
      rest = html.slice(textEnd)
      while (
        !endTag.test(rest) &&
        !startTagOpen.test(rest)
      ) {
        next = rest.indexOf('<', 1)
        if (next < 0) break
        // 再往后截取
        textEnd += next
        rest = html.slice(textEnd)
      }
      // 截取标签之前的文本
      text = html.substring(0, textEnd)
    }

    // 不包含<,内容全是文本
    if (textEnd < 0) text = html
    
    // 截掉文本内容
    if (text) advance(text.length)
    
    // 调用options.chars,处理文本内容
    if (options.chars && text) {
      options.chars(text, index - text.length, index)
    }

    // 错误文本,语法错误
    if (html === last) {
      options.chars && options.chars(html)
      break
    }
  }

  // Clean up any remaining tags
  parseEndTag()

  // 截取字符串
  function advance(n: number) {
    index += n
    html = html.substring(n)
  }

  function parseStartTag() {
    // 是否是开始标签
    const start = html.match(startTagOpen)
    if (start) {
      const match: any = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      let end = html.match(startTagClose)
      // ...
      if (end) {
        // ...
        return match
      }
    }
  }

  function handleStartTag(match: any) {
    const tagName = match.tagName
      // 标签属性当做对象,push到stack栈中
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      // 设置上一次标签
      lastTag = tagName
    // 调用start方法。创建ast
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }

  function parseEndTag(tagName?: any, start?: any, end?: any) {
    let pos, lowerCasedTagName
    // 调用结束标签的方法
    if (options.end) {
      options.end(stack[i].tag, start, end)
    }
      // 标签出栈
      //...
      stack.pop()
      lastTag = stack.length && stack[pos - 1].tag
    }
  }
}



// 调用parseHTML方法。
这个stack是维护AST语法树的,与上面的不是同一个
cosnt stack = []
let currentParent: any
let root: any
let stack: any = []
parseHTML(html, {
  start(tag: any, attrs: any, unary: any, start: any, end: any) {
    // 创建stack语法树
    let element: any = createASTElement(tag, attrs, currentParent)
    processFor(element) // v-for
    processIf(element) // v-if
    processOnce(element) // v-once
    if (!root) root = element
    currentParent = element
    stack.push(element)
  },
  end(tag: any, start: any, end: any) {
    const element = stack[stack.length - 1]
    stack.length -= 1
    currentParent = stack[stack.length - 1]
    closeElement(element)
  },
  chars(text: string, start: number, end: number) {
    if (!currentParent) {
      return
    }
    const children = currentParent.children
    text = text.trim()
    if (text) {
      let res
      let child: any
      if ((res = parseText(text))) {
        // 带{{}}的文本,type为2
        child = {
          type: 2,
          expression: res.expression,
          tokens: res.tokens,
          text
        }
      } else {
        // 纯文本,type为3
        child = {
          type: 3,
          text
        }
      }
      if (child) {
        children.push(child)
      }
    }
  }
})

// 创建AST语法树
function createASTElement(
  tag: string,
  attrs: Array<any>,
  parent: any | void
): any {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
}

// 解析带{{}}文本内容
export function parseText(text: string): any | void {
  const tagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // 先将{{前边文本添加到tokens中
    if (index > lastIndex) {
      tokenValue = text.slice(lastIndex, index)
      tokens.push(JSON.stringify(tokenValue))
    }
    // 将花括号里面的内容当做参数,传入_s方法中,最后将作为一个表达式执行
    const exp = match[1].trim()
    tokens.push(`_s(${exp})`)
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    tokenValue = text.slice(lastIndex)
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+')
  }
}

最终的root就是一颗抽象语法树

{
    attrsList: [{…}]
    attrsMap: {class'box'}
    children: (2) [{…}, {…}]
    parentundefined
    plainfalse
    rawAttrsMap: {}
    tag"div"
    type1
}

优化器

优化器的内部实现主要分为两个步骤:

  • 在AST中找出所有静态节点并打上标记
  • 在AST中找出所有静态根节点并打上标记 静态节点:永远都不会发生变化的节点属于静态节点 静态根节点:如果一个节点下面的所有子节点都是静态节点,并且它的父级是动态节点,那么它就是静态根节点
export function optimize (root: ?ASTElement, options: CompilerOptions) {
    if (!root) return
    markStatic(root)
    markStaticRoots(root, false)
  }
  
// 标记静态节点
function markStatic (node: any) {
    node.static = isStatic(node)
    if (node.type === 1) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        const child = node.children[i]
        markStatic(child)
        // 子节点是动态节点,将父节点改为动态节点
        if (!child.static) {
          node.static = false
        }
      }
    }
}

// 标记静态根节点
function markStaticRoots (node: any) {
    if (node.type === 1) {
      // 要使节点符合静态根节点的要求,它必须有子节点
      // 这个节点不能是只有一个静态文本的子节点,否则优化成本将超过收益
      if (node.static && node.children.length && !(
        node.children.length === 1 &&
        node.children[0].type === 3
      )) {
        node.staticRoot = true
        // 找到静态根节点,直接退出
        return
      } else {
        node.staticRoot = false
      }
      if (node.children) {
        for (let i = 0, l = node.children.length; i < l; i++) {
          markStaticRoots(node.children[i])
        }
      }
    }
}

// 判断是否静态节点
function isStatic (node: any): boolean {
if (node.type === 2) { // expression
  return false
}
if (node.type === 3) { // text
  return true
}
return !!(node.pre || (
  !node.hasBindings && // 不能使用动态绑定语法
  !node.if && !node.for && // 不能使用 v-if or v-for or v-else
  !isBuiltInTag(node.tag) && // 标签名不是slot或compoennt
  isPlatformReservedTag(node.tag) && // 不能是组件
  !isDirectChildOfTemplateFor(node) && // 父节点不能是带v-for的template
  Object.keys(node).every(isStaticKey) // 不存在动态节点才有的属性
))
}

代码生成器

代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容,这个内容可以成为代码字符串。 代码字符串可以被包装在函数中执行,这个函数就是我们通常所说的渲染函数。 渲染函数被执行之后,可以生成一份VNode。

假设现有这样一个简单的模板:

Hello {{name}}

被处理完成的代码:

_c:其实就是creaElement的别名,它的作用是创建虚拟节点,其实就是我们手写render函数中的h函数。

with(this) {
    return _c(
        "div",
        {
            attrs: {"id": "el"}
        },
        [
            _v("Hello " + _s(name))
        ]
    )
}

源码实现

  • 根据ast的每一个标签信息,拼接_c函数,children的子节点,拼接在数组中
  • 根据不用type,使用不同的方法拼接节点_c(元素节点方法)、_e(文本节点方法)、_v(文本节点方法)
  • 最后将代码字符串拼在with中返回给调用者。
// 传入ast抽象语法树,生成render函数代码
export function generate(ast: any): any {
  const code = ast ? genElement(ast) : '_c("div")'
  return {
    render: `with(this){return ${code}}`
  }
}

// 生成节点的方法,第三个参数中递归生成每一个子元素
export function genElement(el: any): string {
  let code
  const data = el.plain ? undefined : genData(el)
  const children = genChildren(el)
  code = `_c('${el.tag}'${data ? `,${data}` : '' // data
    }${children ? `,${children}` : '' // children
    })`
  return code
}

// 递归生成子节点,形成虚拟dom树
export function genChildren(el: any,): string | void {
  const children = el.children
  if (children.length) {
    return `[${children.map((c: any) => genNode(c)).join(',')}]`
  }
}

// 根据类型生成不同的节点 
function genNode(node: any): string {
  if (node.type === 1) {
    return genElement(node)
  } else if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

// 生成注释节点
function genComment(comment: any): string {
  return `_e(${JSON.stringify(comment.text)})`
}

// 生成文本节点
function genText(text: any): string {
  return `_v(${text.type === 2 ? text.expression : JSON.stringify(text.text)})`
}

// 生成标签属性
export function genData(el: any): string {
  let data = '{'
  // attributes
  if (el.attrsList) {
    data += `attrs:${genProps(el.attrsList)},`
  }
  // ...
  data = data.replace(/,$/, '') + '}'
  return data
}

function genProps(props: Array<any>): string {
  let staticProps = ``
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    const value = JSON.stringify(prop.value)
    staticProps += `"${prop.name}":${value},`
  }
  staticProps = `{${staticProps.slice(0, -1)}}`
  return staticProps
}

最后

这篇文章七七八八花了一天的时间写,虽然,参考了《深入浅出Vue.js》第三篇的内容,但是还是挺花时间的,涉及到的很多复杂的代码处理都是删掉了,感兴趣的朋友还是需要自己去看看源码。2021年接近尾声了,希望大家一起加油,多多点赞,圣诞快乐~

代码地址:github.com/zhuye1993/m…