vue2源码之模板编译

264 阅读17分钟

什么是模板编译?

模板编译就是从我们在vue中写的html代码,找出原生的html和非原生的html(vue中的语法比如v-if,v-for等指令);经过一系列的处理生成render函数的过程就是模板编译;经过模板编译之后最终转换成虚拟节点vnode;

在模板编译过程中需要用到抽象语法树,来获描述原生的html;

什么是抽象语法树?

抽象语法树指的是源代码语法结构的一种抽象表示;个人理解按照规定的语法对真实的dom进行描述;而虚拟dom是没有规定的语法,可以随意通过js对象对真实的dom进行描述;

模板编译的流程

  1. 模板解析:把模板字符串通过正则等方式解析成抽象语法树(AST)
  2. 优化解析:从AST中找出静态节点,并且打上标记
  3. 生成render函数:将AST生成render函数

源码入口

// 源码位置 ./src/compiler/index.js

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 字符串模板解析成抽象语法树
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 处理静态节点
    optimize(ast, options)
  }
  // 生成Render函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

模板编译的入口代码非常简单,首先解析字符串模板成AST抽象语法树,再从抽象语法树中处理静态节点;最后把抽象语法树转成render函数;

模板解析生成AST

vue中解析模板的主要流程:定义模板字符串中不同类型内容的正则,通过正则从头匹配模板字符串中相应的内容,匹配到相应的内容进行处理生成对应类型的ast,处理完成之后截取剩余未匹配的模板字符串,直到模板字符串全部匹配处理完;

模板字符串中的内容

模板字符串中包含以下全部内容类型需要解析:

  • 文本类型 比如:我是张三
  • html注释类型 比如:<!--这里是注释 -->
  • html条件类型注释 比如:<!-- [if !IE]> -->我是注释<!--< ![endif] -->
  • DOCTYPE类型 比如<!DOCTYPE html>
  • 开始标签类型 比如:<div>
  • 结束标签类型 比如:</div>

以上就是模板字符串包含的内容类型,vue中根据这些类型通过相应的正则进行匹配,找出不同的类型进行处理,最后生成相应的ast树;

parse方法

parse方法就是用来解析模板,parse中调用了parseHTML方法,parseHTML方法就是解析html生成ast树;

// 源码位置: ./src/compiler/parse/index.js
export function parse (
  template,
  options
) {
    let root
    // 解析html字符串
    parseHTML(template, {
        warn,
        expectHTML: options.expectHTML,
        isUnaryTag: options.isUnaryTag,
        canBeLeftOpenTag: options.canBeLeftOpenTag, //用来检测一个标签是否是可以省略闭合标签的非自闭合标签
        shouldDecodeNewlines: options.shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
        shouldKeepComment: options.comments,
        outputSourceRange: options.outputSourceRange,
        // 开始标签的回调
        start (tag, attrs, unary, start, end) {},
        // 结束标签的回调
        end (tag, start, end) {},
        // 字符串的回调
        chars (text: string, start: number, end: number) {},
        // 注释的回调
        comment (text: string, start, end) {}
    })
    return root
}

小结: parse函数接收两个参数,template表示模板字符串,options表示需要的选项;其中parseHTML方法主要用来解析html字符串。传递了模板字符串,和一些选项参数,最主要的是后面的四个回调函数,在相应的匹配完成之后会执行相应的回调,ast的生成就在这些回调函数中;下面进行分析parseHTML方法;

解析html注释

  1. 如果模板字符串是<!-- 这里是注释 -->,那需要解析出其中的内容并且生成相应的ast树;
  • 首先定义一个匹配<!--的正则
  • 遍历模板字符串,看看当前模板字符串的第一个字符是不是<
  • 如果是,那么就判断是否开启了注释,如果是就通过正则判断是否是注释标签
  • 如果是注释标签就截取其中的注释内容
  • 把截取的内容传递给parseHtml的注释的回调生成注释类型的ast树
// 源码位置: ./src/compiler/parse/html-parse.js

// 匹配注释 <!--
const comment = /^<!\--/
const no = false
// 得到一个获取script,style,textarea属性的对象的函数
const isPlainTextElement = makeMap('script,style,textarea', true)
function parseHTML (html, options) {
  // 游标
  let index = 0
  let last, // 上一次的模板
      lastTag // 上次的标签
  // 循环模板
  while (html) {
    // 保存当前模板
    last = html
    // Make sure we're not in a plaintext content element like script/style
    // 上个标签不存在或不是script,style,textarea
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 找出文本的结束位置
      let textEnd = html.indexOf('<')
      // 不存在文本
      if (textEnd === 0) {
        // Comment:
        // 判断是否是注释元素
        if (comment.test(html)) {
          // 找出注释元素的闭合位置
          const commentEnd = html.indexOf('-->')
          // 如果存在闭合
          if (commentEnd >= 0) {
            // 如果允许显示注释元素
            if (options.shouldKeepComment) {
                  // 打印处理结果
                  console.log(html.substring(4, commentEnd), index, index + commentEnd + 3)
                  // 截取注释的内容,传递给回调函数处理
                  options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3)
            }
            // 移动游标到注释元素的末尾
            advance(commentEnd + 3)
            continue
          }
        }
      }
    }
  }

  // 截取剩余的模板字符串,并且移动游标的位置
  function advance (n) {
    // 移动游标
    index += n
    // 截取剩下的模板字符串
    html = html.substring(n)
  }
}

