【vue2.x原理剖析四】模板编译原理

157 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

compiler版本和only版本区别

compiler版本: options(template) -> AST -> render -> vdom -> UI

import App from './App'

new Vue({
  el: '#app',
  components: {App},
  template: '<App/>'
})

only版: render -> vdom -> UI

import App from './App'

new Vue({
  el: '#app',
  render: h => h(App)
})

由此可以看出,only版比complier版少两个步骤,所以only版的运行效率高,开发时为该版本,主要分析compiler版本

compiler版本和only版本方法入口

compiler版本$mount是在only版本的基础上做类似切片编程(装饰模式),调用$mount方法会先执行重写后的方法,增加compilerToFunction功能,之后再调用only版的$mount方法,这和数组的响应式拦截如出一辙

  • only版
// /src/platforms/web/runtime/index.js
Vue.prototype.$mount = function ( //公共的$mount
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating) //组件挂载
}
  • compiler版
// /src/platforms/web/entry-runtime-with-compiler.js
//缓存mount方法(only版)
const mount = Vue.prototype.$mount
// 重新定义了一次
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 相关扩展逻辑
  return mount.call(this, el, hydrating) 
}

模板编译初始化

// /src/platforms/web/entry-runtime-with-compiler.js
//缓存mount方法
const mount = Vue.prototype.$mount
// 重新定义了一次
Vue.prototype.$mount = function (el,hydrating) {
  el = el && query(el)
  //判断dom元素是body或者是文档会报错,vue不可以直接挂载,会被覆盖整个文档
  if (el === document.body || el === document.documentElement) {
    return this
  }

  const options = this.$options
  // 判断是否有render函数
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        // 获取要挂载的节点
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
        //如果是dom直接取内容
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        return this
      }
      //如果options里没有写template就利用outerHTML API取到外部模板
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      // 增加compilerToFunction功能
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 得到render函数,将template模板转化成render函数
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating) 
}

模板转化成render函数

先将template转化成ast,再将ast转化成render函数,在转化ast后有一步优化静态节点的操作,它对parse解析后的AST进行了优化,标记了静态节点和静态根节点,这样这些静态根节点就不需要参与第二次的页面渲染了,大大提升了渲染效率,其主要是递归遍历如果是一个普通文本的话就直接标记为静态节点,如果既不是表达式也不是文本节点,就说明这是一个标签,有子节点,就根据这个标签上的一些一些属性或者标签名等判断是不是一个静态节点。

// src/compiler/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 生成ast树
  const ast = parse(template.trim(), options)
  // 优化静态节点 
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 将ast转化成render函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

生成ast

// /src/compiler/parser/index.js
// 创建ast
export function createASTElement (tag,attrs, parent) {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent,
    children: []
  }
}

