【六】Vue2 源码 — 编译详解

之前我们分析过模板到真实 DOM 渲染的过程,中间有一个环节是把模板编译成 render 函数,这个过程我们把它称作编译。

虽然我们可以直接为组件编写 render 函数,但是编写 template 模板更加直观,也更符合我们的开发习惯。

Vue.js 提供了 2 个版本,一个是 Runtime + Compiler,一个是 Runtime only。前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render 函数。

编译入口

当我们使用 Runtime + Compiler 的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js,看一下它对 $mount 函数的定义:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      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

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

这段函数逻辑之前分析过,编译的入口就是在这里:

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

compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns,它定义在 src/platforms/web/compiler/index.js 中:

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

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

可以看到 compileToFunctions 方法实际上是 createCompiler 方法的返回值,createCompiler 方法接收一个编译配置参数,接下来我们看一下 createCompiler 方法的定义,在 src/compiler/index.js 中:

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  }
})

createCompiler 方法实际上是通过调用 createCompilerCreator 方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile 函数里执行,createCompilerCreator 定义在 src/compiler/create-compiler.js 中:

export 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) {
        if (
          process.env.NODE_ENV !== 'production' &&
          options.outputSourceRange
        ) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\s*/)[0].length

          warn = (msg, range, tip) => {
            const data: WarningMessage = { msg }
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            ;(tip ? tips : errors).push(data)
          }
        }
        // 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)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

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

可以看到该方法返回了一个 createCompiler 的函数,createCompiler 接收一个 baseOptions 的参数,返回的是一个对象,包括 compile方法属性和 compileToFunctions 属性。这个 compileToFunctions 对应的就是 $mount 函数调用的 compileToFunctions 方法,它是调用 createCompileToFunctionFn 方法的返回值,createCompileToFunctionFn 定义在 src/compiler/to-function.js 中:

export function createCompileToFunctionFn(compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions(
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
              'environment with Content Security Policy that prohibits unsafe-eval. ' +
              'The template compiler cannot work in this environment. Consider ' +
              'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
              'templates into render functions.'
          )
        }
      }
    }

    // check cache
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    const compiled = compile(template, options)

    // check compilation errors/tips
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        if (options.outputSourceRange) {
          compiled.errors.forEach((e) => {
            warn(
              `Error compiling template:\n\n${e.msg}\n\n` +
                generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(
            `Error compiling template:\n\n${template}\n\n` +
              compiled.errors.map((e) => `- ${e}`).join('\n') +
              '\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {
        if (options.outputSourceRange) {
          compiled.tips.forEach((e) => tip(e.msg, vm))
        } else {
          compiled.tips.forEach((msg) => tip(msg, vm))
        }
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map((code) => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
            fnGenErrors
              .map(({ err, code }) => `${err.toString()} in\n\n${code}\n`)
              .join('\n'),
          vm
        )
      }
    }

    return (cache[key] = res)
  }
}

至此,我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数:编译模板 template,编译配置 options 和 Vue 实例 vm。核心的编译过程就一行代码:

// compile
const compiled = compile(template, options)

compile 函数在执行 createCompileToFunctionFn 的时候作为参数传入,它是 createCompiler 函数中定义的 compile 函数:

/** src/compiler/create-compiler.js */

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) {
    if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
      // $flow-disable-line
      const leadingSpaceLength = template.match(/^\s*/)[0].length

      warn = (msg, range, tip) => {
        const data: WarningMessage = { msg }
        if (range) {
          if (range.start != null) {
            data.start = range.start + leadingSpaceLength
          }
          if (range.end != null) {
            data.end = range.end + leadingSpaceLength
          }
        }
        ;(tip ? tips : errors).push(data)
      }
    }
    // 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)
  if (process.env.NODE_ENV !== 'production') {
    detectErrors(compiled.ast, warn)
  }
  compiled.errors = errors
  compiled.tips = tips
  return compiled
}

compile 函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:

const compiled = baseCompile(template.trim(), finalOptions)

baseCompile 在执行 createCompilerCreator 方法时作为参数传入,如下:

/** src/compiler/index.js */

export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  }
})

所以编译的入口我们终于找到了,它主要就是执行了如下几个逻辑:

  • 解析模板字符串生成 AST

const ast = parse(template.trim(), options)
  • 优化语法树

if (options.optimize !== false) {
  optimize(ast, options)
}
  • 生成代码

const code = generate(ast, options)

编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化的技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其他逻辑,如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。

parse

编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。

这个过程是比较复杂的,它会用到大量正则表达式对字符串解析。为了直观的演示 parse 的过程,我们先来看一个例子:

<ul :class="bindCls" class="list" v-if="isShow">
  <li v-for="(item,index) in data" @click="clickItem(index)">
    {{item}}:{{index}}
  </li>
</ul>

经过 parse 过程后,生成的 AST 如下:

ast = {
  'type': 1,
  'tag': 'ul',
  'attrsList': [],
  'attrsMap': {
    ':class': 'bindCls',
    'class': 'list',
    'v-if': 'isShow'
  },
  'if': 'isShow',
  'ifConditions': [{
    'exp': 'isShow',
    'block': // ul ast element
  }],
  'parent': undefined,
  'plain': false,
  'staticClass': 'list',
  'classBinding': 'bindCls',
  'children': [{
    'type': 1,
    'tag': 'li',
    'attrsList': [{
      'name': '@click',
      'value': 'clickItem(index)'
    }],
    'attrsMap': {
      '@click': 'clickItem(index)',
      'v-for': '(item,index) in data'
     },
    'parent': // ul ast element
    'plain': false,
    'events': {
      'click': {
        'value': 'clickItem(index)'
      }
    },
    'hasBindings': true,
    'for': 'data',
    'alias': 'item',
    'iterator1': 'index',
    'children': [
      'type': 2,
      'expression': '_s(item)+":"+_s(index)'
      'text': '{{item}}:{{index}}',
      'tokens': [
        {'@binding':'item'},
        ':',
        {'@binding':'index'}
      ]
    ]
  }]
}

可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点, children 指向它的所有子节点。先对 AST 有一些直观的印象,那么接下来我们来分析一下这个 AST 是如何得到的。

parse 整体流程

首先来看一下 parse 的定义,在 src/compiler/parser/index.js 中:

/**
 * Convert HTML string to AST.
 */
export function parse(
  template: string,
  options: CompilerOptions
): ASTElement | void {
  getFnsAndConfigFromOptions(options)

  parseHTML(template, {
    // options ...
    start(tag, attrs, unary) {
      let element = createASTElement(tag, attrs)
      processElement(element)
      treeManagement()
    },

    end() {
      treeManagement()
      closeElement()
    },

    chars(text: string) {
      handleText()
      createChildrenASTOfText()
    },
    comment(text: string) {
      createChildrenASTOfComment()
    },
  })
  return astRootElement
}

parse 函数的代码很长,先把它拆成伪代码的形式,方便对整体流程现有一个大致的了解。接下来我们就来分解分析每段伪代码的作用。

从 options 中获取方法和配置

对应伪代码:

getFnsAndConfigFromOptions(options)

parse 函数的输入是 templateoptions,输出是 AST 的根节点。template 就是我们的模板字符串,而 options 实际上是和平台相关的一些配置,它定义在 src/platforms/web/compiler/options.js 中:

import {
  isPreTag,
  mustUseProp,
  isReservedTag,
  getTagNamespace,
} from '../util/index'

import modules from './modules/index'
import directives from './directives/index'
import { genStaticKeys } from 'shared/util'
import { isUnaryTag, canBeLeftOpenTag } from './util'

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules),
}

这些属性和方法之所以放到 platforms 目录下是因为它们在不同的平台(web 和 weex)的实现是不同的。

我们用伪代码 getFnsAndConfigFromOptions 表示了这一过程,它的实际代码如下:

platformIsPreTag = options.isPreTag || no
platformMustUseProp = options.mustUseProp || no
platformGetTagNamespace = options.getTagNamespace || no
const isReservedTag = options.isReservedTag || no
maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag)

transforms = pluckModuleFunction(options.modules, 'transformNode')
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode')

delimiters = options.delimiters

这些方法和配置都是后续解析的时候需要的。

解析 HTML 模板

对应伪代码:

parseHTML(template, options)

对于 template 模板的解析主要是通过 parseHTML 函数,它定义在 src/compiler/parser/html-parser.js 中:

export function parseHTML(html, options) {
  let lastTag
  while (html) {
    if (!lastTag || !isPlainTextElement(lastTag)) {
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
        if (matchComment) {
          advance(commentLength)
          continue
        }
        if (matchDoctype) {
          advance(doctypeLength)
          continue
        }
        if (matchEndTag) {
          advance(endTagLength)
          parseEndTag()
          continue
        }
        if (matchStartTag) {
          parseStartTag()
          handleStartTag()
          continue
        }
      }
      handleText()
      advance(textLength)
    } else {
      handlePlainTextElement()
      parseEndTag()
    }
  }
}

由于 parseHTML 的逻辑也非常复杂,因此也用了伪代码的方式表达,整体来说它的逻辑就是循环解析 template,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。

function advance(n) {
  index += n
  html = html.substring(n)
}

为了更加直观地说明 advance 的作用,可以通过一幅图表示,最开始 index 是 0 :

然后调用 advance 函数:

advance(4)

得到结果:

匹配的过程中主要利用了正则表达式,如下:

// Regular Expressions for parsing tags and attributes
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${unicodeRegExp.source}]*`
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
// #7298: escape - to avoid being passed as HTML comment when inlined in page
const comment = /^<!\--/
const conditionalComment = /^<!\[/

通过这些正则表达式,我们可以匹配注释节点、文档类型节点、开始闭合标签等。

  • 注释节点、文档类型节点

对于注释节点和文档类型节点的匹配,如果匹配到,我们仅仅做的是前进即可。

// Comment:
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
  }
}

// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (conditionalComment.test(html)) {
  const conditionalEnd = html.indexOf(']>')

  if (conditionalEnd >= 0) {
    advance(conditionalEnd + 2)
    continue
  }
}

// Doctype:
const doctypeMatch = html.match(doctype)
if (doctypeMatch) {
  advance(doctypeMatch[0].length)
  continue
}

对于注释和条件注释节点,前进至它们的末位位置;对于文档类型节点,则前进它自身长度。

  • 开始标签

// Start tag:
const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
    advance(1)
  }
  continue
}

首先通过 parseStartTag 解析开始标签:

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
    }
  }
}

对于开始标签,除了标签名之外,还有一些标签相关的属性。函数先通过正则表达式 startTagOpen 匹配到开始标签,然后定义了 match 对象,接着循环去匹配开始标签中的属性并添加到 match.attrs 中,直到匹配的开始标签的闭合符结束。如果匹配到闭合符,则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给 match.end

parseStartTag 对开始标签解析拿到 match 后,紧接着会执行 handleStartTagmatch 做处理:

function handleStartTag(match) {
  const tagName = match.tagName
  const unarySlash = match.unarySlash

  if (expectHTML) {
    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
      parseEndTag(lastTag)
    }
    if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
      parseEndTag(tagName)
    }
  }

  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 (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
      attrs[i].start = args.start + args[0].match(/^\s*/).length
      attrs[i].end = args.end
    }
  }

  if (!unary) {
    stack.push({
      tag: tagName,
      lowerCasedTag: tagName.toLowerCase(),
      attrs: attrs,
      start: match.start,
      end: match.end,
    })
    lastTag = tagName
  }

  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}

handleStartTag 的核心逻辑很简单,先判断开始标签是否是一元标签,类似 <img> 、 <br/> 这样,接着对 match.attrs 遍历并做了一些处理,最后判断如果非一元标签,则往stack 里 push 一个对象,并且把 tagName 赋值给 lastTag

最后调用了 options.start 回调函数,并传入一些参数。

  • 闭合标签

// End tag:
const endTagMatch = html.match(endTag)
if (endTagMatch) {
  const curIndex = index
  advance(endTagMatch[0].length)
  parseEndTag(endTagMatch[1], curIndex, index)
  continue
}

先通过正则 endTag 匹配到闭合标签,然后前进到闭合标签末尾,然后执行 parseEndTag 方法对闭合标签做解析。

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
  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
    for (let i = stack.length - 1; i >= pos; i--) {
      if (
        process.env.NODE_ENV !== 'production' &&
        (i > pos || !tagName) &&
        options.warn
      ) {
        options.warn(`tag <${stack[i].tag}> has no matching end tag.`, {
          start: stack[i].start,
          end: stack[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') {
    if (options.start) {
      options.start(tagName, [], true, start, end)
    }
  } else if (lowerCasedTagName === 'p') {
    if (options.start) {
      options.start(tagName, [], false, start, end)
    }
    if (options.end) {
      options.end(tagName, start, end)
    }
  }
}

parseEndTag 的核心逻辑很简单,在介绍之前我们回顾一下在执行 handleStartTag 的时候,对于非一元标签(有 endTag),我们都把它构成一个对象压入到 stack 中,如图所示:

那么对于闭合标签的解析,就是倒序 stack 找到第一个和当前 endTag 匹配的元素。如果是正常的标签匹配,那么 stack 的最后一个元素应该和当前的 endTag 匹配,但是考虑到如下错误情况:

<div><span></div>

这个时候当 endTag</div> 的时候,从 stack 尾部找到的标签是 <span>,就不能匹配,因此这种情况会报警告。匹配后把栈到 pos 位置的都弹出,并从 stack 尾部拿到 lastTag

最后调用了 options.end 回到函数,并传入一些参数。

  • 文本

let text, rest, next
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)
}

if (textEnd < 0) {
  text = html
}

if (text) {
  advance(text.length)
}

if (options.chars && text) {
  options.chars(text, index - text.length, index)
}

接下来判断 textEnd 是否是大于等于 0 的,满足则说明从当前位置到 textEnd 位置都是文本,并且如果 < 是纯文本中的字符,就继续找到真正的文本结束的位置,然后前进到结束的位置。

再继续判断 textEnd 小于 0 的情况,则说明整个 template 解析完毕了,把剩余的 html 都复制给了 text

最后调用了 options.chars 回到函数,并传入 text 参数。

因此,在循环解析整个 template 的过程中,会根据不同的情况,去执行不同的回调函数。

Last updated

Was this helpful?