// 把字符串 存入到对象中并且返回一个函数判断当前字符串是否存在于对象中
function makeMap (
  str,
  expectsLowerCase
){
  const map = Object.create(null)
  const list = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

// 测试
const html = '<!-- 这里是注释 -->'
parse(html, { comments: true })

结果:

image.png

我们已经拿到注释标签的内容,下面生成注释标签的ast;生成ast都是在parse的回调中完成;

// 源码位置: ./src/compiler/parse/index.js

function parse (
  template,
  options
) {
    ...
    // 存储父标签的Ast遍历,便于测试这里假设它是有值,实际默认值为空
    // let currentParent
    let currentParent = {
      children: [],
    }
    // 解析html字符串
    parseHTML(template, {
        ...
        // 完善注释的回调
        comment (text, start, end) {
          // 如果父元素存在
          if (currentParent) {
            // 定义注释标签的ast
            const child = {
              type: 3, // 注释标签的类型
              text, // 内容
              isComment: true // 标注是注释标签
            }
            // 添加到父元素的children中
            currentParent.children.push(child)
            // 测试打印
            console.log(currentParent)
          }
        }
    })
    return root
}

// 测试
const html = '<!-- 这里是注释 -->'
parse(html, { comments: true })

结果:

image.png

  1. 如果模板字符串是<!-- [if !IE]> -->我是注释<!--< ![endif] -->,那需要解析出其中的内容并且生成相应的ast树;

解析html中的Doctype

通过正则匹配Doctype标签,如果匹配到直接删除,截取剩余部分的模板字符串,不生成ast树;

// 源码位置: ./src/compiler/parse/html-parse.js

// 创建匹配的正则
const doctype = /^<!DOCTYPE [^>]+>/i
function parseHTML (html, options) {
    ...
    while (html) {
        ...
        if (!lastTag || !isPlainTextElement(lastTag)) {
            ...
             // 找出<!DOCTYPE [^>]+> 存在就截取
            const doctypeMatch = html.match(doctype)
            if (doctypeMatch) {
              advance(doctypeMatch[0].length)
              console.log(html)
              continue
            }
        }
    }
}

// 测试
const html = '<!DOCTYPE html>删除DOCTYPE'
parse(html, { comments: true })

结果: 删除了DOCTYPE标签,截取剩下的模板字符串;

image.png

解析html中的开始标签

通过正则匹配到开始标签的头部,比如<div;匹配到之后就获取到标签名、标签中的属性 、当前标签在模板字符串中的开始和结束位置;单独处理a标签上的href属性,把href中的转义字符串转成对应的符号;如果当前标签不是单闭合标签就添加到标签栈中,调用Start回调生成ast;

// 源码位置: ./src/compiler/parse/html-parse.js

// 开始标签的正则
const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
// 属性的正则
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

const decodingMap = {
  '&lt;': '<',
  '&gt;': '>',
  '&quot;': '"',
  '&amp;': '&',
  '&#10;': '\n',
  '&#9;': '\t',
  '&#39;': "'"
}
const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
    
function parseHTML (html, options) {
    // 存储父标签名的栈
    const stack = []
    let lastTag // 上次的标签
    ...
    while (html) {
        ...
        if (!lastTag || !isPlainTextElement(lastTag)) {
            ...
            // 匹配开始标签
            // 解析开始标签获取其中的属性
            const startTagMatch = parseStartTag()
            if (startTagMatch) {
              handleStartTag(startTagMatch)
              // if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
              //   advance(1)
              // }
              continue
            }
        }
    }
    // 解析开始标签
    function parseStartTag () {
        // 匹配开始标签正则 -> 比如 <div id="app" class="dot">
        const start = html.match(startTagOpen)
        // 如果匹配到 -> ['<div', 'div', index: 0, input: '<div id="app" class="dot">', groups: undefined]
        if (start) {
          // 存储匹配的相应属性
          const match = {
            tagName: start[1],
            attrs: [],
            start: index
          }
          // 删除开始标签,截取剩余的字符串 ->  id="app" class="dot">
          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
            // 比如 -> [' id="app"', 'id', '=', 'app', undefined, undefined, index: 0, input: ' id="app" class="dot">', groups: undefined, start: 4]
            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 unary = !!unarySlash

        const l = match.attrs.length
        const attrs = new Array(l)
        // 遍历属性 找出其中的name和value进行存储
        for (let i = 0; i < l; i++) {
          const args = match.attrs[i]
          const value = args[3] || args[4] || args[5] || ''
          // 处理a标签的href属性值
          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
        }
        // 调用Start回调
        if (options.start) {
          options.start(tagName, attrs, unary, match.start, match.end)
        }
      }
      // 把href中的转义字符转成真实的符号
      function decodeAttr (value, shouldDecodeNewlines) {
        const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
        return value.replace(re, match => decodingMap[match])
      }
    }

function parse (
  template,
  options
) {
    ...
    // 解析html字符串
    parseHTML(template, {
        ...
        // 开始标签的回调
        start (tag, attrs, unary, start, end) {
          console.log('开始标签', tag, attrs, unary, start, end)
        },
    }
}

// 测试
const html = '<div id="app" class="dot"><a href="&lt;www.baidu.com ">'
parse(html, { comments: true })

结果:

image.png

完善start回调函数
  1. 生成ast树,相当于创建一个对象,把标签的名称,属性等添加到对象的相应属性中;属性数组中的每项映射到一个对象上;
// 源码位置 ./src/compiler/parse/index.js

function parse (
  template,
  options
) {
    ...
    // 解析html字符串
    parseHTML(template, {
        ...
        // 开始标签的回调
        start (tag, attrs, unary, start, end) {
          console.log('开始标签', tag, attrs, unary, start, end)
          // 创建AST抽象元素
          let element = createASTElement(tag, attrs, currentParent)
          console.log('ast', element)
        },
    }
}

// 生成标签的ast
function createASTElement (
  tag,
  attrs,
  parent
) {
  return {
    type: 1, // 类型 元素类型为1
    tag, // 标签名
    attrsList: attrs, // 属性列表
    attrsMap: makeAttrsMap(attrs), // 属性映射的对象
    rawAttrsMap: {},
    parent, // 父元素
    children: [] // 子元素
  }
}

// 把数组中的每项解析成对象的属性
function makeAttrsMap (attrs) {
  const map = {}
  for (let i = 0, l = attrs.length; i < l; i++) {
    map[attrs[i].name] = attrs[i].value
  }
  return map
}
// 测试
const html = '<div id="app" class="dot"><a href="&lt;www.baidu.com ">'
parse(html, { comments: true })

测试结果 image.png

image.png 2. v-for的解析,在属性数组列表attrsList中找出v-for的值并且在列表中进行删除,通过正则解析这个值,获取到遍历的对象,遍历的key和value,分别放在for,iterator1和alias属性上;最后把解析的属性拷贝到ast对象上;

// 源码位置 ./src/compiler/parse/index.js

// 解析v-for的正则
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g

function parse (
  template,
  options
) {
    ...
    // 解析html字符串
    parseHTML(template, {
        ...
        // 开始标签的回调
        start (tag, attrs, unary, start, end) {
          console.log('开始标签', tag, attrs, unary, start, end)
          if (!element.processed) {
            // structural directives
            /*
            解析for循环  把v-for的值进行解析,把迭代的对象放在for属性,value放在iterator属性,
            alias放在key属性
            */
            processFor(element)
            console.log('v-for', JSON.parse(JSON.stringify(element)))
           }
        },
    }
}
// 解析v-for
function processFor (el) {
  let exp
  // 得到v-for的值
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    // 解析获取到key和value
    const res = parseFor(exp)
    if (res) {
      // 拷贝到el上
      extend(el, res)
    }
  }
}
// 解析v-for
// 比如 "(key,vlaue) in list"
function parseFor (exp) {
  // 匹配in ['(key,vlaue) in list', '(key,vlaue)', 'list', index: 0,...]
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return
  const res = {}
  // 获取到遍历的对象 list
  res.for = inMatch[2].trim()
  // 获取到遍历的key和value  'key,vlaue'
  const alias = inMatch[1].trim().replace(stripParensRE, '')
  // 获取到遍历的value  [',vlaue', 'vlaue', ...]
  const iteratorMatch = alias.match(forIteratorRE)
  if (iteratorMatch) {
    // 获取到key
    res.alias = alias.replace(forIteratorRE, '').trim()
    // 获取到value
    res.iterator1 = iteratorMatch[1].trim()
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else { // 没有值 就处理key
    res.alias = alias
  }
  return res
}
function extend (to, _from) {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}
// 测试
const html = '<div v-for="(key,item) in list" id=app class="dot"><a href="&lt;www.baidu.com ">'
parse(html, { comments: true })

v-for的测试结果 image.png 3. v-if、v-else、v-else-if的解析,在属性列表中获取到v-if,v-else,v-else-if的值;v-if的值添加到ast的if属性上,并且出入到ifConditions数组中;v-else如果存在就把ast上的else设置为true;v-else-if的值添加到ast的elseif属性上;

// 源码位置 ./src/compiler/parse/index.js

function parse (
 template,
 options
) {
   ...
   // 解析html字符串
   parseHTML(template, {
       ...
       // 开始标签的回调
       start (tag, attrs, unary, start, end) {
         console.log('开始标签', tag, attrs, unary, start, end)
         if (!element.processed) {
           // 解析if
           processIf(element)
           console.log('v-if', element)
          }
       },
   }
}
// 处理v-if v-else v-if-else
function processIf (el) {
 // 获取到v-if并且在属性列表中删除
 const exp = getAndRemoveAttr(el, 'v-if')
 // 有值
 if (exp) {
   el.if = exp
   // 把值和元素存储到ifConditions属性数组中
   addIfCondition(el, {
     exp: exp,
     block: el
   })
 } else { // 没有值 就获取v-else
   if (getAndRemoveAttr(el, 'v-else') != null) {
     el.else = true
   }
   // 处理v-else-if
   const elseif = getAndRemoveAttr(el, 'v-else-if')
   if (elseif) {
     el.elseif = elseif
   }
 }
}
// 添加if条件到ifConditions中
function addIfCondition (el, condition) {
 if (!el.ifConditions) {
   el.ifConditions = []
 }
 el.ifConditions.push(condition)
}
// 测试
// const html = '<div v-if="list.length" id=app class="dot"><a href="&lt;www.baidu.com ">'
// const html = '<div v-else id=app class="dot"><a href="&lt;www.baidu.com ">'
const html = '<div v-else-if="data" id=app class="dot"><a href="&lt;www.baidu.com ">'
parse(html, { comments: true })

v-if的测试结果 image.png

v-else的测试结果: image.png

v-else-if image.png

  1. v-once的解析,获取到v-once的值,不为空就存储在ast对象上的once属性上
// 源码位置 ./src/compiler/parse/index.js

function parse (
 template,
 options
) {
   ...
   // 解析html字符串
   parseHTML(template, {
       ...
       // 开始标签的回调
       start (tag, attrs, unary, start, end) {
         console.log('开始标签', tag, attrs, unary, start, end)
         if (!element.processed) {
           ...
           // 解析once
           processOnce(element)
           console.log('v-once', element)
          }
       },
   }
}
function processOnce (el) {
 const once = getAndRemoveAttr(el, 'v-once')
 if (once != null) {
   el.once = true
 }
// 测试
const html = '<div v-once id=app class="dot"><a href="&lt;www.baidu.com ">'
parse(html, { comments: true })

结果 image.png

  1. 其他解析,如果当前没有根元素就把当前标签添加为根元素,如果当前标签不是单元素标签那么就设置为父级标签,并且添加到标签栈中,如果当前标签是单元素标签,那么进行闭合,并且解析v-on、插槽、ref、组件、v-if、v-else-if等;
// 源码位置 ./src/compiler/parse/index.js

function parse (
 template,
 options
) {
   ...
   // 解析html字符串
   parseHTML(template, {
       ...
       // 开始标签的回调
       start (tag, attrs, unary, start, end) {
         console.log('开始标签', tag, attrs, unary, start, end)
         // 没有根标签 ,设置当前元素为根标签
         if (!root) {
           root = element
         }
         // 有闭合
         if (!unary) {
           // 把当前元素设置为父元素 
           currentParent = element
           // 添加到标签栈中
           stack.push(element)
         } else {
           // 如果没有闭合,则添加闭合
           closeElement(element)
         }
       },
   }
}

解析HTML中的结束标签

通过正则匹配结束标签,如果匹配到就从标签栈中找出相同的标签并记录其所在的位置pos,不存在pos为0;如果pos大于等于0就通过for循环,从栈顶往前遍历到pos位置,如果遍历到了标签,表示这个是非闭合标签,在非生成环境下进行提示,调用end回调进行闭合处理;如果pos小于0表示是自闭合标签,再判断是否是p或br,分别进行单独处理,因为浏览器中对于</br></p>进行自动处理,把</br>转成<br>,把</p>转成<p></p>,因此为了和浏览器保持一致就进行单独处理;

// 源码位置 ./src/compiler/parse/html-parse.js

// 找出闭合标签
const endTagMatch = html.match(endTag)
if (endTagMatch) {
  const curIndex = index
  // 截取剩余的模板字符串
  advance(endTagMatch[0].length)
  // 解析闭合标签进行处理
  parseEndTag(endTagMatch[1], curIndex, index)
  continue
}

// 解析结束标签
function parseEndTag (tagName, start, end) {
    let pos, lowerCasedTagName
    if (start == null) start = index
    if (end == null) end = index

    // Find the closest opened tag of the same type
    // 如果闭合标签存在,就从标签栈中从后往前找出它的开始标签的位置pos,不存在pos为0
    if (tagName) {
      lowerCasedTagName = tagName.toLowerCase()
      for (pos = stack.length - 1; pos >= 0; pos--) {
        if (stack[pos].lowerCasedTag === lowerCasedTagName) {
          break
        }
      }
    } else {
      // If no tag name is provided, clean shop
      pos = 0
    }
    // 如果在标签栈中存在
    if (pos >= 0) {
      // Close all the open elements, up the stack
      // 找出栈顶到pos之间没有闭合的标签 进行闭合
      for (let i = stack.length - 1; i >= pos; i--) {
        // 执行end回调函数,创建闭合标签
        if (options.end) {
          options.end(stack[i].tag, start, end)
        }
      }

      // Remove the open elements from the stack
      // 标签栈中删除掉闭合标签
      stack.length = pos
      // 找出上个闭合标签(下次内容的父标签)
      lastTag = pos && stack[pos - 1].tag
    } else if (lowerCasedTagName === 'br') { // 么有闭合并且是br 处理br
      if (options.start) {
        // 通过Start回调 生成ast
        options.start(tagName, [], true, start, end)
      }
    } else if (lowerCasedTagName === 'p') { // 没有闭合如果是p 添加闭合
      if (options.start) {
        // 通过Start回调 生成ast
        options.start(tagName, [], false, start, end)
      }
      if (options.end) {
        // 通过end 回调自动添加闭合
        options.end(tagName, start, end)
      }
    }
    }
}

闭合标签的回调函数

// 源码位置 ./src/compiler/parse/index.js

function parse (
 template,
 options
) {
   ...
   // 解析html字符串
   parseHTML(template, {
       ...
       end (tag, start, end) {
         // 取出栈中最后一个标签
         const element = stack[stack.length - 1]
         // pop stack
         // 在栈中删除这个标签
         stack.length -= 1
         // 取出当前标签的父级
         currentParent = stack[stack.length - 1]
         // 添加闭合,解析插槽等指令
         closeElement(element)
       },
   }
}

解析HTML中的文本

在解析模板字符串之前,先查找了'<'的位置,如果它的位置等于0,表示是以上四种情况,如果它的位置不是0,而是在模板字符串的中间位置,表示从开始到这个位置都是文本;如果整个模板字符串中都没有'<'表示是纯文本;最后调用chars回调生成文本ast;

// 源码位置 ./src/compiler/parse/html-parse.js

// 找出文本的结束位置
let textEnd = html.indexOf('<')
let text, rest, next
// 如果开始标签大于0,标签<之前有文本
if (textEnd >= 0) {
    // 截取
    rest = html.slice(textEnd)
    // 循环判断如果不是结束,不是开始,不是注释不是条件注释,那么就是文本
    while (
      !endTag.test(rest) &&
      !startTagOpen.test(rest) &&
      !comment.test(rest) &&
      !conditionalComment.test(rest)
    ) {
      // < in plain text, be forgiving and treat it as text
      // 继续往后找<字符
      next = rest.indexOf('<', 1)
      // 如果没有找到就直接跳出循环,表示这个纯文本
      if (next < 0) break
      // 如果找到就表示<是文本中的内容,直接截取进行下次匹配,知道找到以上四种情况或全部截取完
      textEnd += next
      rest = html.slice(textEnd)
    }
    // 截取文本内容
    text = html.substring(0, textEnd)
}
    // textEnd不存在表示全是文本
    if (textEnd < 0) {
    text = html
    }
    // 进行截取
    if (text) {
    advance(text.length)
    }
    // 文本内容存在就通过Chars回调函数生成相应的ast
    if (options.chars && text) {
    options.chars(text, index - text.length, index)
}

chars回调

// 源码位置 ./src/compiler/parse/index.js

function parse (
 template,
 options
) {
   ...
      chars (text: string, start: number, end: number) {
         if (!currentParent) {
           return
         }
         const children = currentParent.children
         if (text) {
           let res
           let child
           // 解析文本中的动态变量
           if (res = parseText(text, delimiters)) {
             child = {
               type: 2, // 标记为2 表示具有动态变量的文本节点
               expression: res.expression,
               tokens: res.tokens,
               text
             }
             // 没有动态变量
           } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
             child = {
               type: 3, // 标记为3,表示纯文本
               text
             }
           }
           console.log('child', child)
           // 添加到children中
           if (child) {
             children.push(child)
           }
         }
       },
}
解析文本中的动态变量

如果文本中有动态变量就需要提取出动态变量,比如姓名为:{{ name }};,则需要提出name变量;而parseText函数是用来处理此功能的;
先看下经过parseText函数处理之后的结果是什么样子?

{
    expression:"姓名为:"+_s(name)+";",
    tokens: [
        "姓名为:",
        {'@binding': name },
        ";",
    ],
}

可以看到结果parseText处理之后返回一个对象,对象中包含expression和tokens;expression是由非变量和变量组成,变量通过_s()包裹,最后通过+按照文本的顺序进行连接;tokens是一个数组,也是由变量和非变量组成,变量作为@binding的值;

// 源码位置: ./src/compiler/parse/index.js
// 解析文本
function parseText (
  text,
  delimiters
) {
  // 得到正则
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  // 没有动态变量直接返回
  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
    // 如果当前匹配的位置大于了上个位置就进行截取
    if (index > lastIndex) {
      // 截取非变量的文本进行存储
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // 提取变量
    const exp = parseFilters(match[1].trim())
    // 进行包裹存储
    tokens.push(`_s(${exp})`)
    // 设置为@binding的值
    rawTokens.push({ '@binding': exp })
    // 重置上个位置
    lastIndex = index + match[0].length
  }
  // 如果上个位置小于文本的长度,表示还有非变量,直接提取存储
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

// 测试
const html = '姓名:{{ name }};'
parse(html, { comments: true })

结果: image.png parseText接收两个参数,一个是文本字符串,一个是指定的匹配的内容,默认是{{}},循环匹配的值,每次匹配的结果中有index属性,表示匹配到的开始位置,通过index和上次匹配的lastIndex位置进行比较,如果index大于了lastIndex表示,此区间是非变量文本,进行截取存储,把匹配到的内容通过parseFilters进行处理获取到其中的变量进行_s()包裹存储,最后把index和当前匹配结果的长度之和赋值给lastIndex,进行下次的匹配截取;全部匹配完成之后,如果lastIndex小于文本的长度,表示还有剩余的非变量,直接进行截取存储;

// 通过正则匹配的结果
const text = '姓名:{{ name }};'
match = tagRE.exec(text)
console.log(match)
// ['{{ name }}', ' name ', index: 3, input: '姓名:{{ name }};', groups: undefined]
/*
    index为3,初始lastIndex为0,截取区间的文本为'姓名:'
    把{{ name }}通过parseFilters进行解析处理得到name变量
    把lastIndex = '{{ name }}'.length + 3
    下次匹配直接截取13开始位置的字符
*/

源码中标签栈的作用

把模板字符串解析生成对应的ast,通过标签栈就可以把每个单独的ast进行关联起来,最后形成一个ast树(维护ast的层级关系);
例如解析一下模板字符串

<div><p><span><br/></p></div>

当解析<div>的时候,匹配到它是开始标签,在start回调中把它推入到stack标签栈中;解析到<p>的时候,匹配到它也是开始标签,再Start回调中把它推入到stack标签栈中;同理解析<span>推入到标签栈中;当解析到<br/>的时候,匹配到它是自闭合标签,在start回调中从栈中弹出最后一个元素span作为它的父级,把它作为span的子元素添加到children中;继续解析到</p>标签;从栈中从后往前找出p标签的开始标签的位置并且弹出p,遍历它们之间的标签span,进行自动闭合,把span作为p的子元素添加到p的children中;找到栈中最后一个元素div,把p作为div的子元素;继续解析到</div>重复p的步骤;

总结:栈有两个作用分别是维护ast的层级关系和检测当前标签是否是正确的闭合标签;

优化阶段

优化节点就是寻找静态节点和静态根节点进行标记,目的因为静态节点一旦创建就不再变化了,因此后续在diff算法中就无需进行比较,直接拷贝使用即可,这就是优化阶段存在的意义;

// 源码位置: ./src/compiler/optimizer.js

function optimize (root, options) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // 静态节点的处理
  markStatic(root)
  // 静态根节点的处理
  markStaticRoots(root, false)
}

标记静态节点

静态节点:节点中没有vue相关的指令和动态变量,只有纯文本和标签,以及样式等原始属性;

通过isStatic判断是否是静态节点,如果当前元素为节点类型,那么遍历它的子节点进行递归判断,如果子节点不是静态的,就标记当前节点也不是静态的;如果当前节点有条件判断,那么就遍历它所关联的条件的节点,如果关联的条件的节点是非静态的,那么标记此节点也是非静态的;isStatic判断了如果是动态变量文本节点就标记为非静态的,如果是纯文本节点就标记为静态的,如果带有v-pre指令就标记为静态的,如果节点上没有动态bind并且没有if、没有for、不是一个built-in,不是一个组件,不是一个带有for属性的template,并且当前节点上的属性都在指定属性的列表中,那么它就是一个静态节点否则不是;

// 源码位置: ./src/compiler/optimizer.js

// 静态节点的标记
function markStatic (node: ASTNode) {
  // 获取当前节点的是否是静态节点
  node.static = isStatic(node)
  // 如果为元素节点
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    // 
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    // 遍历节点的子元素
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      // 递归判断子元素
      markStatic(child)
      // 如果子元素不是静态的,那么标记当前元素也不是静态的
      if (!child.static) {
        node.static = false
      }
    }
    // 如果有if条件
    if (node.ifConditions) {
      // 遍历if条件
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        // 找到每个条件的元素
        const block = node.ifConditions[i].block
        // 递归判断
        markStatic(block)
        // 如果带有条件的元素不是静态的,那么当前元素也标记为不是静态的
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

const isBuiltInTag = makeMap('slot,component', true)

// 是否是静态节点
function isStatic (node) {
  // 如果是动态的变量文本节点就标记为非静态的
  if (node.type === 2) { // expression
    return false
  }
  // 纯文本节点标记为静态的
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || ( // 如果节点使用了`v-pre`指令,那就断定它是静态节点
    !node.hasBindings && // 没有动态绑定bind属性
    !node.if && !node.for && // 没有 v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // 不是一个 built-in
    isPlatformReservedTag(node.tag) && // 不是一个 component
    !isDirectChildOfTemplateFor(node) && // 父节点不能是带有 v-for 的 template 标签
    Object.keys(node).every(isStaticKey) // 节点的所有属性的 key 都必须是静态节点才有的 key,注:静态节点的key是有限的,它只能是type,tag,attrsList,attrsMap,plain,parent,children,attrs之一;
  ))
}
// 父节点不能是带有 v-for 的 template 标签
function isDirectChildOfTemplateFor (node) {
  while (node.parent) {
    node = node.parent
    if (node.tag !== 'template') {
      return false
    }
    if (node.for) {
      return true
    }
  }
  return false
}

标记静态根节点

在静态节点的基础上进行判断,静态根节点的判断条件是,当前节点是静态节点,并且不仅仅只有一个子节点,或只有一个子节点并且这个子节点不应该是纯文本节点;通过递归判断它的子节点和条件相关的节点;

function markStaticRoots (node, isInFor) {
  // 当前节点为元素节点
  if (node.type === 1) {
    // 如果是静态的或者有once标注
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    // 是静态的节点并且不是只有一个子节点并且第一个节点为纯文本节点标志为静态根节点
    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], isInFor || !!node.for)
      }
    }
    // 有条件进行递归
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

生成render函数

把ast中的每个节点通过类型的判断使用相应类型的函数进行包裹,最后使用with执行;

  • template模板
<div id='app'><p>姓名:{{ name }}<p/></div>
  • 对应的ast树
{
'type': 1,
'tag': 'div',
'attrsList': [
    {
        'name':'id',
        'value':'app',
    }
],
'attrsMap': {
  'id': 'app',
},
'static':false,
'parent': undefined,
'plain': false,
'children': [{
  'type': 1,
  'tag': 'p',
  'plain': false,
  'static':false,
  'children': [
    {
        'type': 2,
        'expression': '"姓名:"+_s(name)',
        'text': '"姓名:{{name}}',
        'static':false,
    }
  ]
}]
}
  • 生成render函数
`
with(this){
    reurn _c( // 节点类型使用_c处理
        'div',
        {
            attrs:{"id":"app"}, // 属性
        }
        [ // 子元素
            _c('p',
                {},
                [ // 子元素
                    _v("姓名 "+_s(name)) // 文本使用_v处理
                ]
            ),
        ])
}
`

_c(节点名,{属性},[子节点])

源码分析

  1. 入口
// 源码位置 src/compiler/codegen/index.js
function generate (
  ast,
  options
) {
  // 把属性展开挂载到一个实例上
  const state = new CodegenState(options)
  // 如果有ast树,不是Script就通过genElement生成render 否则就通过_c创建一个div元素
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    // 使用with执行
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

函数需要两个参数,分别是一个ast树,一个是options参数项,通过codegenState类把options挂载在实例上;判断Ast存在并且不是script通过genElement生成render,如果没有ast,就通过_c创建一个div;

  1. 解析genElement函数 根据节点的不同属性进行判断,从而调用相应的函数进行处理
// 源码位置 src/compiler/codegen/index.js
function genElement (el, state) {
  // 如果有父级 获取到pre
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  // 以下是通过不同的类型创建不同的render函数
  // 如果是静态根节点 _m()
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  // 如果带有once _o()
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  // 具有for _
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  // 具有if
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  // 是template
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  // 是slot
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else { // 否则是节点 组件
    // component or element
    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)
      }
      // 生成子节点的render
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

2.1 首先分析下处理节点
通过genData处理节点上的所有属性,通过genChildren处理它的子节点,最后通过_c进行包裹当前节点和属性和子节点;

let data
// 属性的节点
if (!el.plain || (el.pre && state.maybeComponent(el))) {
    data = genData(el, state)
}
// 生成子节点的render
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
    data ? `,${data}` : '' // data
}${
    children ? `,${children}` : '' // children
})`

2.1.1 处理节点上的属性genData 把节点上的属性通过字符串的形式拼接起来最后形成一个字符串json;对于动态的属性通过_d函数包裹;对于v-bind使用_b函数进行包裹;

// 源码位置 src/compiler/codegen/index.js

function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind dynamic argument wrap
  // v-bind with dynamic arguments must be applied using the same v-bind object
  // merge helper so that class/style/mustUseProp attrs are handled correctly.
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

2.1.2 处理子节点
判断子节点是否只有一个,如果只有一个就判断是否是组件进行标记;遍历子节点,通过判断节点的类型进行不同的函数处理,文本节点通过_v()进行包裹,注释通过_e()进行包裹;

// 源码位置 src/compiler/codegen/index.js

function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      const normalizationType = checkSkip
        ? state.maybeComponent(el) ? `,1` : `,0`
        : ``
      return `${(altGenElement || genElement)(el, state)}${normalizationType}`
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

function genNode (node: ASTNode, state: CodegenState): string {
  // 元素节点
  if (node.type === 1) {
    return genElement(node, state)
  } else if (node.type === 3 && node.isComment) { // 注释节点
    return genComment(node)
  } else { // 文本
    return genText(node)
  }
}

function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

function genComment (comment: ASTText): string {
  return `_e(${JSON.stringify(comment.text)})`
}

2.2 静态节点
静态节点通过_m进行包裹

// 源码位置 src/compiler/codegen/index.js

function genStatic (el: ASTElement, state: CodegenState): string {
  // 开启正在解析此静态节点的开关,防止递归的时候再次解析
  el.staticProcessed = true
  // Some elements (templates) need to behave differently inside of a v-pre
  // node.  All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  return `_m(${
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

2.3 v-once属性节点

// v-once
function genOnce (el: ASTElement, state: CodegenState): string {
  el.onceProcessed = true
  // 具有if
  if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.staticInFor) {
    let key = ''
    let parent = el.parent
    while (parent) {
      if (parent.for) {
        key = parent.key
        break
      }
      parent = parent.parent
    }
    if (!key) {
      return genElement(el, state)
    }
    return `_o(${genElement(el, state)},${state.onceId++},${key})`
  } else {
    return genStatic(el, state)
  }
}

2.4 v-for属性节点

 function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  el.forProcessed = true // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}

2.5 v-if属性节点

function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  const condition = conditions.shift()
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

2.6 v-slot属性节点

function genSlot (el: ASTElement, state: CodegenState): string {
  const slotName = el.slotName || '"default"'
  const children = genChildren(el, state)
  let res = `_t(${slotName}${children ? `,function(){return ${children}}` : ''}`
  const attrs = el.attrs || el.dynamicAttrs
    ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(attr => ({
        // slot props are camelized
        name: camelize(attr.name),
        value: attr.value,
        dynamic: attr.dynamic
      })))
    : null
  const bind = el.attrsMap['v-bind']
  if ((attrs || bind) && !children) {
    res += `,null`
  }
  if (attrs) {
    res += `,${attrs}`
  }
  if (bind) {
    res += `${attrs ? '' : ',null'},${bind}`
  }
  return res + ')'
}

总流程

解析模板成Ast树,处理静态节点,ast生成render函数都是在baseCompile函数中调用的;

// 源码位置 src/compiler/index.js
import { createCompilerCreator } from './create-compiler'

const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 字符串模板解析成抽象语法树
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 处理静态节点
    optimize(ast, options)
  }
  // 生成Render函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

baseCompile是放在createCompilerCreator中调用的;createCompilerCreator函数返回一个createCompiler函数,这个函数中有compile函数并且返回了这个函数,compile函数中执行了baseCompile;

 function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      if (options) {
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

      const compiled = baseCompile(template.trim(), finalOptions)
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

createCompilerCreator函数返回的createCompiler在其他地方直接被执行结构返回

// 源码位置 src/platforms/web/complier/index.js

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

compileToFunctions函数是在Vue.prototype.$mount中被调用

// 源码位置 src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 获取到元素
  el = el && query(el)

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    // 拿到template
    let template = options.template
    // 目标存在
    if (template) {
      // 如果是字符串
      if (typeof template === 'string') {
        // 有id
        if (template.charAt(0) === '#') {
          // 获取到id对应的元素作为模板
          template = idToTemplate(template)
        }
      // 如果是节点
      } else if (template.nodeType) {
        // 直接获取到其中的内容
        template = template.innerHTML
      } else {
        // 没有模板直接返回this
        return this
      }
    } else if (el) { // 如果没有模板,直接获取元素上的内容作为模板
      template = getOuterHTML(el)
    }
    if (template) {
      // 编译模板转成render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

    }
  }
  return mount.call(this, el, hydrating)
}

首先从options中获取,如果没有就直接拿传递的元素中的内容作为模板,把模板传递给compileToFunctions执行,并且返回render函数;

image.png

总结:

模板编译就是通过正则表达式匹配出不同类型(文本类型,注释类型,节点类型,条件注释,文档类型)的字符串,不断匹配不断截取剩余字符串,并且匹配出元素中的属性,生成ast抽象语法树,把ast抽象语法树通过不同类型的字符串函数进行包裹并且放入with中最终转成render函数;