parseHTML(template, {
  ..., 
  start(tag, attrs, unary, start,end){
    ...
    // 将传入的template转化成ast树
    let element = createASTElement(tag, attrs, currentParent)
    ...
  }
})
// /src/compiler/parser/html-parser.js
// 匹配属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 匹配标签名
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
// 匹配特殊标签
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
// 匹配标签开始
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 匹配标签结束
const startTagClose = /^\s*(\/?)>/
// 匹配结束标签</p>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
// 匹配<!-
const comment = /^<!\--/
// 匹配<![
const conditionalComment = /^<!\[/
...

export function parseHTML (html, options) {
  const stack = []
  const expectHTML = options.expectHTML
  const isUnaryTag = options.isUnaryTag || no
  const canBeLeftOpenTag = options.canBeLeftOpenTag || no
  let index = 0
  let last, lastTag
  // 如果存在template就迭代解析template
  while (html) {
    last = html
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 查找'<'
      let textEnd = html.indexOf('<')
      // 如果在第一个位置,那么他就是一个标签
      if (textEnd === 0) {
        // 过滤html注释<!-- -->
        if (comment.test(html)) {
          const commentEnd = html.indexOf('-->')
          if (commentEnd >= 0) {
            if (options.shouldKeepComment) {
              options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            advance(commentEnd + 3)
            continue
          }
        }
        // 过滤条件注释
        if (conditionalComment.test(html)) {
          const conditionalEnd = html.indexOf(']>')
          if (conditionalEnd >= 0) {
            advance(conditionalEnd + 2)
            continue
          }
        }
        // 过滤声明
        const doctypeMatch = html.match(doctype)
        if (doctypeMatch) {
          advance(doctypeMatch[0].length)
          continue
        }
        // 匹配结束标签
        const endTagMatch = html.match(endTag)
        if (endTagMatch) {
          const curIndex = index
          advance(endTagMatch[0].length)
          // 处理结束标签 例如</br>等这种
          parseEndTag(endTagMatch[1], curIndex, index)
          continue
        }
        // 匹配开始标签 拿到开始标签中的内容
        const startTagMatch = parseStartTag()
        if (startTagMatch) {
          // 处理开始标签
          handleStartTag(startTagMatch)
          if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
            advance(1)
          }
          continue
        }
      }
      // 处理标签内的内容 aaa</p>
      let text, rest, next
      if (textEnd >= 0) {
        rest = html.slice(textEnd)
        while (
          !endTag.test(rest) &&
          !startTagOpen.test(rest) &&
          !comment.test(rest) &&
          !conditionalComment.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)
      }

      if (options.chars && text) {
        options.chars(text, index - text.length, index)
      }
    } else {
      ...
    }
  }
  // 截取字符串,将匹配到的截取掉
  function advance (n) {
    index += n
    html = html.substring(n)
  }
  // 匹配解析开始标签内的内容
  function parseStartTag () {
    const start = html.match(startTagOpen)
    if (start) {
      const match = {
        tagName: start[1],
        attrs: [],
        start: index
      }
      advance(start[0].length)
      let end, attr
      // 如果没有遇到结束符号就不停的解析
      while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
        attr.start = index
        advance(attr[0].length)
        attr.end = index
        match.attrs.push(attr)
      }
      if (end) {
        match.unarySlash = end[1]
        advance(end[0].length)
        match.end = index
        return match
      }
    }
  }

  // 处理开始标签的内容将其格式化一下
  function handleStartTag (match) {
    const tagName = match.tagName
    const unarySlash = match.unarySlash
    const unary = isUnaryTag(tagName) || !!unarySlash
    const l = match.attrs.length
    const attrs = new Array(l)
    for (let i = 0; i < l; i++) {
      const args = match.attrs[i]
      const value = args[3] || args[4] || args[5] || ''
      const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
        ? options.shouldDecodeNewlinesForHref
        : options.shouldDecodeNewlines
      attrs[i] = {
        name: args[1],
        value: decodeAttr(value, shouldDecodeNewlines)
      }
    }

    if (!unary) {
      stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end })
      lastTag = tagName
    }
    // 将解析后的结果转化为ast
    if (options.start) {
      options.start(tagName, attrs, unary, match.start, match.end)
    }
  }
}

ast转化成render

核心是利用withnew Function生成模板编译引擎(render函数),会先将之前的ast拼接成字符串(_c('div', {id: 'app'}, _c('span', {}, 'world'), _v('hello'))),再让其执行

// /src/compiler/codegen/index.js
export function generate ( ast,options) {
  const state = new CodegenState(options)
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

export function genElement (el, state) {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  //针对不同的指令或标签进行不同的处理
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
      // 遍历树,将树拼成字符串
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

总结

compiler版的$mount函数基于only版的$mount函数做了功能扩展,多了将template转化成ast,再将ast转化成render函数两个过程。生成ast语法树时会利用正则对传入的template,进行正则匹配出开头结尾等内容,不断迭代找出标签名以及属性,文本,然后将匹配过的进行截取,知道解析完成template,在匹配过程中利用栈结构储存相关信息,组装形成一个ast树。在转化成render函数之前会对ast树进行优化静态节点的操作,它对parse解析后的AST进行了优化,标记了静态节点和静态根节点,这样这些静态根节点就不需要参与第二次的页面渲染了,大大提升了渲染效率。之后将ast树通过遍历树将其拼接成模板字符串,最后利用with+new Function得到render函数。

系列链接

【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理
【Vue2.x原理剖析六】diff算法原理
【Vue2.x原理剖析七】组件原理